@znight/graphql 0.2.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/README.md +288 -5
- package/dist/compiler.d.mts +24 -3
- package/dist/compiler.mjs +1876 -109
- package/dist/compiler.mjs.map +1 -1
- package/dist/index.d.mts +49 -8
- package/dist/index.mjs +10 -4
- package/dist/index.mjs.map +1 -1
- package/dist/names-BlzDM5cF.mjs.map +1 -1
- package/dist/{plugin-BdBxr5NQ.mjs → plugin-CR_wVfRU.mjs} +247 -24
- package/dist/plugin-CR_wVfRU.mjs.map +1 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/{types-CgbV9LjD.d.mts → types-DQ713UgI.d.mts} +34 -2
- package/package.json +8 -8
- package/src/compiler/analyze.ts +129 -10
- package/src/compiler/builder.ts +6 -85
- package/src/compiler/checks.ts +105 -0
- package/src/compiler/conformance.ts +1068 -0
- package/src/compiler/constants.ts +6 -0
- package/src/compiler/data.ts +10 -0
- package/src/compiler/generate.ts +41 -27
- package/src/compiler/graphql-js.ts +42 -0
- package/src/compiler/index.ts +8 -3
- package/src/compiler/locate.ts +39 -1
- package/src/compiler/model.ts +16 -1
- package/src/compiler/options.ts +39 -8
- package/src/compiler/schema-first.ts +595 -0
- package/src/compiler/sdl-source.ts +529 -0
- package/src/compiler/sdl-view.ts +3 -1
- package/src/compiler/suggest.ts +39 -0
- package/src/decorators.ts +9 -3
- package/src/index.ts +5 -0
- package/src/metadata.ts +28 -1
- package/src/options.ts +23 -1
- package/src/pipeline.ts +66 -5
- package/src/plugin.ts +27 -5
- package/src/results.ts +206 -0
- package/src/scalars.ts +10 -3
- package/src/schema.ts +90 -12
- package/src/sdl.ts +17 -2
- package/src/types.ts +35 -0
- package/dist/plugin-BdBxr5NQ.mjs.map +0 -1
package/dist/testing.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as LoaderValue, h as LoaderKey, r as BatchLoader, t as BatchLoad } from "./types-DQ713UgI.mjs";
|
|
2
2
|
import { GraphQLFormattedError } from "graphql";
|
|
3
3
|
import { MockFunction, TestApp } from "@znight/testing";
|
|
4
4
|
/** An operation for {@link executeOperation} and {@link subscribeOperation}. */
|
package/dist/testing.mjs
CHANGED
|
@@ -213,6 +213,38 @@ interface GraphQLErrorMapper {
|
|
|
213
213
|
}
|
|
214
214
|
/** A class producing {@link GraphQLErrorMapper}s. */
|
|
215
215
|
type GraphQLErrorMapperClass = abstract new (...args: never[]) => GraphQLErrorMapper;
|
|
216
|
+
/**
|
|
217
|
+
* Turns the errors of a field into values of the field (errors as data, RFC 0008). It applies to
|
|
218
|
+
* the resolver-backed fields whose type is a union holding one of the object types it declares
|
|
219
|
+
* (`static types`), decided at boot; every other field keeps its errors in `errors[]`.
|
|
220
|
+
*
|
|
221
|
+
* ```ts
|
|
222
|
+
* @Injectable()
|
|
223
|
+
* export class Rejections implements GraphQLResultMapper {
|
|
224
|
+
* static readonly types = ['NotFoundRejection']
|
|
225
|
+
*
|
|
226
|
+
* toResult(error: unknown): NotFoundRejection | undefined {
|
|
227
|
+
* if (error instanceof NotFoundException) return { __typename: 'NotFoundRejection', message: error.message }
|
|
228
|
+
* return undefined
|
|
229
|
+
* }
|
|
230
|
+
* }
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
interface GraphQLResultMapper {
|
|
234
|
+
/**
|
|
235
|
+
* The value of the field for `error`: an object of a member of the field's union, or `undefined`
|
|
236
|
+
* to leave the error to the next mapper, then to `errors[]`. Synchronous. `error` is the raw
|
|
237
|
+
* error, a `GraphQLException` included (a guard returning `false` is `FORBIDDEN`, a Standard
|
|
238
|
+
* Schema failure of the arguments `BAD_USER_INPUT` with `extensions.issues`).
|
|
239
|
+
*/
|
|
240
|
+
toResult(error: unknown, ctx: ResolverContext): unknown;
|
|
241
|
+
}
|
|
242
|
+
/** The object types a result mapper may produce: names of object types of the schema. */
|
|
243
|
+
interface GraphQLResultTypes {
|
|
244
|
+
readonly types: readonly string[];
|
|
245
|
+
}
|
|
246
|
+
/** A class producing {@link GraphQLResultMapper}s, with the object types it produces (`static types`). */
|
|
247
|
+
type GraphQLResultMapperClass = (abstract new (...args: never[]) => GraphQLResultMapper) & GraphQLResultTypes;
|
|
216
248
|
/**
|
|
217
249
|
* The implementation of a custom scalar (§5.7), given to `scalar<T>(name, definition)`. The
|
|
218
250
|
* runtime maps it to graphql-js 17 `GraphQLScalarType` coercers: `serialize` → `coerceOutputValue`,
|
|
@@ -230,5 +262,5 @@ interface ScalarDefinition<T> {
|
|
|
230
262
|
/** Literal of the document → internal value. Default: `parseValue` of the literal's JSON value. */
|
|
231
263
|
parseLiteral?(literal: ConstValueNode): T;
|
|
232
264
|
}
|
|
233
|
-
export {
|
|
234
|
-
//# sourceMappingURL=types-
|
|
265
|
+
export { GraphQLExceptionOptions as C, GraphQLException as S, LoaderValue as _, GraphQLContext as a, GraphQLErrorCode as b, GraphQLGuard as c, GraphQLResultMapper as d, GraphQLResultMapperClass as f, LoaderOptions as g, LoaderKey as h, GraphQLArgs as i, GraphQLGuardClass as l, GraphQLTransport as m, BatchLoadClass as n, GraphQLErrorMapper as o, GraphQLResultTypes as p, BatchLoader as r, GraphQLErrorMapperClass as s, BatchLoad as t, GraphQLOperationKind as u, ResolverContext as v, GraphQLErrorCodeValue as x, ScalarDefinition as y };
|
|
266
|
+
//# sourceMappingURL=types-DQ713UgI.d.mts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@znight/graphql",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Znight GraphQL: code-first GraphQL whose schema is derived from TypeScript types at build time, on graphql-js 17",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ugho STEPHAN",
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@znight/core": "0.2.
|
|
49
|
+
"@znight/core": "0.2.1",
|
|
50
50
|
"dataloader": "^2.2.3"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@znight/http": "^0.2.
|
|
54
|
-
"@znight/compiler": "^0.2.
|
|
55
|
-
"@znight/testing": "^0.2.
|
|
53
|
+
"@znight/http": "^0.2.1",
|
|
54
|
+
"@znight/compiler": "^0.2.1",
|
|
55
|
+
"@znight/testing": "^0.2.1",
|
|
56
56
|
"graphql": "^17.0.2"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@znight/http": "0.2.
|
|
68
|
-
"@znight/compiler": "0.2.
|
|
69
|
-
"@znight/testing": "0.2.
|
|
67
|
+
"@znight/http": "0.2.1",
|
|
68
|
+
"@znight/compiler": "0.2.1",
|
|
69
|
+
"@znight/testing": "0.2.1",
|
|
70
70
|
"graphql": "17.0.2",
|
|
71
71
|
"graphql-http": "1.23.1",
|
|
72
72
|
"graphql-sse": "2.6.3",
|
package/src/compiler/analyze.ts
CHANGED
|
@@ -18,7 +18,8 @@ import type {
|
|
|
18
18
|
} from '@znight/compiler'
|
|
19
19
|
import type { Scope } from '@znight/core'
|
|
20
20
|
import type { MetaEntry } from '@znight/http/compiler'
|
|
21
|
-
import type { FieldData, FieldOperation, GraphQLData, ParamData } from './data.ts'
|
|
21
|
+
import type { FieldData, FieldOperation, GraphQLData, ParamData, ResolverData } from './data.ts'
|
|
22
|
+
import type { ResolvedGraphQLCompilerOptions } from './options.ts'
|
|
22
23
|
|
|
23
24
|
import { effectiveRouteMeta, readRouteMeta } from '@znight/http/compiler'
|
|
24
25
|
|
|
@@ -41,12 +42,17 @@ const SCOPES: readonly Scope[] = ['singleton', 'request', 'transient']
|
|
|
41
42
|
/** Shape options reading `Loader<L>` parameters: the alias is kept, `L` stays a named shape. */
|
|
42
43
|
const LOADER_SHAPE = Object.freeze({ keep: Object.freeze([GraphQLIds.Loader]), jsdoc: false })
|
|
43
44
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Analyses a class carrying at least one graphql decorator (class, method or parameter). In
|
|
47
|
+
* schema-first mode (RFC 0007), `@Resolver('Type')` names the SDL type of the field resolvers and
|
|
48
|
+
* `@Parent()` is optional.
|
|
49
|
+
*/
|
|
50
|
+
export function analyzeGraphQLClass(cls: ClassInfo, ctx: Context, options: ResolvedGraphQLCompilerOptions): void {
|
|
46
51
|
const resolvers = cls.decorators.filter(decorator => decorator.identity === GraphQLIds.Resolver)
|
|
47
52
|
const resolver = resolvers[0]
|
|
48
53
|
if (resolver === undefined) {
|
|
49
54
|
reportOutsideResolver(cls, ctx)
|
|
55
|
+
storeClaims(ctx, cls, undefined, options)
|
|
50
56
|
return
|
|
51
57
|
}
|
|
52
58
|
for (const extra of resolvers.slice(1)) {
|
|
@@ -66,6 +72,7 @@ export function analyzeGraphQLClass(cls: ClassInfo, ctx: Context): void {
|
|
|
66
72
|
],
|
|
67
73
|
[{ message: '@Controller is applied here', location: controller.location }],
|
|
68
74
|
)
|
|
75
|
+
storeClaims(ctx, cls, resolver, options)
|
|
69
76
|
return
|
|
70
77
|
}
|
|
71
78
|
if (ctx.options.library) {
|
|
@@ -76,6 +83,7 @@ export function analyzeGraphQLClass(cls: ClassInfo, ctx: Context): void {
|
|
|
76
83
|
`${cls.displayName} is a @Resolver in a compiled provider library: the schema is derived from the sources`,
|
|
77
84
|
['Publish the package as a source library (without library: true), so that applications compile its resolvers'],
|
|
78
85
|
)
|
|
86
|
+
storeClaims(ctx, cls, resolver, options)
|
|
79
87
|
return
|
|
80
88
|
}
|
|
81
89
|
if (cls.abstract || cls.typeParameters.length > 0) {
|
|
@@ -88,10 +96,11 @@ export function analyzeGraphQLClass(cls: ClassInfo, ctx: Context): void {
|
|
|
88
96
|
: `@Resolver cannot be applied to the generic class ${cls.displayName}<${cls.typeParameters.join(', ')}>`,
|
|
89
97
|
[cls.abstract ? 'Resolvers are instantiated: remove `abstract`' : 'Remove the type parameters of the resolver'],
|
|
90
98
|
)
|
|
99
|
+
storeClaims(ctx, cls, resolver, options)
|
|
91
100
|
return
|
|
92
101
|
}
|
|
93
102
|
|
|
94
|
-
const scope =
|
|
103
|
+
const { scope, typeName } = resolverArguments(ctx, resolver, options.schema !== false)
|
|
95
104
|
ctx.registerProvider(cls, { scope })
|
|
96
105
|
// resolvers are entry points of the plugin: roots of ZNIGHT019 and prune
|
|
97
106
|
ctx.addRoot(cls, { reason: ROOT_REASON, location: resolver.location })
|
|
@@ -105,9 +114,79 @@ export function analyzeGraphQLClass(cls: ClassInfo, ctx: Context): void {
|
|
|
105
114
|
reportStrayMethodDecorators(cls, method, ctx)
|
|
106
115
|
continue
|
|
107
116
|
}
|
|
108
|
-
fields.push(...analyzeMethod(ctx, cls, method, decorators, classMeta))
|
|
117
|
+
fields.push(...analyzeMethod(ctx, cls, method, decorators, classMeta, options.schema !== false))
|
|
118
|
+
}
|
|
119
|
+
const data: ResolverData = {
|
|
120
|
+
kind: 'resolver',
|
|
121
|
+
host: cls,
|
|
122
|
+
scope,
|
|
123
|
+
location: resolver.location,
|
|
124
|
+
guards: classGuards,
|
|
125
|
+
fields,
|
|
126
|
+
...(typeName === undefined ? {} : { typeName }),
|
|
127
|
+
}
|
|
128
|
+
if (options.schema !== false && typeName === undefined) {
|
|
129
|
+
for (const field of fields) {
|
|
130
|
+
if (field.operation !== 'field') continue
|
|
131
|
+
error(
|
|
132
|
+
ctx,
|
|
133
|
+
Codes.InvalidResolver,
|
|
134
|
+
field.location,
|
|
135
|
+
`@ResolveField ${cls.displayName}.${field.method} needs the type it resolves a field of: name it in @Resolver (schema-first)`,
|
|
136
|
+
[
|
|
137
|
+
`@Resolver('Event') on ${cls.displayName}, where Event is the object type of the SDL declaring '${field.name}'`,
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
}
|
|
109
141
|
}
|
|
110
|
-
ctx.store(
|
|
142
|
+
ctx.store(data)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Schema-first: a class rejected above (no `@Resolver`, abstract, a controller...) still names the
|
|
147
|
+
* fields its methods resolve, stored as an `invalid` resolver, so that these fields are not also
|
|
148
|
+
* reported as missing a resolver (one diagnostic per mistake).
|
|
149
|
+
*/
|
|
150
|
+
function storeClaims(
|
|
151
|
+
ctx: Context,
|
|
152
|
+
cls: ClassInfo,
|
|
153
|
+
resolver: DecoratorInfo | undefined,
|
|
154
|
+
options: ResolvedGraphQLCompilerOptions,
|
|
155
|
+
): void {
|
|
156
|
+
if (options.schema === false) return
|
|
157
|
+
const typeArg = resolver?.args[0]
|
|
158
|
+
const typeName =
|
|
159
|
+
typeArg?.evaluated === true && typeof typeArg.value === 'string'
|
|
160
|
+
? { name: typeArg.value, location: typeArg.location }
|
|
161
|
+
: undefined
|
|
162
|
+
const fields: FieldData[] = []
|
|
163
|
+
for (const method of cls.methods) {
|
|
164
|
+
for (const decorator of method.decorators.filter(isFieldDecorator)) {
|
|
165
|
+
const arg = decorator.args[0]
|
|
166
|
+
fields.push({
|
|
167
|
+
method: method.name,
|
|
168
|
+
operation: FIELD_DECORATORS.get(decorator.identity as string) as FieldOperation,
|
|
169
|
+
name: arg?.evaluated === true && typeof arg.value === 'string' ? arg.value : method.name,
|
|
170
|
+
location: decorator.location,
|
|
171
|
+
methodLocation: method.location,
|
|
172
|
+
guards: [],
|
|
173
|
+
meta: [],
|
|
174
|
+
params: [],
|
|
175
|
+
complete: false,
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (fields.length === 0) return
|
|
180
|
+
ctx.store({
|
|
181
|
+
kind: 'resolver',
|
|
182
|
+
host: cls,
|
|
183
|
+
scope: 'singleton',
|
|
184
|
+
location: resolver?.location ?? cls.location,
|
|
185
|
+
guards: [],
|
|
186
|
+
fields,
|
|
187
|
+
invalid: true,
|
|
188
|
+
...(typeName === undefined ? {} : { typeName }),
|
|
189
|
+
})
|
|
111
190
|
}
|
|
112
191
|
|
|
113
192
|
function isFieldDecorator(decorator: DecoratorInfo): boolean {
|
|
@@ -118,9 +197,47 @@ function isParameterDecorator(decorator: DecoratorInfo): boolean {
|
|
|
118
197
|
return decorator.identity !== undefined && PARAMETER_DECORATORS.has(decorator.identity)
|
|
119
198
|
}
|
|
120
199
|
|
|
121
|
-
/** `@Resolver({ scope })
|
|
122
|
-
function
|
|
123
|
-
|
|
200
|
+
/** `@Resolver()`, `@Resolver({ scope })`, `@Resolver('Event')`, `@Resolver('Event', { scope })` (schema-first). */
|
|
201
|
+
function resolverArguments(
|
|
202
|
+
ctx: Context,
|
|
203
|
+
decorator: DecoratorInfo,
|
|
204
|
+
schemaFirst: boolean,
|
|
205
|
+
): { scope: Scope; typeName?: { readonly name: string; readonly location: Location } } {
|
|
206
|
+
const [first, second] = decorator.args
|
|
207
|
+
if (first?.evaluated !== true || typeof first.value !== 'string') {
|
|
208
|
+
if (second !== undefined) {
|
|
209
|
+
error(ctx, Codes.InvalidDecorator, second.location, '@Resolver takes the options as its only argument here', [
|
|
210
|
+
"@Resolver({ scope: 'request' }), or @Resolver('Event', { scope: 'request' }) in schema-first mode",
|
|
211
|
+
])
|
|
212
|
+
}
|
|
213
|
+
return { scope: resolverScope(ctx, first) }
|
|
214
|
+
}
|
|
215
|
+
const scope = resolverScope(ctx, second)
|
|
216
|
+
const name = first.value
|
|
217
|
+
if (!schemaFirst) {
|
|
218
|
+
error(
|
|
219
|
+
ctx,
|
|
220
|
+
Codes.InvalidDecorator,
|
|
221
|
+
first.location,
|
|
222
|
+
`@Resolver('${name}'): naming the type is schema-first only, the code-first schema is derived from the types`,
|
|
223
|
+
[
|
|
224
|
+
'Remove the type name: @ResolveField methods add fields to the type of their @Parent() parameter',
|
|
225
|
+
"Or switch to schema-first: graphql.configure({ schema: 'src/**/*.graphql' }) in znight.config.ts",
|
|
226
|
+
],
|
|
227
|
+
)
|
|
228
|
+
return { scope }
|
|
229
|
+
}
|
|
230
|
+
if (!isGraphQLName(name)) {
|
|
231
|
+
error(ctx, Codes.NameConflict, first.location, `'${name}' is not a valid GraphQL type name (@Resolver)`, [
|
|
232
|
+
'GraphQL names match /^[_A-Za-z][_0-9A-Za-z]*$/ and do not start with __',
|
|
233
|
+
])
|
|
234
|
+
return { scope }
|
|
235
|
+
}
|
|
236
|
+
return { scope, typeName: { name, location: first.location } }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The `{ scope }` options of `@Resolver`. */
|
|
240
|
+
function resolverScope(ctx: Context, arg: DecoratorInfo['args'][number] | undefined): Scope {
|
|
124
241
|
if (arg === undefined || !arg.evaluated || arg.value === undefined) return 'singleton'
|
|
125
242
|
const value = arg.value
|
|
126
243
|
if (!isRecord(value)) {
|
|
@@ -171,6 +288,7 @@ function analyzeMethod(
|
|
|
171
288
|
method: MethodInfo,
|
|
172
289
|
decorators: readonly DecoratorInfo[],
|
|
173
290
|
classMeta: readonly MetaEntry[],
|
|
291
|
+
schemaFirst: boolean,
|
|
174
292
|
): FieldData[] {
|
|
175
293
|
const where = `${cls.displayName}.${method.name}`
|
|
176
294
|
const primary = decorators[0] as DecoratorInfo
|
|
@@ -216,7 +334,8 @@ function analyzeMethod(
|
|
|
216
334
|
}
|
|
217
335
|
seen.add(key)
|
|
218
336
|
let fieldComplete = complete && explicit !== undefined
|
|
219
|
-
|
|
337
|
+
// schema-first: the type is named by @Resolver('Type'), the parent is optional (NestJS)
|
|
338
|
+
if (operation === 'field' && parents.length === 0 && !schemaFirst) {
|
|
220
339
|
error(ctx, Codes.InvalidResolver, decorator.location, `@ResolveField ${where} has no @Parent() parameter`, [
|
|
221
340
|
`Declare the parent object first: ${method.name}(@Parent() parent: ParentType, ...)`,
|
|
222
341
|
])
|
package/src/compiler/builder.ts
CHANGED
|
@@ -39,17 +39,16 @@ import type {
|
|
|
39
39
|
} from './model.ts'
|
|
40
40
|
import type { ResolvedGraphQLCompilerOptions } from './options.ts'
|
|
41
41
|
|
|
42
|
-
import { requiredRouteGroups } from '@znight/http/compiler'
|
|
43
|
-
|
|
44
42
|
import { isGraphQLName, RESERVED_ENUM_VALUES, ROOT_TYPE_NAMES, SPECIFIED_SCALARS } from '../names.ts'
|
|
45
43
|
import { schemaHash } from '../schema-hash.ts'
|
|
46
44
|
import { printSdl } from '../sdl.ts'
|
|
47
45
|
import { printTypeRef } from '../type-ref.ts'
|
|
46
|
+
import { checkDecisions, checkLoaders } from './checks.ts'
|
|
48
47
|
import { ASYNC_ITERABLES, BASE_KEEP, Codes, compareStrings, OUTPUT_ITERABLES } from './constants.ts'
|
|
49
48
|
import { externalValue } from './defaults.ts'
|
|
50
49
|
import { discriminatorOf } from './discriminators.ts'
|
|
51
50
|
import { defaultTagValue, deprecationOf, descriptionOf, isHidden, paramDescription, tagOf } from './docs.ts'
|
|
52
|
-
import {
|
|
51
|
+
import { SCALAR_ALIASES } from './ids.ts'
|
|
53
52
|
import { annotationLocation } from './locate.ts'
|
|
54
53
|
import { readScalars, type ScalarDeclaration } from './scalars.ts'
|
|
55
54
|
import { sdlType } from './sdl-view.ts'
|
|
@@ -151,7 +150,7 @@ class SchemaBuilder {
|
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
build(resolvers: readonly ResolverData[]): GraphQLModel | undefined {
|
|
154
|
-
this.#
|
|
153
|
+
checkLoaders(this.#ctx, resolvers, diagnostic => this.#report(diagnostic))
|
|
155
154
|
this.#indexFieldResolvers(resolvers)
|
|
156
155
|
const roots = new Map<Operation, ObjectModel>()
|
|
157
156
|
const rootFields = new Map<string, { readonly field: FieldData; readonly resolver: ResolverData }>()
|
|
@@ -194,7 +193,7 @@ class SchemaBuilder {
|
|
|
194
193
|
this.#reportUnreachableFieldResolvers()
|
|
195
194
|
this.#checkDefaults()
|
|
196
195
|
this.#checkInputCycles()
|
|
197
|
-
this.#
|
|
196
|
+
checkDecisions(this.#ctx, resolvers, diagnostic => this.#report(diagnostic))
|
|
198
197
|
if (this.#errors > 0 || !hasQuery) return undefined
|
|
199
198
|
return this.#assemble(roots, resolvers)
|
|
200
199
|
}
|
|
@@ -261,6 +260,7 @@ class SchemaBuilder {
|
|
|
261
260
|
root: undefined,
|
|
262
261
|
classIdentity: undefined,
|
|
263
262
|
properties: [],
|
|
263
|
+
interfaces: [],
|
|
264
264
|
}
|
|
265
265
|
roots.set(operation, root)
|
|
266
266
|
this.#claim(root, undefined)
|
|
@@ -511,34 +511,6 @@ class SchemaBuilder {
|
|
|
511
511
|
])
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
-
/** `Loader<L>`: `L` must be a loader provider, implementing `BatchLoad<K, V>` (ZNIGHT065). */
|
|
515
|
-
#checkLoaders(resolvers: readonly ResolverData[]): void {
|
|
516
|
-
const seen = new Set<number>()
|
|
517
|
-
for (const resolver of resolvers) {
|
|
518
|
-
for (const field of resolver.fields) {
|
|
519
|
-
for (const param of field.params) {
|
|
520
|
-
if (param?.kind !== 'loader' || seen.has(param.binding.id)) continue
|
|
521
|
-
seen.add(param.binding.id)
|
|
522
|
-
const ref = this.#ctx.binding(param.binding)
|
|
523
|
-
const token = tokenOfRef(ref)
|
|
524
|
-
const provider = token === undefined ? undefined : this.#ctx.provider(token)
|
|
525
|
-
// an unbound loader is ZNIGHT001 (reported by the binding)
|
|
526
|
-
if (provider === undefined || provider.heritage.some(type => type.identity === GraphQLIds.BatchLoad)) continue
|
|
527
|
-
if (provider.kind !== 'class' || hasLoadMethod(this.#ctx, provider.classIdentity)) continue
|
|
528
|
-
this.#report({
|
|
529
|
-
code: Codes.InvalidLoader,
|
|
530
|
-
severity: 'error',
|
|
531
|
-
message: `${provider.displayName} is not a loader: Loader<${provider.displayName}> needs a provider implementing BatchLoad<K, V> (${resolver.host.displayName}.${field.method})`,
|
|
532
|
-
location: param.location,
|
|
533
|
-
hints: [
|
|
534
|
-
`export class ${provider.displayName} implements BatchLoad<ID, Value> { async load(keys: readonly ID[]): Promise<readonly (Value | Error)[]> { ... } }`,
|
|
535
|
-
],
|
|
536
|
-
})
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
514
|
// ------------------------------------------------------------------------------ field resolvers
|
|
543
515
|
|
|
544
516
|
#indexFieldResolvers(resolvers: readonly ResolverData[]): void {
|
|
@@ -1029,6 +1001,7 @@ class SchemaBuilder {
|
|
|
1029
1001
|
? structure.identity
|
|
1030
1002
|
: undefined,
|
|
1031
1003
|
properties: properties.map(property => this.#discriminant(property)),
|
|
1004
|
+
interfaces: [],
|
|
1032
1005
|
}
|
|
1033
1006
|
this.#models.set(key, model)
|
|
1034
1007
|
if (!this.#claim(model, use)) return undefined
|
|
@@ -1508,37 +1481,6 @@ class SchemaBuilder {
|
|
|
1508
1481
|
for (const model of inputs) if (!state.has(model.name)) visit(model)
|
|
1509
1482
|
}
|
|
1510
1483
|
|
|
1511
|
-
/** RFC 0001: every root field decides every required route decorator group (ZNIGHT023). */
|
|
1512
|
-
#checkDecisions(resolvers: readonly ResolverData[]): void {
|
|
1513
|
-
const applied: MetaEntry[] = []
|
|
1514
|
-
for (const resolver of resolvers) for (const field of resolver.fields) applied.push(...field.meta)
|
|
1515
|
-
const required = requiredRouteGroups(this.#ctx, applied)
|
|
1516
|
-
if (required.length === 0) return
|
|
1517
|
-
for (const resolver of resolvers) {
|
|
1518
|
-
for (const field of resolver.fields) {
|
|
1519
|
-
if (field.operation === 'field') continue
|
|
1520
|
-
const decided = new Set(field.meta.map(entry => entry.group))
|
|
1521
|
-
for (const group of required) {
|
|
1522
|
-
if (decided.has(group.group)) continue
|
|
1523
|
-
const root = ROOT_TYPE_NAMES[field.operation]
|
|
1524
|
-
this.#report({
|
|
1525
|
-
code: Codes.MissingRouteDecision,
|
|
1526
|
-
severity: 'error',
|
|
1527
|
-
message: `Root field ${root}.${field.name} (${resolver.host.displayName}.${field.method}) has no "${group.group}" decision`,
|
|
1528
|
-
location: field.methodLocation,
|
|
1529
|
-
hints: [`Add ${group.suggestion} to the method or to ${resolver.host.displayName}`],
|
|
1530
|
-
related: [
|
|
1531
|
-
{
|
|
1532
|
-
message: `The "${group.group}" group is required`,
|
|
1533
|
-
location: (group.decorators[0] as { definition: Location }).definition,
|
|
1534
|
-
},
|
|
1535
|
-
],
|
|
1536
|
-
})
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
1484
|
// ------------------------------------------------------------------------------------ assembly
|
|
1543
1485
|
|
|
1544
1486
|
#assemble(roots: ReadonlyMap<Operation, ObjectModel>, resolvers: readonly ResolverData[]): GraphQLModel {
|
|
@@ -1557,21 +1499,6 @@ class SchemaBuilder {
|
|
|
1557
1499
|
|
|
1558
1500
|
// ------------------------------------------------------------------------------------------ helpers
|
|
1559
1501
|
|
|
1560
|
-
/** The class (or one of its base classes) declares an instance `load` method. */
|
|
1561
|
-
function hasLoadMethod(ctx: Context, identity: string): boolean {
|
|
1562
|
-
const seen = new Set<string>()
|
|
1563
|
-
let current: string | undefined = identity
|
|
1564
|
-
while (current !== undefined && !seen.has(current)) {
|
|
1565
|
-
seen.add(current)
|
|
1566
|
-
const cls: ClassInfo | undefined = ctx.classInfo(current)
|
|
1567
|
-
if (cls === undefined) return true // not analysable (a compiled class): trust the type checker
|
|
1568
|
-
if (cls.methods.some(method => method.name === 'load' && method.kind === 'method' && !method.static)) return true
|
|
1569
|
-
if (cls.properties.some(property => property.name === 'load' && !property.static)) return true
|
|
1570
|
-
current = cls.superClass?.identity
|
|
1571
|
-
}
|
|
1572
|
-
return false
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
1502
|
/** A (nullable, until `#convert` decides) reference to a named type. */
|
|
1576
1503
|
function refTo(name: string): Inner {
|
|
1577
1504
|
return { ref: { kind: 'named', name, nonNull: false }, nullable: false }
|
|
@@ -1587,12 +1514,6 @@ function decoratorName(operation: Operation): string {
|
|
|
1587
1514
|
return ROOT_TYPE_NAMES[operation]
|
|
1588
1515
|
}
|
|
1589
1516
|
|
|
1590
|
-
/** The token of a binding result (`undefined` when binding failed). */
|
|
1591
|
-
function tokenOfRef(ref: ReturnType<Context['binding']>): string | undefined {
|
|
1592
|
-
if (typeof ref === 'string') return ref
|
|
1593
|
-
return ref !== undefined && 'token' in ref ? ref.token : undefined
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
1517
|
function describeKey(model: TypeModel): string {
|
|
1597
1518
|
return model.key.startsWith('#') ? `the ${model.kind} ${model.name}` : model.key
|
|
1598
1519
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks shared by the code-first schema builder (`builder.ts`) and the schema-first one
|
|
3
|
+
* (`schema-first.ts`): loader providers (ZNIGHT065) and the route decisions of root fields
|
|
4
|
+
* (ZNIGHT023, RFC 0001).
|
|
5
|
+
*/
|
|
6
|
+
import type { ClassInfo, Diagnostic, Location, ValidateContext } from '@znight/compiler'
|
|
7
|
+
import type { MetaEntry } from '@znight/http/compiler'
|
|
8
|
+
import type { GraphQLData, ResolverData } from './data.ts'
|
|
9
|
+
|
|
10
|
+
import { requiredRouteGroups } from '@znight/http/compiler'
|
|
11
|
+
|
|
12
|
+
import { ROOT_TYPE_NAMES } from '../names.ts'
|
|
13
|
+
import { Codes } from './constants.ts'
|
|
14
|
+
import { GraphQLIds } from './ids.ts'
|
|
15
|
+
|
|
16
|
+
type Context = ValidateContext<GraphQLData>
|
|
17
|
+
type Report = (diagnostic: Diagnostic) => void
|
|
18
|
+
|
|
19
|
+
/** `Loader<L>`: `L` must be a loader provider, implementing `BatchLoad<K, V>` (ZNIGHT065). */
|
|
20
|
+
export function checkLoaders(ctx: Context, resolvers: readonly ResolverData[], report: Report): void {
|
|
21
|
+
const seen = new Set<number>()
|
|
22
|
+
for (const resolver of resolvers) {
|
|
23
|
+
for (const field of resolver.fields) {
|
|
24
|
+
for (const param of field.params) {
|
|
25
|
+
if (param?.kind !== 'loader' || seen.has(param.binding.id)) continue
|
|
26
|
+
seen.add(param.binding.id)
|
|
27
|
+
const ref = ctx.binding(param.binding)
|
|
28
|
+
const token = tokenOfRef(ref)
|
|
29
|
+
const provider = token === undefined ? undefined : ctx.provider(token)
|
|
30
|
+
// an unbound loader is ZNIGHT001 (reported by the binding)
|
|
31
|
+
if (provider === undefined || provider.heritage.some(type => type.identity === GraphQLIds.BatchLoad)) continue
|
|
32
|
+
if (provider.kind !== 'class' || hasLoadMethod(ctx, provider.classIdentity)) continue
|
|
33
|
+
report({
|
|
34
|
+
code: Codes.InvalidLoader,
|
|
35
|
+
severity: 'error',
|
|
36
|
+
message: `${provider.displayName} is not a loader: Loader<${provider.displayName}> needs a provider implementing BatchLoad<K, V> (${resolver.host.displayName}.${field.method})`,
|
|
37
|
+
location: param.location,
|
|
38
|
+
hints: [
|
|
39
|
+
`export class ${provider.displayName} implements BatchLoad<ID, Value> { async load(keys: readonly ID[]): Promise<readonly (Value | Error)[]> { ... } }`,
|
|
40
|
+
],
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* RFC 0001: every root field decides every required route decorator group (ZNIGHT023). `roots`
|
|
49
|
+
* names the root types (`schema { query: RootQuery }` in schema-first).
|
|
50
|
+
*/
|
|
51
|
+
export function checkDecisions(
|
|
52
|
+
ctx: Context,
|
|
53
|
+
resolvers: readonly ResolverData[],
|
|
54
|
+
report: Report,
|
|
55
|
+
roots: { readonly query: string; readonly mutation?: string; readonly subscription?: string } = ROOT_TYPE_NAMES,
|
|
56
|
+
): void {
|
|
57
|
+
const applied: MetaEntry[] = []
|
|
58
|
+
for (const resolver of resolvers) for (const field of resolver.fields) applied.push(...field.meta)
|
|
59
|
+
const required = requiredRouteGroups(ctx, applied)
|
|
60
|
+
if (required.length === 0) return
|
|
61
|
+
for (const resolver of resolvers) {
|
|
62
|
+
for (const field of resolver.fields) {
|
|
63
|
+
if (field.operation === 'field') continue
|
|
64
|
+
const decided = new Set(field.meta.map(entry => entry.group))
|
|
65
|
+
for (const group of required) {
|
|
66
|
+
if (decided.has(group.group)) continue
|
|
67
|
+
const root = roots[field.operation] ?? ROOT_TYPE_NAMES[field.operation]
|
|
68
|
+
report({
|
|
69
|
+
code: Codes.MissingRouteDecision,
|
|
70
|
+
severity: 'error',
|
|
71
|
+
message: `Root field ${root}.${field.name} (${resolver.host.displayName}.${field.method}) has no "${group.group}" decision`,
|
|
72
|
+
location: field.methodLocation,
|
|
73
|
+
hints: [`Add ${group.suggestion} to the method or to ${resolver.host.displayName}`],
|
|
74
|
+
related: [
|
|
75
|
+
{
|
|
76
|
+
message: `The "${group.group}" group is required`,
|
|
77
|
+
location: (group.decorators[0] as { definition: Location }).definition,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The class (or one of its base classes) declares an instance `load` method. */
|
|
87
|
+
function hasLoadMethod(ctx: Context, identity: string): boolean {
|
|
88
|
+
const seen = new Set<string>()
|
|
89
|
+
let current: string | undefined = identity
|
|
90
|
+
while (current !== undefined && !seen.has(current)) {
|
|
91
|
+
seen.add(current)
|
|
92
|
+
const cls: ClassInfo | undefined = ctx.classInfo(current)
|
|
93
|
+
if (cls === undefined) return true // not analysable (a compiled class): trust the type checker
|
|
94
|
+
if (cls.methods.some(method => method.name === 'load' && method.kind === 'method' && !method.static)) return true
|
|
95
|
+
if (cls.properties.some(property => property.name === 'load' && !property.static)) return true
|
|
96
|
+
current = cls.superClass?.identity
|
|
97
|
+
}
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The token of a binding result (`undefined` when binding failed). */
|
|
102
|
+
function tokenOfRef(ref: ReturnType<Context['binding']>): string | undefined {
|
|
103
|
+
if (typeof ref === 'string') return ref
|
|
104
|
+
return ref !== undefined && 'token' in ref ? ref.token : undefined
|
|
105
|
+
}
|