@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
@@ -25,6 +25,10 @@ export const Codes: {
25
25
  readonly UnresolvableAbstractType: ErrorCode
26
26
  /** ZNIGHT065: `@Loader()` parameters and loader providers. */
27
27
  readonly InvalidLoader: ErrorCode
28
+ /** ZNIGHT067: schema-first SDL files (RFC 0007): reading, syntax, validation, unsupported constructs. */
29
+ readonly InvalidSdl: ErrorCode
30
+ /** ZNIGHT068: schema-first resolvers against the SDL (RFC 0007): coverage and type conformance. */
31
+ readonly SchemaMismatch: ErrorCode
28
32
  } = Object.freeze({
29
33
  InvalidDecorator: 'ZNIGHT005',
30
34
  MissingRouteDecision: 'ZNIGHT023',
@@ -34,6 +38,8 @@ export const Codes: {
34
38
  InvalidInput: 'ZNIGHT063',
35
39
  UnresolvableAbstractType: 'ZNIGHT064',
36
40
  InvalidLoader: 'ZNIGHT065',
41
+ InvalidSdl: 'ZNIGHT067',
42
+ SchemaMismatch: 'ZNIGHT068',
37
43
  })
38
44
 
39
45
  /** Root reason given to `AnalyzeContext.addRoot()`. */
@@ -20,6 +20,16 @@ export interface ResolverData {
20
20
  readonly scope: Scope
21
21
  /** Location of `@Resolver`. */
22
22
  readonly location: Location
23
+ /**
24
+ * `@Resolver('Event')`: the SDL object type its `@ResolveField` methods belong to (schema-first,
25
+ * RFC 0007), with the location of the argument.
26
+ */
27
+ readonly typeName?: { readonly name: string; readonly location: Location }
28
+ /**
29
+ * Schema-first: a class the analysis rejected (an error was reported); it only tells which fields
30
+ * its methods name (never bound, never a provider).
31
+ */
32
+ readonly invalid?: true
23
33
  /** Class `@UseGuard` guards. */
24
34
  readonly guards: readonly ClassRef[]
25
35
  /** One entry per field decorator of the class's methods, in source order. */
@@ -17,7 +17,7 @@ import type { GenerateContext, GenInput, GenNode, GenParam } from '@znight/compi
17
17
  import type { Token } from '@znight/core'
18
18
  import type { MetaEntry } from '@znight/http/compiler'
19
19
  import type { FieldData, GraphQLData, ParamData, ResolverData } from './data.ts'
20
- import type { ArgModel, FieldModel, GraphQLModel, TypeModel } from './model.ts'
20
+ import type { ArgModel, DiscriminatorModel, FieldModel, GraphQLModel, TypeModel } from './model.ts'
21
21
  import type { ResolvedGraphQLCompilerOptions } from './options.ts'
22
22
 
23
23
  import { GRAPHQL_METADATA_VERSION } from '../metadata.ts'
@@ -106,7 +106,13 @@ function typeNode(ctx: Context, imports: Imports, type: TypeModel): GenNode {
106
106
  if (type.description !== undefined && type.kind !== 'scalar') head.push(['description', type.description])
107
107
  switch (type.kind) {
108
108
  case 'object':
109
- return ctx.obj([...head, ['fields', ctx.arr(type.fields.map(field => fieldNode(ctx, imports, field)))]])
109
+ case 'interface': {
110
+ const entries: [string, GenInput][] = [...head]
111
+ if (type.interfaces.length > 0) entries.push(['interfaces', ctx.lit([...type.interfaces])])
112
+ entries.push(['fields', ctx.arr(type.fields.map(field => fieldNode(ctx, imports, field)))])
113
+ if (type.kind === 'interface') entries.push(['discriminator', discriminatorNode(ctx, type.discriminator)])
114
+ return ctx.obj(entries)
115
+ }
110
116
  case 'input':
111
117
  return ctx.obj([...head, ['fields', ctx.arr(type.fields.map(field => inputValueNode(ctx, field)))]])
112
118
  case 'enum':
@@ -127,36 +133,44 @@ function typeNode(ctx: Context, imports: Imports, type: TypeModel): GenNode {
127
133
  ),
128
134
  ],
129
135
  ])
130
- case 'union': {
131
- const discriminator = type.discriminator
132
- let node: GenNode
133
- if (discriminator === undefined || discriminator.by === 'typename') node = ctx.obj([['by', 'typename']])
134
- else if (discriminator.by === 'property') {
135
- node = ctx.obj([
136
- ['by', 'property'],
137
- ['property', discriminator.property],
138
- ['values', ctx.obj(Object.entries(discriminator.values))],
139
- ])
140
- } else {
141
- node = ctx.obj([
142
- ['by', 'class'],
143
- [
144
- 'classes',
145
- ctx.arr(discriminator.classes.map(entry => ctx.arr([ctx.importValue(entry.identity), entry.name]))),
146
- ],
147
- ])
136
+ case 'union':
137
+ return ctx.obj([
138
+ ...head,
139
+ ['members', ctx.lit([...type.members])],
140
+ ['discriminator', discriminatorNode(ctx, type.discriminator)],
141
+ ])
142
+ case 'scalar': {
143
+ // built-in scalars carry no description (the runtime builds them, sdl.ts prints them alike);
144
+ // code-first user scalars bring theirs with the imported definition, schema-first ones take
145
+ // the SDL's (they win over the definition's)
146
+ if (type.definition === undefined) return ctx.obj([...head, ['builtin', ctx.lit(true)]])
147
+ const entries: [string, GenInput][] = [...head, ['scalar', ctx.importValue(type.definition)]]
148
+ if (type.fromSdl === true) {
149
+ if (type.description !== undefined) entries.push(['description', type.description])
150
+ if (type.specifiedByURL !== undefined) entries.push(['specifiedByURL', type.specifiedByURL])
151
+ entries.push(['sdl', ctx.lit(true)])
148
152
  }
149
- return ctx.obj([...head, ['members', ctx.lit([...type.members])], ['discriminator', node]])
153
+ return ctx.obj(entries)
150
154
  }
151
- case 'scalar':
152
- // built-in scalars carry no description (the runtime builds them, sdl.ts prints them alike);
153
- // user scalars bring theirs with the imported definition
154
- return type.definition === undefined
155
- ? ctx.obj([...head, ['builtin', ctx.lit(true)]])
156
- : ctx.obj([...head, ['scalar', ctx.importValue(type.definition)]])
157
155
  }
158
156
  }
159
157
 
158
+ /** How the runtime tells the members of a union or the implementations of an interface apart. */
159
+ function discriminatorNode(ctx: Context, discriminator: DiscriminatorModel | undefined): GenNode {
160
+ if (discriminator === undefined || discriminator.by === 'typename') return ctx.obj([['by', 'typename']])
161
+ if (discriminator.by === 'property') {
162
+ return ctx.obj([
163
+ ['by', 'property'],
164
+ ['property', discriminator.property],
165
+ ['values', ctx.obj(Object.entries(discriminator.values))],
166
+ ])
167
+ }
168
+ return ctx.obj([
169
+ ['by', 'class'],
170
+ ['classes', ctx.arr(discriminator.classes.map(entry => ctx.arr([ctx.importValue(entry.identity), entry.name])))],
171
+ ])
172
+ }
173
+
160
174
  function fieldNode(ctx: Context, imports: Imports, field: FieldModel): GenNode {
161
175
  const entries: [string, GenInput][] = [
162
176
  ['name', field.name],
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The graphql-js modules schema-first compiles use (RFC 0007 §7): the language (parser, AST), the
3
+ * type system (`validateSchema`, type predicates) and the utilities (`buildASTSchema`,
4
+ * `concatAST`, `valueFromASTUntyped`). Loaded lazily, on the first schema-first compile, with
5
+ * `createRequire`: code-first compiles never load graphql-js, and the execution engine
6
+ * (`graphql/execution`, the `graphql` root entry) and dataloader are never loaded at build time
7
+ * (tests/boundaries.test.ts). graphql is a required peer of `@znight/graphql`.
8
+ */
9
+ import type * as Language from 'graphql/language'
10
+ import type * as Type from 'graphql/type'
11
+ import type * as Utilities from 'graphql/utilities'
12
+
13
+ import { createRequire } from 'node:module'
14
+
15
+ /** The build-time graphql-js modules. */
16
+ export interface GraphQLBuildModules {
17
+ readonly language: typeof Language
18
+ readonly type: typeof Type
19
+ readonly utilities: typeof Utilities
20
+ }
21
+
22
+ /** The only graphql-js modules the compiler extension may load (checked by tests/boundaries.test.ts). */
23
+ export const GRAPHQL_BUILD_MODULES: readonly string[] = Object.freeze([
24
+ 'graphql/language',
25
+ 'graphql/type',
26
+ 'graphql/utilities',
27
+ ])
28
+
29
+ let loaded: GraphQLBuildModules | undefined
30
+
31
+ /** The graphql-js modules, loaded on first use. */
32
+ export function graphqlModules(): GraphQLBuildModules {
33
+ if (loaded === undefined) {
34
+ const load = createRequire(import.meta.url)
35
+ loaded = Object.freeze({
36
+ language: load('graphql/language') as typeof Language,
37
+ type: load('graphql/type') as typeof Type,
38
+ utilities: load('graphql/utilities') as typeof Utilities,
39
+ })
40
+ }
41
+ return loaded
42
+ }
@@ -32,6 +32,7 @@ import { buildModel } from './builder.ts'
32
32
  import { generateGraphQL } from './generate.ts'
33
33
  import { EXTENSION_NAME, GraphQLIds } from './ids.ts'
34
34
  import { DEFAULT_OPTIONS, resolveOptions } from './options.ts'
35
+ import { buildSchemaFirstModel } from './schema-first.ts'
35
36
 
36
37
  export type { FieldData, FieldOperation, GraphQLData, ParamData, ResolverData } from './data.ts'
37
38
  export type { GraphQLCompilerOptions, ResolvedGraphQLCompilerOptions } from './options.ts'
@@ -41,7 +42,7 @@ const NO_ARGUMENT: DecoratorSpec = { targets: ['parameter'], params: [] }
41
42
 
42
43
  /** Decorators owned by the extension, by canonical identity (static / value arguments, §4.6). */
43
44
  const decorators: Readonly<Record<string, DecoratorSpec>> = Object.freeze({
44
- [GraphQLIds.Resolver]: { targets: ['class'], params: ['static'] },
45
+ [GraphQLIds.Resolver]: { targets: ['class'], params: ['static', 'static'] },
45
46
  [GraphQLIds.Query]: FIELD,
46
47
  [GraphQLIds.Mutation]: FIELD,
47
48
  [GraphQLIds.Subscription]: FIELD,
@@ -74,9 +75,13 @@ function createExtension(options: ResolvedGraphQLCompilerOptions): GraphQLCompil
74
75
  name: EXTENSION_NAME,
75
76
  decorators,
76
77
  options,
77
- analyzeClass: analyzeGraphQLClass,
78
+ analyzeClass: (cls, ctx) => analyzeGraphQLClass(cls, ctx, options),
78
79
  validate(ctx) {
79
- models.set(ctx.graph.extensions, buildModel(ctx, options))
80
+ const schema = options.schema
81
+ models.set(
82
+ ctx.graph.extensions,
83
+ schema === false ? buildModel(ctx, options) : buildSchemaFirstModel(ctx, schema, options.sdl),
84
+ )
80
85
  },
81
86
  generate(ctx) {
82
87
  return generateGraphQL(ctx, modelOf(ctx.graph), options)
@@ -2,7 +2,7 @@
2
2
  * Source locations of diagnostics. Computed only when a diagnostic is reported: the happy path
3
3
  * never reads source text.
4
4
  */
5
- import type { Location, Project } from '@znight/compiler'
5
+ import type { Location, MethodInfo, Project } from '@znight/compiler'
6
6
 
7
7
  /** From the member name: `name?: ` or `name(): ` (a getter), up to the annotation. */
8
8
  const MEMBER_HEAD = /^(?:[\w$]+|'[^']*'|"[^"]*")\s*\??\s*(?:\(\s*\))?\s*:\s*/
@@ -42,3 +42,41 @@ export function annotationLocation(project: Project, member: Location | undefine
42
42
  }
43
43
  return end > start ? file.location({ start, end }) : member
44
44
  }
45
+
46
+ /**
47
+ * The return type annotation of a method, from the location of its name: after the parameter list
48
+ * (balanced brackets), `: T` up to the body; `undefined` when it cannot be found.
49
+ */
50
+ export function returnTypeLocation(project: Project, method: MethodInfo): Location | undefined {
51
+ const file = project.getFile(method.location.file)
52
+ if (file === undefined) return undefined
53
+ const text = file.text
54
+ let index = text.indexOf('(', method.location.end)
55
+ if (index === -1) return undefined
56
+ let depth = 0
57
+ for (; index < text.length; index++) {
58
+ const char = text[index] as string
59
+ if (OPENING.has(char)) depth++
60
+ else if (CLOSING.has(char) && --depth === 0) break
61
+ }
62
+ const head = /^\s*:\s*/.exec(text.slice(index + 1, index + 64))
63
+ if (head === null) return undefined
64
+ const start = index + 1 + head[0].length
65
+ let end = start
66
+ depth = 0
67
+ for (let cursor = start; cursor < text.length; cursor++) {
68
+ const char = text[cursor] as string
69
+ if (char === '=' && text[cursor + 1] === '>') {
70
+ cursor++
71
+ continue
72
+ }
73
+ if (depth === 0 && (char === '{' || char === ';') && end > start) break
74
+ if (OPENING.has(char)) depth++
75
+ else if (CLOSING.has(char)) {
76
+ if (depth === 0) break
77
+ depth--
78
+ }
79
+ if (!/\s/.test(char)) end = cursor + 1
80
+ }
81
+ return end > start ? file.location({ start, end }) : undefined
82
+ }
@@ -55,6 +55,16 @@ export interface ObjectModel extends BaseModel {
55
55
  readonly classIdentity: string | undefined
56
56
  /** Property names of the declaration, with their literal value when they have one (discriminators). */
57
57
  readonly properties: readonly DiscriminantProperty[]
58
+ /** Implemented interfaces (schema-first, RFC 0007), in declaration order; empty in code-first. */
59
+ readonly interfaces: readonly string[]
60
+ }
61
+
62
+ /** An interface type (schema-first, RFC 0007). Its fields have no binding. */
63
+ export interface InterfaceModel extends BaseModel {
64
+ readonly kind: 'interface'
65
+ readonly fields: FieldModel[]
66
+ readonly interfaces: readonly string[]
67
+ discriminator: DiscriminatorModel | undefined
58
68
  }
59
69
 
60
70
  /** A property of an object type's declaration, as union discriminators see it. */
@@ -104,10 +114,15 @@ export interface ScalarModel extends BaseModel {
104
114
  /** Identity of the exported `scalar<T>()` constant; `undefined` for a built-in scalar. */
105
115
  readonly definition: string | undefined
106
116
  readonly specifiedByURL: string | undefined
117
+ /**
118
+ * Schema-first: the description and `specifiedByURL` come from the SDL and are emitted in the
119
+ * metadata, where they win over the definition's (RFC 0007 §5.2).
120
+ */
121
+ readonly fromSdl?: true
107
122
  }
108
123
 
109
124
  /** A named type of the model. */
110
- export type TypeModel = ObjectModel | InputModel | EnumModel | UnionModel | ScalarModel
125
+ export type TypeModel = ObjectModel | InterfaceModel | InputModel | EnumModel | UnionModel | ScalarModel
111
126
 
112
127
  /** The schema of the application, built once per compile. */
113
128
  export interface GraphQLModel {
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Options of the compiler extension (RFC 0005 §6.7), given with `graphql.configure({ ... })` in
3
- * `znight.config.ts`. Import-free.
2
+ * Options of the compiler extension (RFC 0005 §6.7, RFC 0007 §4.1), given with
3
+ * `graphql.configure({ ... })` in `znight.config.ts`. Import-free.
4
4
  */
5
5
 
6
6
  /** Options of `graphql.configure()`. */
@@ -10,20 +10,32 @@ export interface GraphQLCompilerOptions {
10
10
  * generated module and checked by `znight generate --check`. Default `false`: no file.
11
11
  */
12
12
  readonly sdl?: string | false
13
- /** GraphQL type of `number`. Default `'Float'`; `'Int'` suits counts and identifiers. */
13
+ /** GraphQL type of `number`. Default `'Float'`; `'Int'` suits counts and identifiers. Code-first only. */
14
14
  readonly number?: 'Float' | 'Int'
15
+ /**
16
+ * Schema-first mode (RFC 0007): globs of the `.graphql` files that define the schema, relative to
17
+ * the project root (`'src/**\/*.graphql'`, `!` negations allowed). Resolvers are bound to their
18
+ * fields by name and checked against the SDL. Default `false`: code-first.
19
+ */
20
+ readonly schema?: string | readonly string[] | false
15
21
  }
16
22
 
17
23
  /** {@link GraphQLCompilerOptions} with their defaults. */
18
24
  export interface ResolvedGraphQLCompilerOptions {
19
25
  readonly sdl: string | false
20
26
  readonly number: 'Float' | 'Int'
27
+ /** The schema-first globs; `false`: code-first. */
28
+ readonly schema: readonly string[] | false
21
29
  }
22
30
 
23
31
  /** The defaults. */
24
- export const DEFAULT_OPTIONS: ResolvedGraphQLCompilerOptions = Object.freeze({ sdl: false, number: 'Float' })
32
+ export const DEFAULT_OPTIONS: ResolvedGraphQLCompilerOptions = Object.freeze({
33
+ sdl: false,
34
+ number: 'Float',
35
+ schema: false,
36
+ })
25
37
 
26
- const KNOWN = new Set(['sdl', 'number'])
38
+ const KNOWN = new Set(['sdl', 'number', 'schema'])
27
39
 
28
40
  /**
29
41
  * Merges `options` onto `base`. Throws a `TypeError` naming the option on an invalid value: options
@@ -34,11 +46,11 @@ export function resolveOptions(
34
46
  options: GraphQLCompilerOptions,
35
47
  ): ResolvedGraphQLCompilerOptions {
36
48
  if (typeof options !== 'object' || options === null || Array.isArray(options)) {
37
- throw new TypeError('[znight] graphql.configure() expects an options object: { sdl?, number? }')
49
+ throw new TypeError('[znight] graphql.configure() expects an options object: { sdl?, number?, schema? }')
38
50
  }
39
51
  for (const key of Object.keys(options)) {
40
52
  if (!KNOWN.has(key))
41
- throw new TypeError(`[znight] graphql.configure(): unknown option "${key}" (options: sdl, number)`)
53
+ throw new TypeError(`[znight] graphql.configure(): unknown option "${key}" (options: sdl, number, schema)`)
42
54
  }
43
55
  const sdl = options.sdl === undefined ? base.sdl : options.sdl
44
56
  if (sdl !== false && (typeof sdl !== 'string' || sdl.trim() === '')) {
@@ -50,5 +62,24 @@ export function resolveOptions(
50
62
  if (number !== 'Float' && number !== 'Int') {
51
63
  throw new TypeError(`[znight] graphql.configure(): "number" must be 'Float' or 'Int'`)
52
64
  }
53
- return Object.freeze({ sdl, number })
65
+ return Object.freeze({ sdl, number, schema: resolveSchema(base.schema, options.schema) })
66
+ }
67
+
68
+ function resolveSchema(
69
+ base: readonly string[] | false,
70
+ schema: GraphQLCompilerOptions['schema'],
71
+ ): readonly string[] | false {
72
+ if (schema === undefined) return base
73
+ if (schema === false) return false
74
+ const globs = typeof schema === 'string' ? [schema] : schema
75
+ if (
76
+ !Array.isArray(globs) ||
77
+ globs.length === 0 ||
78
+ globs.some(glob => typeof glob !== 'string' || glob.trim() === '')
79
+ ) {
80
+ throw new TypeError(
81
+ '[znight] graphql.configure(): "schema" must be a glob or a non-empty list of globs relative to the project root (\'src/**/*.graphql\'), or false',
82
+ )
83
+ }
84
+ return Object.freeze([...globs])
54
85
  }