Skip to content
This repository was archived by the owner on Jul 11, 2025. It is now read-only.

Commit c948d4f

Browse files
committed
Initial commit
1 parent d4b9c4a commit c948d4f

11 files changed

Lines changed: 624 additions & 0 deletions

File tree

.github/dependabot.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# To get started with Dependabot version updates, you'll need to specify which
2+
# package ecosystems to update and where the package manifests are located.
3+
# Please see the documentation for all configuration options:
4+
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5+
6+
version: 2
7+
updates:
8+
9+
# Maintain dependencies for GitHub Actions
10+
- package-ecosystem: "github-actions"
11+
directory: "/"
12+
schedule:
13+
interval: "weekly"

.github/workflows/ci.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: GraphQLStandardSchema
2+
on:
3+
push:
4+
5+
jobs:
6+
gqlstdschema:
7+
name: Install
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: denoland/setup-deno@v1
12+
with:
13+
deno-version: '2.1.9'
14+
- name: Cache https://
15+
uses: actions/cache@v4
16+
with:
17+
path: ~/.cache/deno/deps/https
18+
key: deno-https/v1-${{ github.sha }}
19+
restore-keys: deno-https/v1-
20+
- run: deno task lint
21+
- run: deno task coverage
22+
- name: Get tag version
23+
if: startsWith(github.ref, 'refs/tags/')
24+
id: get_tag_version
25+
run: echo TAG_VERSION=${GITHUB_REF/refs\/tags\//} >> $GITHUB_OUTPUT
26+
- uses: actions/setup-node@v4
27+
with:
28+
node-version: '22.x'
29+
registry-url: 'https://registry.npmjs.org'
30+
- name: npm build
31+
run: deno run -A ./scripts/build_npm.ts ${{steps.get_tag_version.outputs.TAG_VERSION}}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.vscode/settings.json
2+
npm/
3+
covresults

GraphQLStandardSchema.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
2+
3+
export interface ValidationOptions {
4+
/**
5+
* In a GraphQL response only the fields "data", "errors" and "extensions" are allowed. When set to true
6+
* this checks allows additional fields. According to GraphQL spec October 2021 this should not be the case.
7+
*/
8+
allowAdditionalFieldsInResponse?: boolean
9+
10+
/**
11+
* Defines if both "errors" and "data" fields are allowed in the response.
12+
* According to GraphQL spec October 2021 this should not be the case.
13+
*/
14+
allowBothErrorsAndDataFields?: boolean
15+
16+
/**
17+
* If value is a string, define if it should be parsed to an object. Otherwise an issue will be created.
18+
*/
19+
parseStringToObject?: boolean
20+
}
21+
22+
/**
23+
* Checks for issues in the response object with the given options
24+
* @param {any} response - The response object
25+
* @param {ValidationOptions} options - The optional validation options for the schema validation
26+
* @returns
27+
*/
28+
export function findIssues(
29+
// deno-lint-ignore no-explicit-any
30+
response: any,
31+
options?: ValidationOptions,
32+
): StandardSchemaV1.Issue[] {
33+
const foundIssues: StandardSchemaV1.Issue[] = []
34+
35+
if (!response.data && !response.errors) {
36+
foundIssues.push({
37+
message:
38+
'GraphQL response should contain at least one of data or error field, but both are missing.',
39+
})
40+
} else if (
41+
response.data && response.errors &&
42+
(!options || !options.allowBothErrorsAndDataFields)
43+
) {
44+
foundIssues.push({
45+
message:
46+
'GraphQL response contains both data and errors fields but should contain only one of them.',
47+
})
48+
}
49+
50+
if (
51+
response.data && response.data !== null &&
52+
typeof response.data !== 'object'
53+
) {
54+
foundIssues.push({
55+
message:
56+
`GraphQL response contains "data" field of type "${typeof response
57+
.data}" but "data" should be an "object".`,
58+
})
59+
}
60+
61+
if (
62+
response.errors &&
63+
!Array.isArray(response.errors)
64+
) {
65+
foundIssues.push({
66+
message:
67+
`GraphQL response contains "errors" field of type "${typeof response
68+
.errors}" but "errors" should be an "array".`,
69+
})
70+
}
71+
72+
if (
73+
response.errors &&
74+
Array.isArray(response.errors) &&
75+
response.errors.length < 1
76+
) {
77+
foundIssues.push({
78+
message:
79+
'GraphQL response contains empty "errors" field but should at least have one error entry.',
80+
})
81+
}
82+
83+
if (
84+
response.extensions &&
85+
typeof response.extensions !== 'object'
86+
) {
87+
foundIssues.push({
88+
message:
89+
`GraphQL response contains "extensions" field of type "${typeof response
90+
.extensions}" but "extensions" should be an "object".`,
91+
})
92+
}
93+
94+
if (!options || !options.allowAdditionalFieldsInResponse) {
95+
const additionalProperties = Object.getOwnPropertyNames(
96+
response,
97+
).filter((property) =>
98+
property !== 'data' && property !== 'errors' &&
99+
property !== 'extensions'
100+
)
101+
if (additionalProperties.length > 0) {
102+
foundIssues.push({
103+
message:
104+
`GraphQL response should contain only "data", "errors" or "extensions" fields but has the following fields: "${additionalProperties}".`,
105+
})
106+
}
107+
}
108+
return foundIssues
109+
}
110+
111+
/**
112+
* Creates a GraphQL response schema
113+
* @param {ValidationOptions} options - The optional validation options for the schema validation
114+
* @returns {StandardSchemaV1<object>} The GraphQL response Standard Schema
115+
*/
116+
export function graphQLResponseSchema(
117+
options?: ValidationOptions,
118+
): StandardSchemaV1<object | string> {
119+
return {
120+
'~standard': {
121+
version: 1,
122+
vendor: 'dreamit',
123+
validate(value) {
124+
/*
125+
* Check type of value. If value is a string and parseStringToObject option is enabled try to parse it
126+
* to an object so we can check if. If it is neither an object nor a string return an issue.
127+
*/
128+
let responseAsObject
129+
if (typeof value === 'string') {
130+
if (!options || !options.parseStringToObject) {
131+
return {
132+
issues: [
133+
{
134+
message:
135+
'Provided value is a string and "parseStringToObject" option is disabled',
136+
},
137+
],
138+
}
139+
} else {
140+
try {
141+
responseAsObject = JSON.parse(value)
142+
} catch (error) {
143+
return {
144+
issues: [
145+
{
146+
message:
147+
`String value could not be parsed to object. Error is ${error}`,
148+
},
149+
],
150+
}
151+
}
152+
}
153+
} else if (typeof value === 'object') {
154+
responseAsObject = value
155+
}
156+
157+
const foundIssues = findIssues(responseAsObject, options)
158+
159+
return foundIssues.length > 0
160+
? { issues: foundIssues }
161+
: { value: responseAsObject }
162+
},
163+
},
164+
}
165+
}

GraphQLStandardSchema_test.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { findIssues, graphQLResponseSchema } from './GraphQLStandardSchema.ts'
2+
import { assertEquals, assertRejects } from '@std/assert'
3+
import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
4+
5+
// Helper function from https://standardschema.dev/ to validate an input
6+
export async function standardValidate<T extends StandardSchemaV1>(
7+
schema: T,
8+
input: StandardSchemaV1.InferInput<T>,
9+
): Promise<StandardSchemaV1.InferOutput<T>> {
10+
let result = schema['~standard'].validate(input)
11+
if (result instanceof Promise) result = await result
12+
13+
// if the `issues` field exists, the validation failed
14+
if (result.issues) {
15+
throw new Error(JSON.stringify(result.issues, null, 2))
16+
}
17+
18+
return result.value
19+
}
20+
21+
Deno.test('GraphQLStandardSchema should work as expected when no options are provided', async () => {
22+
const schema = graphQLResponseSchema()
23+
24+
assertEquals(schema['~standard'].vendor, 'dreamit')
25+
assertEquals(schema['~standard'].version, 1)
26+
27+
// Case: Value is a string and parseStringToObject is false
28+
await assertRejects(
29+
async () => {
30+
await standardValidate(schema, 'string')
31+
},
32+
Error,
33+
'Provided value is a string and \\"parseStringToObject\\" option is disabled',
34+
)
35+
36+
// Case: Value is an object but undefined
37+
await assertRejects(
38+
async () => {
39+
await standardValidate(schema, {})
40+
},
41+
Error,
42+
'GraphQL response should contain at least one of data or error field, but both are missing.',
43+
)
44+
45+
// Case: Data is set and valid
46+
assertEquals(
47+
await standardValidate(schema, { data: { message: 'OK' } }),
48+
{ data: { message: 'OK' } },
49+
)
50+
})
51+
52+
Deno.test('GraphQLStandardSchema should work as expected when parseStringToObject is enabled', async () => {
53+
const schema = graphQLResponseSchema({ parseStringToObject: true })
54+
55+
// Case: Value is a string and parseStringToObject is false
56+
await assertRejects(
57+
async () => {
58+
await standardValidate(schema, 'string')
59+
},
60+
Error,
61+
'String value could not be parsed to object. Error is SyntaxError: Unexpected token \'s\', \\"string\\" is not valid JSON"',
62+
)
63+
})
64+
65+
Deno.test('findIssues should find expected issues', () => {
66+
// Case: Both data and errors missing
67+
assertEquals(
68+
findIssues({}).at(0)?.message,
69+
'GraphQL response should contain at least one of data or error field, but both are missing.',
70+
)
71+
72+
// Case: Both data and errors set and allowBothErrorsAndDataFields is false
73+
assertEquals(
74+
findIssues({ data: { message: 'data' }, errors: ['errors'] }).at(0)
75+
?.message,
76+
'GraphQL response contains both data and errors fields but should contain only one of them.',
77+
)
78+
// Case: Both data and errors set and allowBothErrorsAndDataFields is true
79+
assertEquals(
80+
findIssues({ data: { message: 'data' }, errors: ['errors'] }, {
81+
allowBothErrorsAndDataFields: true,
82+
}).length,
83+
0,
84+
)
85+
86+
// Case: Errors is set but empty
87+
assertEquals(
88+
findIssues({ errors: [] }).at(0)
89+
?.message,
90+
'GraphQL response contains empty "errors" field but should at least have one error entry.',
91+
)
92+
93+
// Case: Data is set but not an object
94+
assertEquals(
95+
findIssues({ data: 'string' }).at(0)
96+
?.message,
97+
'GraphQL response contains "data" field of type "string" but "data" should be an "object".',
98+
)
99+
100+
// Case: Data is set but not an object
101+
assertEquals(
102+
findIssues({ errors: 'string' }).at(0)
103+
?.message,
104+
'GraphQL response contains "errors" field of type "string" but "errors" should be an "array".',
105+
)
106+
107+
// Case: Extensions is set but not an object
108+
assertEquals(
109+
findIssues({ data: {}, extensions: 'string' }).at(0)
110+
?.message,
111+
'GraphQL response contains "extensions" field of type "string" but "extensions" should be an "object".',
112+
)
113+
114+
// Case: Additional field myCustomField is set and allowAdditionalFieldsInResponse is false
115+
assertEquals(
116+
findIssues({ data: {}, myCustomField: 'string' }).at(0)
117+
?.message,
118+
'GraphQL response should contain only "data", "errors" or "extensions" fields but has the following fields: "myCustomField".',
119+
)
120+
// Case: Additional field myCustomField is set and allowAdditionalFieldsInResponse is true
121+
assertEquals(
122+
findIssues({ data: {}, myCustomField: 'string' }, {
123+
allowAdditionalFieldsInResponse: true,
124+
}).length,
125+
0,
126+
)
127+
})

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
11
# graphql-std-schema
2+
23
Standard Schema for GraphQL response
4+
5+
## Run
6+
7+
Only tests are executable. To run tests execute **deno test**.
8+
9+
## Lint
10+
11+
To check for linting issues execute **deno lint**.
12+
13+
## Build npm library
14+
15+
To build an npm library execute **deno run -A scripts/build_npm.ts 0.0.1**
16+
replacing 0.0.1 with the version you wish to build. Library will be located in
17+
"/npm" folder. Run **npm pack** in npm folder to create a tar archive of the
18+
library and add them to the projects you want to use the PersonService in.

0 commit comments

Comments
 (0)