@znight/graphql 0.0.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1346 -0
- package/dist/compiler.d.mts +114 -0
- package/dist/compiler.mjs +4599 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/index.d.mts +583 -0
- package/dist/index.mjs +134 -0
- package/dist/index.mjs.map +1 -0
- package/dist/names-BlzDM5cF.mjs +111 -0
- package/dist/names-BlzDM5cF.mjs.map +1 -0
- package/dist/plugin-CR_wVfRU.mjs +3445 -0
- package/dist/plugin-CR_wVfRU.mjs.map +1 -0
- package/dist/testing.d.mts +101 -0
- package/dist/testing.mjs +306 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-DQ713UgI.d.mts +266 -0
- package/package.json +97 -1
- package/src/compiler/analyze.ts +727 -0
- package/src/compiler/builder.ts +1561 -0
- package/src/compiler/checks.ts +105 -0
- package/src/compiler/conformance.ts +1068 -0
- package/src/compiler/constants.ts +80 -0
- package/src/compiler/data.ts +83 -0
- package/src/compiler/defaults.ts +92 -0
- package/src/compiler/discriminators.ts +53 -0
- package/src/compiler/docs.ts +55 -0
- package/src/compiler/generate.ts +419 -0
- package/src/compiler/graphql-js.ts +42 -0
- package/src/compiler/ids.ts +128 -0
- package/src/compiler/index.ts +98 -0
- package/src/compiler/locate.ts +82 -0
- package/src/compiler/model.ts +137 -0
- package/src/compiler/options.ts +85 -0
- package/src/compiler/scalars.ts +138 -0
- package/src/compiler/schema-first.ts +595 -0
- package/src/compiler/sdl-source.ts +529 -0
- package/src/compiler/sdl-view.ts +61 -0
- package/src/compiler/suggest.ts +39 -0
- package/src/context.ts +181 -0
- package/src/decisions.ts +169 -0
- package/src/decorators.ts +202 -0
- package/src/errors.ts +89 -0
- package/src/execution.ts +1064 -0
- package/src/format.ts +285 -0
- package/src/index.ts +89 -0
- package/src/limits.ts +177 -0
- package/src/loaders.ts +149 -0
- package/src/memory-pubsub.ts +133 -0
- package/src/metadata.ts +222 -0
- package/src/names.ts +47 -0
- package/src/options.ts +331 -0
- package/src/pipeline.ts +338 -0
- package/src/plugin.ts +579 -0
- package/src/pubsub.ts +36 -0
- package/src/results.ts +206 -0
- package/src/scalars.ts +138 -0
- package/src/schema-hash.ts +18 -0
- package/src/schema.ts +635 -0
- package/src/sdl.ts +530 -0
- package/src/testing.ts +454 -0
- package/src/tokens.ts +22 -0
- package/src/transport/abort.ts +80 -0
- package/src/transport/accept.ts +155 -0
- package/src/transport/graphiql.ts +106 -0
- package/src/transport/http.ts +373 -0
- package/src/transport/sse.ts +45 -0
- package/src/type-ref.ts +89 -0
- package/src/types.ts +225 -0
- package/src/validate-args.ts +48 -0
package/src/results.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors as data (RFC 0008): result mappers turn the error of a field into a value of its union.
|
|
3
|
+
*
|
|
4
|
+
* Decided at boot, per resolver-backed field: a field carries results when its type, without its
|
|
5
|
+
* non-null wrapper, is a union (not a list) holding an object type declared by a result mapper
|
|
6
|
+
* (`static types`). Only those fields get a wrapper (`pipeline.ts`); the others keep their resolve
|
|
7
|
+
* function. On an error, the applicable mappers run in order; the first value resolving to a member
|
|
8
|
+
* of the union is the field's value, otherwise the error is thrown back, to `errors[]`.
|
|
9
|
+
*/
|
|
10
|
+
import type { Container, CoreLogger } from '@znight/core'
|
|
11
|
+
import type { GraphQLResolveInfo } from 'graphql'
|
|
12
|
+
import type { Resolvable } from './format.ts'
|
|
13
|
+
import type { GraphQLFieldDefinition, GraphQLTypeDefinition } from './metadata.ts'
|
|
14
|
+
import type { GraphQLResultMapper, ResolverContext } from './types.ts'
|
|
15
|
+
|
|
16
|
+
import { ErrorCode, ZnightError } from '@znight/core'
|
|
17
|
+
import { isThenable } from '@znight/http'
|
|
18
|
+
|
|
19
|
+
import { resolveIn } from './format.ts'
|
|
20
|
+
import { typeResolver } from './schema.ts'
|
|
21
|
+
import { parseTypeRef } from './type-ref.ts'
|
|
22
|
+
|
|
23
|
+
/** A result mapper resolved at boot. */
|
|
24
|
+
export interface ResultMapperEntry {
|
|
25
|
+
readonly resolvable: Resolvable<GraphQLResultMapper>
|
|
26
|
+
/** For logs: the class name. */
|
|
27
|
+
readonly name: string
|
|
28
|
+
/** Declared object types, checked by {@link checkResultTypes}. */
|
|
29
|
+
readonly types: readonly string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The value of a field for an error, or the error thrown back. */
|
|
33
|
+
export type Recover = (error: unknown, ctx: ResolverContext) => unknown
|
|
34
|
+
|
|
35
|
+
/** The object types a result mapper ref declares: `static types` of a class; for an instance, its `types` property or its class's. */
|
|
36
|
+
export function declaredTypes(ref: unknown): unknown {
|
|
37
|
+
try {
|
|
38
|
+
if (typeof ref === 'function') return (ref as { types?: unknown }).types
|
|
39
|
+
const own = (ref as { types?: unknown }).types
|
|
40
|
+
if (own !== undefined) return own
|
|
41
|
+
return ((ref as { constructor?: unknown }).constructor as { types?: unknown } | undefined)?.types
|
|
42
|
+
} catch {
|
|
43
|
+
// a throwing getter: reported as no types
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Checks the declared types of the result mappers against the schema's types: a non-empty array of
|
|
50
|
+
* names of object types. Every problem in one ZNIGHT066 error.
|
|
51
|
+
*/
|
|
52
|
+
export function checkResultTypes(
|
|
53
|
+
mappers: readonly { readonly name: string; readonly types: unknown }[],
|
|
54
|
+
types: readonly GraphQLTypeDefinition[],
|
|
55
|
+
): void {
|
|
56
|
+
if (mappers.length === 0) return
|
|
57
|
+
const kinds = new Map(types.map(type => [type?.name, type?.kind]))
|
|
58
|
+
const problems: string[] = []
|
|
59
|
+
for (const mapper of mappers) {
|
|
60
|
+
const declared = mapper.types
|
|
61
|
+
if (!Array.isArray(declared) || declared.length === 0) {
|
|
62
|
+
problems.push(
|
|
63
|
+
`the result mapper ${mapper.name} declares no types (static readonly types = [...object type names])`,
|
|
64
|
+
)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
for (const name of declared as unknown[]) {
|
|
68
|
+
if (typeof name !== 'string' || kinds.get(name) !== 'object') {
|
|
69
|
+
problems.push(
|
|
70
|
+
`the result mapper ${mapper.name} declares ${typeof name === 'string' ? name : String(name)}, which is not an object type of the schema`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (problems.length === 0) return
|
|
76
|
+
throw new ZnightError(ErrorCode.InvalidGraphQLSetup, `GraphQLPlugin: ${problems.join('; ')}`, {
|
|
77
|
+
hints: [
|
|
78
|
+
"A result mapper declares the object types its values may have: static readonly types = ['NotFoundRejection'] (RFC 0008)",
|
|
79
|
+
],
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The union of a field type and its members, by union name. */
|
|
84
|
+
interface Union {
|
|
85
|
+
readonly name: string
|
|
86
|
+
readonly members: ReadonlySet<string>
|
|
87
|
+
readonly typeOf: (value: unknown) => string | undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Decides which fields carry results, and builds their {@link Recover} functions. */
|
|
91
|
+
export class ResultMappers {
|
|
92
|
+
readonly #container: Container
|
|
93
|
+
readonly #logger: CoreLogger
|
|
94
|
+
readonly #mappers: readonly ResultMapperEntry[]
|
|
95
|
+
readonly #types: readonly GraphQLTypeDefinition[]
|
|
96
|
+
/** Built on the first field, once the schema construction checked the section's structure. */
|
|
97
|
+
#index: { readonly objects: ReadonlySet<string>; readonly unions: ReadonlyMap<string, Union> } | undefined
|
|
98
|
+
#fields = 0
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
container: Container,
|
|
102
|
+
logger: CoreLogger,
|
|
103
|
+
mappers: readonly ResultMapperEntry[],
|
|
104
|
+
types: readonly GraphQLTypeDefinition[],
|
|
105
|
+
) {
|
|
106
|
+
this.#container = container
|
|
107
|
+
this.#logger = logger
|
|
108
|
+
this.#mappers = mappers
|
|
109
|
+
this.#types = types
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Fields that carry results so far. */
|
|
113
|
+
get fields(): number {
|
|
114
|
+
return this.#fields
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The {@link Recover} function of `field`, `undefined` when it carries no result. */
|
|
118
|
+
recover(field: GraphQLFieldDefinition, coordinate: string): Recover | undefined {
|
|
119
|
+
if (this.#mappers.length === 0) return undefined
|
|
120
|
+
const ref = parseTypeRef(field.type)
|
|
121
|
+
if (ref?.kind !== 'named') return undefined
|
|
122
|
+
this.#index ??= index(this.#types)
|
|
123
|
+
const objects = this.#index.objects
|
|
124
|
+
const union = this.#index.unions.get(ref.name)
|
|
125
|
+
if (union === undefined) return undefined
|
|
126
|
+
const applicable = this.#mappers.filter(mapper => mapper.types.some(type => union.members.has(type)))
|
|
127
|
+
if (applicable.length === 0) return undefined
|
|
128
|
+
this.#fields++
|
|
129
|
+
const container = this.#container
|
|
130
|
+
const logger = this.#logger
|
|
131
|
+
return (error, ctx) => {
|
|
132
|
+
for (const mapper of applicable) {
|
|
133
|
+
let value: unknown
|
|
134
|
+
let type: string | undefined
|
|
135
|
+
try {
|
|
136
|
+
value = resolveIn(container, mapper.resolvable).toResult(error, ctx)
|
|
137
|
+
if (value === undefined || value === null) continue
|
|
138
|
+
if (isThenable(value)) {
|
|
139
|
+
// not awaited: the field cannot wait for a mapper, and its rejection must not go unhandled
|
|
140
|
+
Promise.resolve(value).catch(noop)
|
|
141
|
+
logger.error(
|
|
142
|
+
`GraphQLPlugin: the result mapper ${mapper.name} returned a promise: toResult() must be synchronous`,
|
|
143
|
+
details(ctx, coordinate, error),
|
|
144
|
+
)
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
// the discriminator reads the value (a getter, a class check): a failure is the mapper's
|
|
148
|
+
type = union.typeOf(value)
|
|
149
|
+
} catch (failure) {
|
|
150
|
+
logger.error(`GraphQLPlugin: the result mapper ${mapper.name} failed`, details(ctx, coordinate, failure))
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
if (type !== undefined && union.members.has(type)) {
|
|
154
|
+
// the business path: no request id (it may be minted on first read) nor path built for a debug line
|
|
155
|
+
logger.debug(`GraphQL: error resolved as ${type} in ${coordinate}`, { coordinate, type })
|
|
156
|
+
return value
|
|
157
|
+
}
|
|
158
|
+
if (type !== undefined && objects.has(type)) {
|
|
159
|
+
logger.debug(
|
|
160
|
+
`GraphQLPlugin: the result mapper ${mapper.name} returned a value of ${type}, not a member of ${union.name} (${coordinate}): skipped`,
|
|
161
|
+
details(ctx, coordinate, error),
|
|
162
|
+
)
|
|
163
|
+
} else {
|
|
164
|
+
logger.warn(
|
|
165
|
+
`GraphQLPlugin: the result mapper ${mapper.name} returned a value of no type of the schema (${coordinate}): skipped`,
|
|
166
|
+
details(ctx, coordinate, error),
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw error
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function index(types: readonly GraphQLTypeDefinition[]): {
|
|
176
|
+
readonly objects: ReadonlySet<string>
|
|
177
|
+
readonly unions: ReadonlyMap<string, Union>
|
|
178
|
+
} {
|
|
179
|
+
const objects = new Set<string>()
|
|
180
|
+
const unions = new Map<string, Union>()
|
|
181
|
+
for (const type of types) {
|
|
182
|
+
if (type.kind === 'object') objects.add(type.name)
|
|
183
|
+
else if (type.kind === 'union') {
|
|
184
|
+
unions.set(type.name, {
|
|
185
|
+
name: type.name,
|
|
186
|
+
members: new Set(type.members),
|
|
187
|
+
typeOf: typeResolver(type.discriminator),
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { objects, unions }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function noop(): void {}
|
|
195
|
+
|
|
196
|
+
function details(ctx: ResolverContext, coordinate: string, err: unknown): Record<string, unknown> {
|
|
197
|
+
return { err, requestId: ctx.operation.requestId, coordinate, path: pathOf(ctx.info).join('.') }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function pathOf(info: GraphQLResolveInfo): (string | number)[] {
|
|
201
|
+
const path: (string | number)[] = []
|
|
202
|
+
for (let step: GraphQLResolveInfo['path'] | undefined = info.path; step !== undefined; step = step.prev) {
|
|
203
|
+
path.push(step.key)
|
|
204
|
+
}
|
|
205
|
+
return path.reverse()
|
|
206
|
+
}
|
package/src/scalars.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scalars of `@znight/graphql` (RFC 0005 §5.7): the built-in `DateTime`, `BigInt` and `JSON`, and
|
|
3
|
+
* the graphql-js 17 scalar built from a user `ScalarDefinition` (`scalar<T>(name, definition)`).
|
|
4
|
+
*
|
|
5
|
+
* Built-in scalars have no description and no `valueToLiteral`, with the `specifiedByURL` of
|
|
6
|
+
* `BUILTIN_SCALARS`, so that graphql-js `printSchema()` prints what `printSdl()` prints (§7.3).
|
|
7
|
+
*/
|
|
8
|
+
import type { ConstValueNode } from 'graphql'
|
|
9
|
+
import type { ScalarDefinition } from './types.ts'
|
|
10
|
+
|
|
11
|
+
import { GraphQLScalarType, Kind, valueFromASTUntyped } from 'graphql'
|
|
12
|
+
|
|
13
|
+
import { BUILTIN_SCALARS } from './names.ts'
|
|
14
|
+
|
|
15
|
+
/** RFC 3339 date-time with a mandatory offset (`2026-11-02T10:00:00Z`, `...+01:00`). */
|
|
16
|
+
const DATE_TIME =
|
|
17
|
+
/^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt]([01]\d|2[0-3]):([0-5]\d):([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)$/
|
|
18
|
+
|
|
19
|
+
/** An integer in decimal notation (`BigInt` on the wire). */
|
|
20
|
+
const DECIMAL_INTEGER = /^-?\d+$/
|
|
21
|
+
|
|
22
|
+
function describe(value: unknown): string {
|
|
23
|
+
if (typeof value === 'string') return JSON.stringify(value)
|
|
24
|
+
if (typeof value === 'bigint') return `${value}n`
|
|
25
|
+
if (value === null || typeof value !== 'object') return String(value)
|
|
26
|
+
return Array.isArray(value) ? 'a list' : 'an object'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseDateTime(value: unknown): Date {
|
|
30
|
+
if (typeof value === 'string' && DATE_TIME.test(value)) {
|
|
31
|
+
const date = new Date(value)
|
|
32
|
+
if (!Number.isNaN(date.getTime())) return date
|
|
33
|
+
}
|
|
34
|
+
throw new TypeError(`DateTime cannot represent ${describe(value)}: expected an RFC 3339 date-time with an offset`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function serializeDateTime(value: unknown): string {
|
|
38
|
+
if (value instanceof Date) {
|
|
39
|
+
if (Number.isNaN(value.getTime())) throw new TypeError('DateTime cannot represent an invalid Date')
|
|
40
|
+
return value.toISOString()
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === 'string' && DATE_TIME.test(value) && !Number.isNaN(Date.parse(value))) return value
|
|
43
|
+
throw new TypeError(`DateTime cannot represent ${describe(value)}: expected a Date`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseBigInt(value: unknown): bigint {
|
|
47
|
+
if (typeof value === 'string' && DECIMAL_INTEGER.test(value)) return BigInt(value)
|
|
48
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) return BigInt(value)
|
|
49
|
+
throw new TypeError(`BigInt cannot represent ${describe(value)}: expected an integer as a decimal string`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function serializeBigInt(value: unknown): string {
|
|
53
|
+
if (typeof value === 'bigint') return value.toString()
|
|
54
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) return String(value)
|
|
55
|
+
if (typeof value === 'string' && DECIMAL_INTEGER.test(value)) return value
|
|
56
|
+
throw new TypeError(`BigInt cannot represent ${describe(value)}: expected a bigint`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function identity(value: unknown): unknown {
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function specifiedBy(name: string): { specifiedByURL?: string } {
|
|
64
|
+
const url = BUILTIN_SCALARS.get(name)?.specifiedByURL
|
|
65
|
+
return url === undefined ? {} : { specifiedByURL: url }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createBuiltin(name: string): GraphQLScalarType | undefined {
|
|
69
|
+
switch (name) {
|
|
70
|
+
case 'DateTime':
|
|
71
|
+
return new GraphQLScalarType<Date, string>({
|
|
72
|
+
name,
|
|
73
|
+
...specifiedBy(name),
|
|
74
|
+
coerceOutputValue: serializeDateTime,
|
|
75
|
+
coerceInputValue: parseDateTime,
|
|
76
|
+
coerceInputLiteral: (node: ConstValueNode) => parseDateTime(node.kind === Kind.STRING ? node.value : node),
|
|
77
|
+
})
|
|
78
|
+
case 'BigInt':
|
|
79
|
+
return new GraphQLScalarType<bigint, string>({
|
|
80
|
+
name,
|
|
81
|
+
...specifiedBy(name),
|
|
82
|
+
coerceOutputValue: serializeBigInt,
|
|
83
|
+
coerceInputValue: parseBigInt,
|
|
84
|
+
coerceInputLiteral: (node: ConstValueNode) =>
|
|
85
|
+
parseBigInt(node.kind === Kind.STRING || node.kind === Kind.INT ? node.value : node),
|
|
86
|
+
})
|
|
87
|
+
case 'JSON':
|
|
88
|
+
return new GraphQLScalarType<unknown, unknown>({
|
|
89
|
+
name,
|
|
90
|
+
...specifiedBy(name),
|
|
91
|
+
coerceOutputValue: identity,
|
|
92
|
+
coerceInputValue: identity,
|
|
93
|
+
coerceInputLiteral: (node: ConstValueNode) => valueFromASTUntyped(node),
|
|
94
|
+
})
|
|
95
|
+
default:
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Built-in scalars, created on first use and shared by every schema (they hold no state). */
|
|
101
|
+
const builtins = new Map<string, GraphQLScalarType>()
|
|
102
|
+
|
|
103
|
+
/** The built-in scalar `name` (`DateTime`, `BigInt`, `JSON`), `undefined` for another name. */
|
|
104
|
+
export function builtinScalar(name: string): GraphQLScalarType | undefined {
|
|
105
|
+
let scalar = builtins.get(name)
|
|
106
|
+
if (scalar === undefined) {
|
|
107
|
+
scalar = createBuiltin(name)
|
|
108
|
+
if (scalar !== undefined) builtins.set(name, scalar)
|
|
109
|
+
}
|
|
110
|
+
return scalar
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The graphql-js scalar of a {@link ScalarDefinition}: `serialize` → `coerceOutputValue`,
|
|
115
|
+
* `parseValue` → `coerceInputValue`, `parseLiteral` → `coerceInputLiteral` (default: `parseValue`
|
|
116
|
+
* of the literal's JSON value). The definition's methods are called on the definition.
|
|
117
|
+
*/
|
|
118
|
+
export function definitionScalar(
|
|
119
|
+
name: string,
|
|
120
|
+
definition: ScalarDefinition<unknown>,
|
|
121
|
+
overrides: { readonly description?: string; readonly specifiedByURL?: string } = {},
|
|
122
|
+
): GraphQLScalarType {
|
|
123
|
+
const parseLiteral = definition.parseLiteral
|
|
124
|
+
// a key present in `overrides` wins, even undefined (schema-first: the SDL is the truth)
|
|
125
|
+
const description = 'description' in overrides ? overrides.description : definition.description
|
|
126
|
+
const specifiedByURL = 'specifiedByURL' in overrides ? overrides.specifiedByURL : definition.specifiedByURL
|
|
127
|
+
return new GraphQLScalarType({
|
|
128
|
+
name,
|
|
129
|
+
...(description === undefined ? {} : { description }),
|
|
130
|
+
...(specifiedByURL === undefined ? {} : { specifiedByURL }),
|
|
131
|
+
coerceOutputValue: (value: unknown) => definition.serialize(value),
|
|
132
|
+
coerceInputValue: (value: unknown) => definition.parseValue(value),
|
|
133
|
+
coerceInputLiteral:
|
|
134
|
+
parseLiteral === undefined
|
|
135
|
+
? (node: ConstValueNode) => definition.parseValue(valueFromASTUntyped(node))
|
|
136
|
+
: (node: ConstValueNode) => parseLiteral.call(definition, node),
|
|
137
|
+
})
|
|
138
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `schemaHash` of `metadata.extensions.graphql` (RFC 0005 §7.1): the first 16 hexadecimal
|
|
3
|
+
* characters of the SHA-256 of the SDL printed by `printSdl()`.
|
|
4
|
+
*
|
|
5
|
+
* Separate from the import-free `sdl.ts` because it needs `node:crypto`. That is fine on both sides:
|
|
6
|
+
* the compiler extension runs at build time on Node.js or Bun, and the runtime targets Node.js >= 24
|
|
7
|
+
* and Bun >= 1.3, which both provide `node:crypto`. The module reaches nothing else, so the
|
|
8
|
+
* `./compiler` boundaries hold (tests/boundaries.test.ts).
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto'
|
|
11
|
+
|
|
12
|
+
/** Length of a schema hash, in hexadecimal characters. */
|
|
13
|
+
export const SCHEMA_HASH_LENGTH = 16
|
|
14
|
+
|
|
15
|
+
/** First 16 hexadecimal characters of the SHA-256 of `sdl` (UTF-8). */
|
|
16
|
+
export function schemaHash(sdl: string): string {
|
|
17
|
+
return createHash('sha256').update(sdl, 'utf8').digest('hex').slice(0, SCHEMA_HASH_LENGTH)
|
|
18
|
+
}
|