@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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1346 -0
  3. package/dist/compiler.d.mts +114 -0
  4. package/dist/compiler.mjs +4599 -0
  5. package/dist/compiler.mjs.map +1 -0
  6. package/dist/index.d.mts +583 -0
  7. package/dist/index.mjs +134 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/names-BlzDM5cF.mjs +111 -0
  10. package/dist/names-BlzDM5cF.mjs.map +1 -0
  11. package/dist/plugin-CR_wVfRU.mjs +3445 -0
  12. package/dist/plugin-CR_wVfRU.mjs.map +1 -0
  13. package/dist/testing.d.mts +101 -0
  14. package/dist/testing.mjs +306 -0
  15. package/dist/testing.mjs.map +1 -0
  16. package/dist/types-DQ713UgI.d.mts +266 -0
  17. package/package.json +97 -1
  18. package/src/compiler/analyze.ts +727 -0
  19. package/src/compiler/builder.ts +1561 -0
  20. package/src/compiler/checks.ts +105 -0
  21. package/src/compiler/conformance.ts +1068 -0
  22. package/src/compiler/constants.ts +80 -0
  23. package/src/compiler/data.ts +83 -0
  24. package/src/compiler/defaults.ts +92 -0
  25. package/src/compiler/discriminators.ts +53 -0
  26. package/src/compiler/docs.ts +55 -0
  27. package/src/compiler/generate.ts +419 -0
  28. package/src/compiler/graphql-js.ts +42 -0
  29. package/src/compiler/ids.ts +128 -0
  30. package/src/compiler/index.ts +98 -0
  31. package/src/compiler/locate.ts +82 -0
  32. package/src/compiler/model.ts +137 -0
  33. package/src/compiler/options.ts +85 -0
  34. package/src/compiler/scalars.ts +138 -0
  35. package/src/compiler/schema-first.ts +595 -0
  36. package/src/compiler/sdl-source.ts +529 -0
  37. package/src/compiler/sdl-view.ts +61 -0
  38. package/src/compiler/suggest.ts +39 -0
  39. package/src/context.ts +181 -0
  40. package/src/decisions.ts +169 -0
  41. package/src/decorators.ts +202 -0
  42. package/src/errors.ts +89 -0
  43. package/src/execution.ts +1064 -0
  44. package/src/format.ts +285 -0
  45. package/src/index.ts +89 -0
  46. package/src/limits.ts +177 -0
  47. package/src/loaders.ts +149 -0
  48. package/src/memory-pubsub.ts +133 -0
  49. package/src/metadata.ts +222 -0
  50. package/src/names.ts +47 -0
  51. package/src/options.ts +331 -0
  52. package/src/pipeline.ts +338 -0
  53. package/src/plugin.ts +579 -0
  54. package/src/pubsub.ts +36 -0
  55. package/src/results.ts +206 -0
  56. package/src/scalars.ts +138 -0
  57. package/src/schema-hash.ts +18 -0
  58. package/src/schema.ts +635 -0
  59. package/src/sdl.ts +530 -0
  60. package/src/testing.ts +454 -0
  61. package/src/tokens.ts +22 -0
  62. package/src/transport/abort.ts +80 -0
  63. package/src/transport/accept.ts +155 -0
  64. package/src/transport/graphiql.ts +106 -0
  65. package/src/transport/http.ts +373 -0
  66. package/src/transport/sse.ts +45 -0
  67. package/src/type-ref.ts +89 -0
  68. package/src/types.ts +225 -0
  69. package/src/validate-args.ts +48 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Source locations of diagnostics. Computed only when a diagnostic is reported: the happy path
3
+ * never reads source text.
4
+ */
5
+ import type { Location, MethodInfo, Project } from '@znight/compiler'
6
+
7
+ /** From the member name: `name?: ` or `name(): ` (a getter), up to the annotation. */
8
+ const MEMBER_HEAD = /^(?:[\w$]+|'[^']*'|"[^"]*")\s*\??\s*(?:\(\s*\))?\s*:\s*/
9
+
10
+ const OPENING = new Set(['(', '[', '{', '<'])
11
+ const CLOSING = new Set([')', ']', '}', '>'])
12
+
13
+ /**
14
+ * The type annotation of a property, a property signature or a getter (`get x(): T`), from the
15
+ * location of the member (its name); the member itself when the annotation cannot be found.
16
+ */
17
+ export function annotationLocation(project: Project, member: Location | undefined): Location | undefined {
18
+ if (member === undefined) return undefined
19
+ const file = project.getFile(member.file)
20
+ if (file === undefined) return member
21
+ const text = file.text
22
+ const head = MEMBER_HEAD.exec(text.slice(member.start, member.start + 256))
23
+ if (head === null) return member
24
+ const start = member.start + head[0].length
25
+ let depth = 0
26
+ let end = start
27
+ for (let index = start; index < text.length; index++) {
28
+ const char = text[index] as string
29
+ if (char === '=' && text[index + 1] === '>') {
30
+ index++
31
+ continue
32
+ }
33
+ // a getter body or an initializer after the annotation
34
+ if (depth === 0 && end > start && (char === '{' || char === '=')) break
35
+ if (OPENING.has(char)) depth++
36
+ else if (CLOSING.has(char)) {
37
+ if (depth === 0) break
38
+ depth--
39
+ } else if (depth === 0 && (char === ';' || char === ',')) break
40
+ else if (depth === 0 && char === '\n' && !/^\s*[|&]/.test(text.slice(index + 1, index + 64))) break
41
+ if (!/\s/.test(char)) end = index + 1
42
+ }
43
+ return end > start ? file.location({ start, end }) : member
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
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The schema model built by `validate` (RFC 0005 §6.1) and emitted by `generate` (§7): the GraphQL
3
+ * types reachable from the root fields, with their TypeScript origins (locations, resolver data).
4
+ */
5
+ import type { Location } from '@znight/compiler'
6
+ import type { MetaEntry } from '@znight/http/compiler'
7
+ import type { GraphQLTypeRef } from '../type-ref.ts'
8
+ import type { FieldData, ResolverData } from './data.ts'
9
+
10
+ /** A field of an object type. */
11
+ export interface FieldModel {
12
+ readonly name: string
13
+ /** Printed type reference (`'[Book!]!'`). */
14
+ type: string
15
+ description: string | undefined
16
+ deprecationReason: string | undefined
17
+ readonly args: ArgModel[]
18
+ /** The resolver method behind the field; `undefined`: the default resolver (`parent[name]`). */
19
+ binding: { readonly resolver: ResolverData; readonly field: FieldData } | undefined
20
+ readonly location: Location | undefined
21
+ }
22
+
23
+ /** An argument or an input field. */
24
+ export interface ArgModel {
25
+ readonly name: string
26
+ type: string
27
+ ref: GraphQLTypeRef
28
+ readonly description: string | undefined
29
+ readonly deprecationReason: string | undefined
30
+ /** External default value (enum values by name); `undefined`: no default. */
31
+ defaultValue: unknown
32
+ /** The TypeScript type does not accept `null` (§5.4). */
33
+ nullAsUndefined: boolean
34
+ readonly location: Location | undefined
35
+ }
36
+
37
+ /** What every named type of the model has. */
38
+ interface BaseModel {
39
+ readonly name: string
40
+ /** The declaration key (shapes) or a synthetic key (`#root:query`, `#builtin:DateTime`). */
41
+ readonly key: string
42
+ readonly description: string | undefined
43
+ readonly location: Location | undefined
44
+ }
45
+
46
+ /** An object type (root types included). */
47
+ export interface ObjectModel extends BaseModel {
48
+ readonly kind: 'object'
49
+ readonly fields: FieldModel[]
50
+ /** How the type was first reached (`Query.book → Book`), for "reached from" diagnostics. */
51
+ readonly trail: string
52
+ /** The root field the type was first reached from. */
53
+ readonly root: Location | undefined
54
+ /** Identity of the class when the type is a non-generic class (union discriminators). */
55
+ readonly classIdentity: string | undefined
56
+ /** Property names of the declaration, with their literal value when they have one (discriminators). */
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
68
+ }
69
+
70
+ /** A property of an object type's declaration, as union discriminators see it. */
71
+ export interface DiscriminantProperty {
72
+ readonly name: string
73
+ readonly optional: boolean
74
+ /** The literal value of a literal-typed property. */
75
+ readonly literal: string | number | boolean | undefined
76
+ }
77
+
78
+ /** An input object type. */
79
+ export interface InputModel extends BaseModel {
80
+ readonly kind: 'input'
81
+ readonly fields: ArgModel[]
82
+ }
83
+
84
+ /** An enum type. */
85
+ export interface EnumModel extends BaseModel {
86
+ readonly kind: 'enum'
87
+ readonly values: readonly EnumValueModel[]
88
+ }
89
+
90
+ /** A value of an enum. */
91
+ export interface EnumValueModel {
92
+ readonly name: string
93
+ readonly value: string | number
94
+ readonly description: string | undefined
95
+ readonly deprecationReason: string | undefined
96
+ }
97
+
98
+ /** A union type. */
99
+ export interface UnionModel extends BaseModel {
100
+ readonly kind: 'union'
101
+ members: readonly string[]
102
+ discriminator: DiscriminatorModel | undefined
103
+ }
104
+
105
+ /** How the runtime tells union members apart. */
106
+ export type DiscriminatorModel =
107
+ | { readonly by: 'property'; readonly property: string; readonly values: Readonly<Record<string, string>> }
108
+ | { readonly by: 'class'; readonly classes: readonly { readonly identity: string; readonly name: string }[] }
109
+ | { readonly by: 'typename' }
110
+
111
+ /** A scalar: built in (`DateTime`, `BigInt`, `JSON`) or declared with `scalar<T>()`. */
112
+ export interface ScalarModel extends BaseModel {
113
+ readonly kind: 'scalar'
114
+ /** Identity of the exported `scalar<T>()` constant; `undefined` for a built-in scalar. */
115
+ readonly definition: string | undefined
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
122
+ }
123
+
124
+ /** A named type of the model. */
125
+ export type TypeModel = ObjectModel | InterfaceModel | InputModel | EnumModel | UnionModel | ScalarModel
126
+
127
+ /** The schema of the application, built once per compile. */
128
+ export interface GraphQLModel {
129
+ readonly roots: { readonly query: string; readonly mutation?: string; readonly subscription?: string }
130
+ /** Sorted by name. */
131
+ readonly types: readonly TypeModel[]
132
+ /** Route metadata entries of every field (effective), for `groups`. */
133
+ readonly meta: readonly MetaEntry[]
134
+ /** The SDL (`printSdl`), without trailing newline. */
135
+ readonly sdl: string
136
+ readonly schemaHash: string
137
+ }
@@ -0,0 +1,85 @@
1
+ /**
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
+ */
5
+
6
+ /** Options of `graphql.configure()`. */
7
+ export interface GraphQLCompilerOptions {
8
+ /**
9
+ * Path of the SDL artifact, relative to the project root (`'schema.graphql'`), written next to the
10
+ * generated module and checked by `znight generate --check`. Default `false`: no file.
11
+ */
12
+ readonly sdl?: string | false
13
+ /** GraphQL type of `number`. Default `'Float'`; `'Int'` suits counts and identifiers. Code-first only. */
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
21
+ }
22
+
23
+ /** {@link GraphQLCompilerOptions} with their defaults. */
24
+ export interface ResolvedGraphQLCompilerOptions {
25
+ readonly sdl: string | false
26
+ readonly number: 'Float' | 'Int'
27
+ /** The schema-first globs; `false`: code-first. */
28
+ readonly schema: readonly string[] | false
29
+ }
30
+
31
+ /** The defaults. */
32
+ export const DEFAULT_OPTIONS: ResolvedGraphQLCompilerOptions = Object.freeze({
33
+ sdl: false,
34
+ number: 'Float',
35
+ schema: false,
36
+ })
37
+
38
+ const KNOWN = new Set(['sdl', 'number', 'schema'])
39
+
40
+ /**
41
+ * Merges `options` onto `base`. Throws a `TypeError` naming the option on an invalid value: options
42
+ * are written in `znight.config.ts`, the error surfaces when the configuration is loaded.
43
+ */
44
+ export function resolveOptions(
45
+ base: ResolvedGraphQLCompilerOptions,
46
+ options: GraphQLCompilerOptions,
47
+ ): ResolvedGraphQLCompilerOptions {
48
+ if (typeof options !== 'object' || options === null || Array.isArray(options)) {
49
+ throw new TypeError('[znight] graphql.configure() expects an options object: { sdl?, number?, schema? }')
50
+ }
51
+ for (const key of Object.keys(options)) {
52
+ if (!KNOWN.has(key))
53
+ throw new TypeError(`[znight] graphql.configure(): unknown option "${key}" (options: sdl, number, schema)`)
54
+ }
55
+ const sdl = options.sdl === undefined ? base.sdl : options.sdl
56
+ if (sdl !== false && (typeof sdl !== 'string' || sdl.trim() === '')) {
57
+ throw new TypeError(
58
+ '[znight] graphql.configure(): "sdl" must be a path relative to the project root (\'schema.graphql\') or false',
59
+ )
60
+ }
61
+ const number = options.number === undefined ? base.number : options.number
62
+ if (number !== 'Float' && number !== 'Int') {
63
+ throw new TypeError(`[znight] graphql.configure(): "number" must be 'Float' or 'Int'`)
64
+ }
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])
85
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * `scalar<T>(name, definition)` declarations of the application (RFC 0005 §5.7): `T` (a plain type
3
+ * reference, by identity) maps to the scalar `name` wherever it appears. The definition is an
4
+ * exported value the generated module imports; its `description` and `specifiedByURL` are read
5
+ * for the SDL when they are statically evaluable.
6
+ */
7
+ import type { ArgInfo, Diagnostic, Location, ValidateContext } from '@znight/compiler'
8
+ import type { GraphQLData } from './data.ts'
9
+
10
+ import { isGraphQLName, SPECIFIED_SCALARS } from '../names.ts'
11
+ import { Codes } from './constants.ts'
12
+ import { GraphQLIds, SCALAR_ALIASES } from './ids.ts'
13
+
14
+ /** A valid `scalar<T>()` declaration. */
15
+ export interface ScalarDeclaration {
16
+ /** GraphQL name. */
17
+ readonly name: string
18
+ /** Identity of the exported constant (imported by the generated module). */
19
+ readonly identity: string
20
+ /** Identity of `T`. */
21
+ readonly type: string
22
+ readonly description: string | undefined
23
+ readonly specifiedByURL: string | undefined
24
+ /** Location of the constant's name. */
25
+ readonly location: Location
26
+ }
27
+
28
+ /** Valid declarations by the identity of their type; invalid ones are reported. */
29
+ export function readScalars(
30
+ ctx: ValidateContext<GraphQLData>,
31
+ report: (diagnostic: Diagnostic) => void,
32
+ ): ReadonlyMap<string, ScalarDeclaration> {
33
+ const byType = new Map<string, ScalarDeclaration>()
34
+ const byName = new Map<string, ScalarDeclaration>()
35
+ for (const definition of ctx.definitionsOf(GraphQLIds.scalar)) {
36
+ const fail = (message: string, hints: readonly string[], related: Diagnostic['related'] = []): void =>
37
+ report({ code: Codes.InvalidDecorator, severity: 'error', message, location: definition.call, hints, related })
38
+ const [nameArg, definitionArg] = definition.args
39
+ const name = nameArg?.evaluated === true ? nameArg.value : undefined
40
+ if (typeof name !== 'string') {
41
+ fail(`scalar() of ${definition.name} needs a static name`, [`scalar<T>('Name', { ... })`])
42
+ continue
43
+ }
44
+ if (!isGraphQLName(name) || SPECIFIED_SCALARS.has(name)) {
45
+ report({
46
+ code: Codes.NameConflict,
47
+ severity: 'error',
48
+ message: SPECIFIED_SCALARS.has(name)
49
+ ? `'${name}' is a specified GraphQL scalar: scalar() cannot redefine it (${definition.name})`
50
+ : `'${name}' is not a valid GraphQL scalar name (${definition.name})`,
51
+ location: nameArg?.location ?? definition.call,
52
+ hints: ['GraphQL names match /^[_A-Za-z][_0-9A-Za-z]*$/ and do not start with __'],
53
+ })
54
+ continue
55
+ }
56
+ const type = definition.typeArgIdentities?.[0]
57
+ if (type === undefined) {
58
+ report({
59
+ code: Codes.InvalidType,
60
+ severity: 'error',
61
+ message: `scalar('${name}') of ${definition.name} needs a named type argument: scalar<T>('${name}', ...)`,
62
+ location: definition.call,
63
+ hints: [
64
+ `Name the TypeScript type of the scalar: export type ${name} = string & { readonly __brand: '${name}' }, then scalar<${name}>('${name}', ...)`,
65
+ ],
66
+ })
67
+ continue
68
+ }
69
+ if (SCALAR_ALIASES.has(type)) {
70
+ fail(
71
+ `scalar('${name}') of ${definition.name} cannot map ${type.slice(type.indexOf('#') + 1)}: it is a built-in scalar alias`,
72
+ ['Declare a branded type for the scalar: export type Name = string & { readonly __brand: "Name" }'],
73
+ )
74
+ continue
75
+ }
76
+ if (!definition.exported) {
77
+ fail(`The scalar ${definition.name} must be exported: the generated module imports it`, [
78
+ `export const ${definition.name} = scalar<...>('${name}', ...)`,
79
+ ])
80
+ continue
81
+ }
82
+ const declaration: ScalarDeclaration = {
83
+ name,
84
+ identity: definition.identity,
85
+ type,
86
+ description: staticProperty(ctx, definitionArg, 'description'),
87
+ specifiedByURL: staticProperty(ctx, definitionArg, 'specifiedByURL'),
88
+ location: definition.location,
89
+ }
90
+ const sameType = byType.get(type)
91
+ if (sameType !== undefined) {
92
+ report({
93
+ code: Codes.NameConflict,
94
+ severity: 'error',
95
+ message: `${definition.name} and ${sameType.identity} both map ${type.slice(type.indexOf('#') + 1)} to a scalar`,
96
+ location: definition.location,
97
+ related: [{ message: 'the other scalar', location: sameType.location }],
98
+ hints: ['Keep one scalar per TypeScript type'],
99
+ })
100
+ continue
101
+ }
102
+ // a user scalar named like a built-in one (DateTime) only conflicts when both are used: the model reports it
103
+ const sameName = byName.get(name)
104
+ if (sameName !== undefined) {
105
+ report({
106
+ code: Codes.NameConflict,
107
+ severity: 'error',
108
+ message: `Two scalars are named '${name}': ${definition.identity} and ${sameName.identity}`,
109
+ location: definition.location,
110
+ related: [{ message: 'the other scalar', location: sameName.location }],
111
+ hints: ['Rename one of them'],
112
+ })
113
+ continue
114
+ }
115
+ byType.set(type, declaration)
116
+ byName.set(name, declaration)
117
+ }
118
+ return byType
119
+ }
120
+
121
+ /** A string property of an object literal argument, when statically evaluable. */
122
+ function staticProperty(ctx: ValidateContext<GraphQLData>, arg: ArgInfo | undefined, key: string): string | undefined {
123
+ if (arg === undefined) return undefined
124
+ if (arg.evaluated && typeof arg.value === 'object' && arg.value !== null && !Array.isArray(arg.value)) {
125
+ const value = (arg.value as Readonly<Record<string, unknown>>)[key]
126
+ return typeof value === 'string' ? value : undefined
127
+ }
128
+ const node = arg.node
129
+ if (node.type !== 'ObjectExpression') return undefined
130
+ for (const property of node.properties) {
131
+ if (property.type !== 'Property' || property.computed) continue
132
+ const name = property.key.type === 'Identifier' ? property.key.name : undefined
133
+ if (name !== key) continue
134
+ const result = ctx.project.evaluate(arg.location.file, property.value, { refs: 'none' })
135
+ return result.kind === 'value' && typeof result.value === 'string' ? result.value : undefined
136
+ }
137
+ return undefined
138
+ }