@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.
Files changed (42) hide show
  1. package/README.md +288 -5
  2. package/dist/compiler.d.mts +24 -3
  3. package/dist/compiler.mjs +1876 -109
  4. package/dist/compiler.mjs.map +1 -1
  5. package/dist/index.d.mts +49 -8
  6. package/dist/index.mjs +10 -4
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/names-BlzDM5cF.mjs.map +1 -1
  9. package/dist/{plugin-BdBxr5NQ.mjs → plugin-CR_wVfRU.mjs} +247 -24
  10. package/dist/plugin-CR_wVfRU.mjs.map +1 -0
  11. package/dist/testing.d.mts +1 -1
  12. package/dist/testing.mjs +1 -1
  13. package/dist/{types-CgbV9LjD.d.mts → types-DQ713UgI.d.mts} +34 -2
  14. package/package.json +8 -8
  15. package/src/compiler/analyze.ts +129 -10
  16. package/src/compiler/builder.ts +6 -85
  17. package/src/compiler/checks.ts +105 -0
  18. package/src/compiler/conformance.ts +1068 -0
  19. package/src/compiler/constants.ts +6 -0
  20. package/src/compiler/data.ts +10 -0
  21. package/src/compiler/generate.ts +41 -27
  22. package/src/compiler/graphql-js.ts +42 -0
  23. package/src/compiler/index.ts +8 -3
  24. package/src/compiler/locate.ts +39 -1
  25. package/src/compiler/model.ts +16 -1
  26. package/src/compiler/options.ts +39 -8
  27. package/src/compiler/schema-first.ts +595 -0
  28. package/src/compiler/sdl-source.ts +529 -0
  29. package/src/compiler/sdl-view.ts +3 -1
  30. package/src/compiler/suggest.ts +39 -0
  31. package/src/decorators.ts +9 -3
  32. package/src/index.ts +5 -0
  33. package/src/metadata.ts +28 -1
  34. package/src/options.ts +23 -1
  35. package/src/pipeline.ts +66 -5
  36. package/src/plugin.ts +27 -5
  37. package/src/results.ts +206 -0
  38. package/src/scalars.ts +10 -3
  39. package/src/schema.ts +90 -12
  40. package/src/sdl.ts +17 -2
  41. package/src/types.ts +35 -0
  42. package/dist/plugin-BdBxr5NQ.mjs.map +0 -1
package/src/schema.ts CHANGED
@@ -17,13 +17,13 @@ import type {
17
17
  GraphQLInputType,
18
18
  GraphQLNamedType,
19
19
  GraphQLOutputType,
20
- GraphQLTypeResolver,
21
20
  } from 'graphql'
22
21
  import type {
23
22
  GraphQLArgumentDefinition,
24
23
  GraphQLDiscriminator,
25
24
  GraphQLFieldDefinition,
26
25
  GraphQLInputTypeDefinition,
26
+ GraphQLInterfaceTypeDefinition,
27
27
  GraphQLMetadata,
28
28
  GraphQLObjectTypeDefinition,
29
29
  GraphQLResolverBinding,
@@ -40,6 +40,7 @@ import {
40
40
  GraphQLID,
41
41
  GraphQLInputObjectType,
42
42
  GraphQLInt,
43
+ GraphQLInterfaceType,
43
44
  GraphQLList,
44
45
  GraphQLNonNull,
45
46
  GraphQLObjectType,
@@ -90,7 +91,7 @@ export function graphqlSection(metadata: ZnightMetadata): GraphQLMetadata {
90
91
 
91
92
  const OPERATIONS: ReadonlySet<string> = new Set(['query', 'mutation', 'subscription', 'field'])
92
93
  const INPUT_KINDS: ReadonlySet<string> = new Set(['input', 'enum', 'scalar'])
93
- const OUTPUT_KINDS: ReadonlySet<string> = new Set(['object', 'enum', 'scalar', 'union'])
94
+ const OUTPUT_KINDS: ReadonlySet<string> = new Set(['object', 'interface', 'enum', 'scalar', 'union'])
94
95
 
95
96
  function isRecord(value: unknown): value is Record<string, unknown> {
96
97
  return value !== null && typeof value === 'object' && !Array.isArray(value)
@@ -172,7 +173,10 @@ export function checkGraphQLSection(section: unknown): string[] {
172
173
  if (!isRecord(type) || typeof type['name'] !== 'string') continue
173
174
  const name = type['name']
174
175
  switch (type['kind']) {
175
- case 'object': {
176
+ case 'object':
177
+ case 'interface': {
178
+ const isInterface = type['kind'] === 'interface'
179
+ checkInterfaces(name, type['interfaces'], kinds, problems)
176
180
  const fields = type['fields']
177
181
  if (!Array.isArray(fields) || fields.length === 0) {
178
182
  problems.push(`${name}: fields must be a non-empty array`)
@@ -190,7 +194,25 @@ export function checkGraphQLSection(section: unknown): string[] {
190
194
  seen.add(field['name'])
191
195
  checkRef(coordinate, field['type'], OUTPUT_KINDS, 'output')
192
196
  if (field['args'] !== undefined) checkInputValues(`${coordinate} arguments`, field['args'], false)
193
- if (field['resolver'] !== undefined) checkBinding(coordinate, field['resolver'], problems)
197
+ if (field['resolver'] === undefined) continue
198
+ if (isInterface)
199
+ problems.push(`${coordinate}: interface fields have no resolver (bind it on the object types)`)
200
+ else checkBinding(coordinate, field['resolver'], problems)
201
+ }
202
+ if (isInterface) {
203
+ const implementations = types.filter(
204
+ other =>
205
+ isRecord(other) &&
206
+ other['kind'] === 'object' &&
207
+ Array.isArray(other['interfaces']) &&
208
+ other['interfaces'].includes(name),
209
+ )
210
+ checkDiscriminator(
211
+ name,
212
+ type['discriminator'],
213
+ implementations.map(other => (other as Record<string, unknown>)['name']),
214
+ problems,
215
+ )
194
216
  }
195
217
  break
196
218
  }
@@ -258,6 +280,11 @@ export function checkGraphQLSection(section: unknown): string[] {
258
280
  )
259
281
  }
260
282
  }
283
+ for (const key of ['description', 'specifiedByURL'] as const) {
284
+ if (type[key] !== undefined && typeof type[key] !== 'string')
285
+ problems.push(`${name}: ${key} must be a string`)
286
+ }
287
+ if (type['sdl'] !== undefined && type['sdl'] !== true) problems.push(`${name}: sdl must be true when present`)
261
288
  break
262
289
  default:
263
290
  problems.push(`${name}: unknown kind ${show(type['kind'])}`)
@@ -268,6 +295,25 @@ export function checkGraphQLSection(section: unknown): string[] {
268
295
  return problems
269
296
  }
270
297
 
298
+ /** `interfaces` of an object or interface type: absent, or interface type names. */
299
+ function checkInterfaces(
300
+ name: string,
301
+ interfaces: unknown,
302
+ kinds: ReadonlyMap<string, string>,
303
+ problems: string[],
304
+ ): void {
305
+ if (interfaces === undefined) return
306
+ if (!Array.isArray(interfaces)) {
307
+ problems.push(`${name}: interfaces must be an array of interface type names (received ${show(interfaces)})`)
308
+ return
309
+ }
310
+ for (const entry of interfaces) {
311
+ if (typeof entry !== 'string' || kinds.get(entry) !== 'interface') {
312
+ problems.push(`${name}: ${show(entry)} is not an interface type`)
313
+ }
314
+ }
315
+ }
316
+
271
317
  function checkBinding(coordinate: string, binding: unknown, problems: string[]): void {
272
318
  if (!isRecord(binding)) {
273
319
  problems.push(`${coordinate}: resolver must be an object (received ${show(binding)})`)
@@ -393,8 +439,8 @@ export function invalidSchema(problems: readonly string[]): never {
393
439
  )
394
440
  }
395
441
 
396
- /** A union's `resolveType`, from its discriminator: data read once, at boot. */
397
- function typeResolver(discriminator: GraphQLDiscriminator): GraphQLTypeResolver<unknown, unknown> {
442
+ /** The `resolveType` of a union or an interface, from its discriminator: data read once, at boot. */
443
+ export function typeResolver(discriminator: GraphQLDiscriminator): (value: unknown) => string | undefined {
398
444
  switch (discriminator.by) {
399
445
  case 'property': {
400
446
  const property = discriminator.property
@@ -454,7 +500,7 @@ export function buildGraphQLSchema(section: GraphQLMetadata, functions: FieldFun
454
500
  ...(value.defaultValue === undefined ? {} : { default: { value: value.defaultValue } }),
455
501
  })
456
502
  const fieldConfig = (
457
- type: GraphQLObjectTypeDefinition,
503
+ type: GraphQLObjectTypeDefinition | GraphQLInterfaceTypeDefinition,
458
504
  field: GraphQLFieldDefinition,
459
505
  ): GraphQLFieldConfig<unknown, unknown> => {
460
506
  const config: GraphQLFieldConfig<unknown, unknown> = {
@@ -465,7 +511,7 @@ export function buildGraphQLSchema(section: GraphQLMetadata, functions: FieldFun
465
511
  ? {}
466
512
  : { args: Object.fromEntries(field.args.map(arg => [arg.name, inputValue(arg)])) }),
467
513
  }
468
- if (field.resolver === undefined) return config
514
+ if (field.resolver === undefined || type.kind === 'interface') return config
469
515
  const resolved = functions(field.resolver, `${type.name}.${field.name}`, field, type)
470
516
  return {
471
517
  ...config,
@@ -502,7 +548,7 @@ export function buildGraphQLSchema(section: GraphQLMetadata, functions: FieldFun
502
548
  function createType(
503
549
  type: GraphQLTypeDefinition,
504
550
  fieldConfig: (
505
- type: GraphQLObjectTypeDefinition,
551
+ type: GraphQLObjectTypeDefinition | GraphQLInterfaceTypeDefinition,
506
552
  field: GraphQLFieldDefinition,
507
553
  ) => GraphQLFieldConfig<unknown, unknown>,
508
554
  inputValue: (value: GraphQLArgumentDefinition) => GraphQLInputFieldConfig,
@@ -514,9 +560,18 @@ function createType(
514
560
  return new GraphQLObjectType({
515
561
  name: type.name,
516
562
  ...description,
563
+ ...interfacesOf(type, lookup),
517
564
  // thunk: object types reference each other (cycles)
518
565
  fields: () => Object.fromEntries(type.fields.map(field => [field.name, fieldConfig(type, field)])),
519
566
  })
567
+ case 'interface':
568
+ return new GraphQLInterfaceType({
569
+ name: type.name,
570
+ ...description,
571
+ ...interfacesOf(type, lookup),
572
+ fields: () => Object.fromEntries(type.fields.map(field => [field.name, fieldConfig(type, field)])),
573
+ resolveType: typeResolver(type.discriminator),
574
+ })
520
575
  case 'input':
521
576
  return new GraphQLInputObjectType({
522
577
  name: type.name,
@@ -548,10 +603,33 @@ function createType(
548
603
  })
549
604
  default: {
550
605
  if ('builtin' in type) return builtinScalar(type.name) as GraphQLScalarType
606
+ // `sdl`: the entry's description and specifiedByURL replace the definition's, absent ones too
607
+ const sdl = type.sdl === true
608
+ const overrides = {
609
+ ...(type.description === undefined && !sdl ? {} : { description: type.description }),
610
+ ...(type.specifiedByURL === undefined && !sdl ? {} : { specifiedByURL: type.specifiedByURL }),
611
+ }
551
612
  const scalar = type.scalar
552
- return scalar instanceof GraphQLScalarType
553
- ? scalar
554
- : definitionScalar(type.name, scalar as ScalarDefinition<unknown>)
613
+ if (scalar instanceof GraphQLScalarType) {
614
+ if (Object.keys(overrides).length === 0) return scalar
615
+ const config = scalar.toConfig()
616
+ return new GraphQLScalarType({
617
+ ...config,
618
+ description: 'description' in overrides ? overrides.description : config.description,
619
+ specifiedByURL: 'specifiedByURL' in overrides ? overrides.specifiedByURL : config.specifiedByURL,
620
+ })
621
+ }
622
+ return definitionScalar(type.name, scalar as ScalarDefinition<unknown>, overrides)
555
623
  }
556
624
  }
557
625
  }
626
+
627
+ /** The `interfaces` thunk of an object or interface type (none: no key, as before RFC 0007). */
628
+ function interfacesOf(
629
+ type: GraphQLObjectTypeDefinition | GraphQLInterfaceTypeDefinition,
630
+ lookup: (name: string) => GraphQLNamedType,
631
+ ): { interfaces?: () => GraphQLInterfaceType[] } {
632
+ const names = type.interfaces
633
+ if (names === undefined || names.length === 0) return {}
634
+ return { interfaces: () => names.map(name => lookup(name) as GraphQLInterfaceType) }
635
+ }
package/src/sdl.ts CHANGED
@@ -36,7 +36,7 @@ export interface SdlRoots {
36
36
  }
37
37
 
38
38
  /** A named type of the schema. */
39
- export type SdlType = SdlObjectType | SdlInputType | SdlEnumType | SdlUnionType | SdlScalarType
39
+ export type SdlType = SdlObjectType | SdlInterfaceType | SdlInputType | SdlEnumType | SdlUnionType | SdlScalarType
40
40
 
41
41
  /** What every named type has. */
42
42
  export interface SdlNamedType {
@@ -47,6 +47,15 @@ export interface SdlNamedType {
47
47
  /** An object type (`type Book { ... }`). */
48
48
  export interface SdlObjectType extends SdlNamedType {
49
49
  readonly kind: 'object'
50
+ /** Implemented interfaces, in declaration order. */
51
+ readonly interfaces?: readonly string[]
52
+ readonly fields: readonly SdlField[]
53
+ }
54
+
55
+ /** An interface type (`interface Node { ... }`). */
56
+ export interface SdlInterfaceType extends SdlNamedType {
57
+ readonly kind: 'interface'
58
+ readonly interfaces?: readonly string[]
50
59
  readonly fields: readonly SdlField[]
51
60
  }
52
61
 
@@ -168,7 +177,8 @@ class SdlPrinter {
168
177
  const head = printDescription(type.description, '', true)
169
178
  switch (type.kind) {
170
179
  case 'object':
171
- return `${head}type ${type.name}${printBlock(type.fields.map((field, i) => this.#field(type.name, field, i)))}`
180
+ case 'interface':
181
+ return `${head}${type.kind === 'object' ? 'type' : 'interface'} ${type.name}${printImplements(type.interfaces)}${printBlock(type.fields.map((field, i) => this.#field(type.name, field, i)))}`
172
182
  case 'input':
173
183
  return `${head}input ${type.name}${printBlock(
174
184
  type.fields.map(
@@ -277,6 +287,11 @@ class SdlPrinter {
277
287
  }
278
288
  }
279
289
 
290
+ /** graphql-js `printImplementedInterfaces`. */
291
+ function printImplements(interfaces: readonly string[] | undefined): string {
292
+ return interfaces === undefined || interfaces.length === 0 ? '' : ` implements ${interfaces.join(' & ')}`
293
+ }
294
+
280
295
  /** The `valueToLiteral` of the specified scalars, and graphql-js' default one for the others. */
281
296
  function scalarLiteral(value: unknown, name: string): Literal | undefined {
282
297
  switch (name) {
package/src/types.ts CHANGED
@@ -171,6 +171,41 @@ export interface GraphQLErrorMapper {
171
171
  /** A class producing {@link GraphQLErrorMapper}s. */
172
172
  export type GraphQLErrorMapperClass = abstract new (...args: never[]) => GraphQLErrorMapper
173
173
 
174
+ /**
175
+ * Turns the errors of a field into values of the field (errors as data, RFC 0008). It applies to
176
+ * the resolver-backed fields whose type is a union holding one of the object types it declares
177
+ * (`static types`), decided at boot; every other field keeps its errors in `errors[]`.
178
+ *
179
+ * ```ts
180
+ * @Injectable()
181
+ * export class Rejections implements GraphQLResultMapper {
182
+ * static readonly types = ['NotFoundRejection']
183
+ *
184
+ * toResult(error: unknown): NotFoundRejection | undefined {
185
+ * if (error instanceof NotFoundException) return { __typename: 'NotFoundRejection', message: error.message }
186
+ * return undefined
187
+ * }
188
+ * }
189
+ * ```
190
+ */
191
+ export interface GraphQLResultMapper {
192
+ /**
193
+ * The value of the field for `error`: an object of a member of the field's union, or `undefined`
194
+ * to leave the error to the next mapper, then to `errors[]`. Synchronous. `error` is the raw
195
+ * error, a `GraphQLException` included (a guard returning `false` is `FORBIDDEN`, a Standard
196
+ * Schema failure of the arguments `BAD_USER_INPUT` with `extensions.issues`).
197
+ */
198
+ toResult(error: unknown, ctx: ResolverContext): unknown
199
+ }
200
+
201
+ /** The object types a result mapper may produce: names of object types of the schema. */
202
+ export interface GraphQLResultTypes {
203
+ readonly types: readonly string[]
204
+ }
205
+
206
+ /** A class producing {@link GraphQLResultMapper}s, with the object types it produces (`static types`). */
207
+ export type GraphQLResultMapperClass = (abstract new (...args: never[]) => GraphQLResultMapper) & GraphQLResultTypes
208
+
174
209
  /**
175
210
  * The implementation of a custom scalar (§5.7), given to `scalar<T>(name, definition)`. The
176
211
  * runtime maps it to graphql-js 17 `GraphQLScalarType` coercers: `serialize` → `coerceOutputValue`,