@znight/graphql 0.0.0-stage → 0.2.0

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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1062 -2
  3. package/dist/compiler.d.mts +93 -0
  4. package/dist/compiler.mjs +2832 -0
  5. package/dist/compiler.mjs.map +1 -0
  6. package/dist/index.d.mts +542 -0
  7. package/dist/index.mjs +128 -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-BdBxr5NQ.mjs +3222 -0
  12. package/dist/plugin-BdBxr5NQ.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-CgbV9LjD.d.mts +234 -0
  17. package/package.json +95 -4
  18. package/src/compiler/analyze.ts +608 -0
  19. package/src/compiler/builder.ts +1640 -0
  20. package/src/compiler/constants.ts +74 -0
  21. package/src/compiler/data.ts +73 -0
  22. package/src/compiler/defaults.ts +92 -0
  23. package/src/compiler/discriminators.ts +53 -0
  24. package/src/compiler/docs.ts +55 -0
  25. package/src/compiler/generate.ts +405 -0
  26. package/src/compiler/ids.ts +128 -0
  27. package/src/compiler/index.ts +93 -0
  28. package/src/compiler/locate.ts +44 -0
  29. package/src/compiler/model.ts +122 -0
  30. package/src/compiler/options.ts +54 -0
  31. package/src/compiler/scalars.ts +138 -0
  32. package/src/compiler/sdl-view.ts +59 -0
  33. package/src/context.ts +181 -0
  34. package/src/decisions.ts +169 -0
  35. package/src/decorators.ts +196 -0
  36. package/src/errors.ts +89 -0
  37. package/src/execution.ts +1064 -0
  38. package/src/format.ts +285 -0
  39. package/src/index.ts +84 -0
  40. package/src/limits.ts +177 -0
  41. package/src/loaders.ts +149 -0
  42. package/src/memory-pubsub.ts +133 -0
  43. package/src/metadata.ts +195 -0
  44. package/src/names.ts +47 -0
  45. package/src/options.ts +309 -0
  46. package/src/pipeline.ts +277 -0
  47. package/src/plugin.ts +557 -0
  48. package/src/pubsub.ts +36 -0
  49. package/src/scalars.ts +131 -0
  50. package/src/schema-hash.ts +18 -0
  51. package/src/schema.ts +557 -0
  52. package/src/sdl.ts +515 -0
  53. package/src/testing.ts +454 -0
  54. package/src/tokens.ts +22 -0
  55. package/src/transport/abort.ts +80 -0
  56. package/src/transport/accept.ts +155 -0
  57. package/src/transport/graphiql.ts +106 -0
  58. package/src/transport/http.ts +373 -0
  59. package/src/transport/sse.ts +45 -0
  60. package/src/type-ref.ts +89 -0
  61. package/src/types.ts +190 -0
  62. package/src/validate-args.ts +48 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Constants of the compiler extension modules: diagnostic codes, shape options, ordering. This
3
+ * module must stay free of runtime imports (the compiler loads the extension entry, which must
4
+ * never load the GraphQL runtime, graphql-js nor dataloader).
5
+ */
6
+ import type { ErrorCode } from '@znight/core'
7
+
8
+ import { GraphQLIds } from './ids.ts'
9
+
10
+ /** Diagnostic codes the extension reports (RFC 0005 §6.6 and the reused framework codes). */
11
+ export const Codes: {
12
+ /** ZNIGHT005: decorator target, arguments, method shape, look-alikes, `@Resolver` + `@Controller`. */
13
+ readonly InvalidDecorator: ErrorCode
14
+ /** ZNIGHT023: a root field without a decision of a required route decorator group (RFC 0001). */
15
+ readonly MissingRouteDecision: ErrorCode
16
+ /** ZNIGHT060: a TypeScript type with no GraphQL mapping, a missing annotation. */
17
+ readonly InvalidType: ErrorCode
18
+ /** ZNIGHT061: name conflicts, invalid or reserved names, field resolver type mismatches. */
19
+ readonly NameConflict: ErrorCode
20
+ /** ZNIGHT062: resolver structure (parent, subscriptions, no query, inheritance, libraries). */
21
+ readonly InvalidResolver: ErrorCode
22
+ /** ZNIGHT063: input position problems (dual use, getters, unions, cycles, defaults). */
23
+ readonly InvalidInput: ErrorCode
24
+ /** ZNIGHT064: union members that cannot be told apart at runtime. */
25
+ readonly UnresolvableAbstractType: ErrorCode
26
+ /** ZNIGHT065: `@Loader()` parameters and loader providers. */
27
+ readonly InvalidLoader: ErrorCode
28
+ } = Object.freeze({
29
+ InvalidDecorator: 'ZNIGHT005',
30
+ MissingRouteDecision: 'ZNIGHT023',
31
+ InvalidType: 'ZNIGHT060',
32
+ NameConflict: 'ZNIGHT061',
33
+ InvalidResolver: 'ZNIGHT062',
34
+ InvalidInput: 'ZNIGHT063',
35
+ UnresolvableAbstractType: 'ZNIGHT064',
36
+ InvalidLoader: 'ZNIGHT065',
37
+ })
38
+
39
+ /** Root reason given to `AnalyzeContext.addRoot()`. */
40
+ export const ROOT_REASON = 'GraphQL resolver'
41
+
42
+ /** Async iterables: the return type of `@Subscription` methods (§6.2). */
43
+ export const ASYNC_ITERABLES: ReadonlySet<string> = new Set([
44
+ 'globalThis#AsyncIterable',
45
+ 'globalThis#AsyncIterableIterator',
46
+ 'globalThis#AsyncGenerator',
47
+ ])
48
+
49
+ /** Iterables mapped to lists in output position (§6.2). */
50
+ export const OUTPUT_ITERABLES: ReadonlySet<string> = new Set([
51
+ 'globalThis#Set',
52
+ 'globalThis#ReadonlySet',
53
+ 'globalThis#Iterable',
54
+ ])
55
+
56
+ /**
57
+ * Identities the shapes keep as references (`ShapeOptions.keep`), before the identities of the
58
+ * `scalar<T>()` declarations of the application.
59
+ */
60
+ export const BASE_KEEP: readonly string[] = Object.freeze([
61
+ ...ASYNC_ITERABLES,
62
+ ...OUTPUT_ITERABLES,
63
+ GraphQLIds.ID,
64
+ GraphQLIds.Int,
65
+ GraphQLIds.Float,
66
+ GraphQLIds.JSONValue,
67
+ GraphQLIds.Loader,
68
+ ])
69
+
70
+ /** Deterministic string comparison (code-unit order, never `localeCompare`). */
71
+ export function compareStrings(a: string, b: string): number {
72
+ if (a === b) return 0
73
+ return a < b ? -1 : 1
74
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Data stored by the extension during analysis (`ctx.store()`), read back by `validate` and
3
+ * `generate`. Only what compiled libraries can encode: primitives, plain objects and arrays,
4
+ * `ClassInfo` (kept for its identity), `ValueRef`, `BindingHandle` and `Location`. Methods and
5
+ * parameters are looked up again by name and index (`ctx.classInfo(host.identity)`).
6
+ */
7
+
8
+ import type { BindingHandle, ClassInfo, ClassRef, Location, ValueRef } from '@znight/compiler'
9
+ import type { Scope } from '@znight/core'
10
+ import type { MetaEntry } from '@znight/http/compiler'
11
+
12
+ /** Kind of field a resolver method provides. */
13
+ export type FieldOperation = 'query' | 'mutation' | 'subscription' | 'field'
14
+
15
+ /** A `@Resolver` class. */
16
+ export interface ResolverData {
17
+ readonly kind: 'resolver'
18
+ /** The resolver class; its identity is the provider token. */
19
+ readonly host: ClassInfo
20
+ readonly scope: Scope
21
+ /** Location of `@Resolver`. */
22
+ readonly location: Location
23
+ /** Class `@UseGuard` guards. */
24
+ readonly guards: readonly ClassRef[]
25
+ /** One entry per field decorator of the class's methods, in source order. */
26
+ readonly fields: readonly FieldData[]
27
+ }
28
+
29
+ /** One `@Query`, `@Mutation`, `@Subscription` or `@ResolveField` decorator. */
30
+ export interface FieldData {
31
+ /** Method name. */
32
+ readonly method: string
33
+ readonly operation: FieldOperation
34
+ /** GraphQL field name (decorator argument, or the method name). */
35
+ readonly name: string
36
+ /** Location of the field decorator. */
37
+ readonly location: Location
38
+ /** Location of the method name. */
39
+ readonly methodLocation: Location
40
+ /** Method `@UseGuard` guards (class guards are on {@link ResolverData.guards}). */
41
+ readonly guards: readonly ClassRef[]
42
+ /** Effective route metadata: the class entries a method decision does not replace, then the method's. */
43
+ readonly meta: readonly MetaEntry[]
44
+ /** One entry per method parameter, by index (`undefined`: the parameter could not be analysed). */
45
+ readonly params: readonly (ParamData | undefined)[]
46
+ /** `false` when a parameter or the method could not be analysed (an error was reported). */
47
+ readonly complete: boolean
48
+ }
49
+
50
+ /** What a method parameter receives (§7.3). */
51
+ export type ParamData =
52
+ | {
53
+ readonly kind: 'args'
54
+ /** `@Args('name')`: one argument; `undefined`: the properties of the type are the arguments. */
55
+ readonly name: string | undefined
56
+ /** `@Args(..., Schema)`: the Standard Schema validating the value. */
57
+ readonly schema: ValueRef | undefined
58
+ readonly location: Location
59
+ }
60
+ | { readonly kind: 'parent'; readonly location: Location }
61
+ | { readonly kind: 'ctx' }
62
+ | { readonly kind: 'info' }
63
+ | {
64
+ readonly kind: 'loader'
65
+ /** Binding of the loader provider (lazy edge from the resolver). */
66
+ readonly binding: BindingHandle
67
+ /** Identity of `L` in `Loader<L>`. */
68
+ readonly identity: string
69
+ readonly location: Location
70
+ }
71
+
72
+ /** Everything the extension stores. */
73
+ export type GraphQLData = ResolverData
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Default values of arguments and input fields (RFC 0005 §6.3): an internal value (a `@default`
3
+ * JSON value, a statically evaluated parameter default) checked against its GraphQL type and turned
4
+ * into the external value graphql-js expects (`default: { value }`, enum values by name).
5
+ */
6
+ import type { GraphQLTypeRef } from '../type-ref.ts'
7
+ import type { TypeModel } from './model.ts'
8
+
9
+ const GRAPHQL_INT_MAX = 2_147_483_647
10
+ const GRAPHQL_INT_MIN = -2_147_483_648
11
+
12
+ /** The external value, or why the value does not fit the type. */
13
+ export type ExternalValue = { readonly value: unknown } | { readonly error: string }
14
+
15
+ /** `value` (an internal value) as a GraphQL external value of `ref`; `types` resolves enums and inputs. */
16
+ export function externalValue(
17
+ value: unknown,
18
+ ref: GraphQLTypeRef,
19
+ types: ReadonlyMap<string, TypeModel>,
20
+ ): ExternalValue {
21
+ if (value === null) return ref.nonNull ? { error: 'null for a non-null type' } : { value: null }
22
+ if (value === undefined) return { error: 'undefined' }
23
+ if (ref.kind === 'list') {
24
+ if (!Array.isArray(value)) return externalValue(value, ref.of, types)
25
+ const items: unknown[] = []
26
+ for (const item of value) {
27
+ const converted = externalValue(item, ref.of, types)
28
+ if ('error' in converted) return converted
29
+ items.push(converted.value)
30
+ }
31
+ return { value: items }
32
+ }
33
+ switch (ref.name) {
34
+ case 'String':
35
+ return typeof value === 'string' ? { value } : { error: 'expected a string' }
36
+ case 'ID':
37
+ return typeof value === 'string' || (typeof value === 'number' && Number.isInteger(value))
38
+ ? { value }
39
+ : { error: 'expected a string or an integer' }
40
+ case 'Int':
41
+ return typeof value === 'number' &&
42
+ Number.isInteger(value) &&
43
+ value <= GRAPHQL_INT_MAX &&
44
+ value >= GRAPHQL_INT_MIN
45
+ ? { value }
46
+ : { error: 'expected a 32-bit integer' }
47
+ case 'Float':
48
+ return typeof value === 'number' && Number.isFinite(value) ? { value } : { error: 'expected a finite number' }
49
+ case 'Boolean':
50
+ return typeof value === 'boolean' ? { value } : { error: 'expected a boolean' }
51
+ case 'JSON':
52
+ return isJson(value) ? { value } : { error: 'expected a JSON value' }
53
+ }
54
+ const type = types.get(ref.name)
55
+ if (type?.kind === 'enum') {
56
+ const found =
57
+ type.values.find(entry => entry.value === value) ??
58
+ (typeof value === 'string' ? type.values.find(entry => entry.name === value) : undefined)
59
+ return found === undefined
60
+ ? { error: `expected one of ${type.values.map(entry => entry.name).join(', ')}` }
61
+ : { value: found.name }
62
+ }
63
+ if (type?.kind === 'input') {
64
+ if (typeof value !== 'object' || Array.isArray(value)) return { error: `expected an object of type ${type.name}` }
65
+ const record = value as Readonly<Record<string, unknown>>
66
+ for (const key of Object.keys(record)) {
67
+ if (!type.fields.some(field => field.name === key)) return { error: `${type.name} has no field '${key}'` }
68
+ }
69
+ const result: Record<string, unknown> = {}
70
+ for (const field of type.fields) {
71
+ if (!Object.hasOwn(record, field.name) || record[field.name] === undefined) {
72
+ if (field.ref.nonNull && field.defaultValue === undefined)
73
+ return { error: `${type.name}.${field.name} is required` }
74
+ continue
75
+ }
76
+ const converted = externalValue(record[field.name], field.ref, types)
77
+ if ('error' in converted) return { error: `${type.name}.${field.name}: ${converted.error}` }
78
+ result[field.name] = converted.value
79
+ }
80
+ return { value: result }
81
+ }
82
+ return { error: `defaults of the scalar ${ref.name} are not supported` }
83
+ }
84
+
85
+ /** A JSON-compatible value (the `JSON` scalar). */
86
+ function isJson(value: unknown): boolean {
87
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return true
88
+ if (typeof value === 'number') return Number.isFinite(value)
89
+ if (Array.isArray(value)) return value.every(isJson)
90
+ if (typeof value !== 'object') return false
91
+ return Object.values(value).every(isJson)
92
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Union discriminators (RFC 0005 §5.7): how the runtime tells the members of a union apart, decided
3
+ * at build time so that `resolveType` is a lookup.
4
+ */
5
+ import type { ClassInfo } from '@znight/compiler'
6
+ import type { DiscriminatorModel, ObjectModel } from './model.ts'
7
+
8
+ import { compareStrings } from './constants.ts'
9
+
10
+ /**
11
+ * `instanceof` when every member is an exported class (subclasses first), `__typename` when every
12
+ * member declares it, else the first property of the first member whose literal value differs in
13
+ * every member; `undefined` when none applies (ZNIGHT064).
14
+ */
15
+ export function discriminatorOf(
16
+ members: readonly ObjectModel[],
17
+ classInfo: (identity: string) => ClassInfo | undefined,
18
+ ): DiscriminatorModel | undefined {
19
+ if (members.every(member => member.classIdentity !== undefined && classInfo(member.classIdentity)?.exported)) {
20
+ const depth = (identity: string): number => classInfo(identity)?.heritage.length ?? 0
21
+ return {
22
+ by: 'class',
23
+ classes: members
24
+ .map(member => ({ identity: member.classIdentity as string, name: member.name }))
25
+ // subclasses first: `instanceof` of a base class also matches its subclasses
26
+ .sort((a, b) => depth(b.identity) - depth(a.identity) || compareStrings(a.name, b.name)),
27
+ }
28
+ }
29
+ if (members.every(member => member.properties.some(property => property.name === '__typename'))) {
30
+ return { by: 'typename' }
31
+ }
32
+ for (const candidate of members[0]?.properties ?? []) {
33
+ if (candidate.optional || candidate.literal === undefined) continue
34
+ const values = literalValues(members, candidate.name)
35
+ if (values !== undefined) return { by: 'property', property: candidate.name, values }
36
+ }
37
+ return undefined
38
+ }
39
+
40
+ /** Literal value → member name when `property` is a required literal with a distinct value in every member. */
41
+ function literalValues(members: readonly ObjectModel[], property: string): Record<string, string> | undefined {
42
+ const values = new Map<string, string>()
43
+ for (const member of members) {
44
+ const found = member.properties.find(entry => entry.name === property)
45
+ if (found === undefined || found.optional || found.literal === undefined) return undefined
46
+ const key = String(found.literal)
47
+ if (values.has(key)) return undefined
48
+ values.set(key, member.name)
49
+ }
50
+ const sorted: Record<string, string> = {}
51
+ for (const key of [...values.keys()].sort(compareStrings)) sorted[key] = values.get(key) as string
52
+ return sorted
53
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The JSDoc vocabulary of the extension (RFC 0005 §5.14): descriptions, `@param`, `@deprecated`,
3
+ * `@default`, `@gqlHidden` and `@gqlName`.
4
+ */
5
+ import type { DocComment, DocTag } from '@znight/compiler'
6
+
7
+ import { DEFAULT_DEPRECATION_REASON } from '../names.ts'
8
+
9
+ /** The comment text (before the first tag), `undefined` when empty. */
10
+ export function descriptionOf(doc: DocComment | undefined): string | undefined {
11
+ return doc === undefined || doc.text === '' ? undefined : doc.text
12
+ }
13
+
14
+ /** The first tag named `name`. */
15
+ export function tagOf(doc: DocComment | undefined, name: string): DocTag | undefined {
16
+ return doc?.tags.find(tag => tag.name === name)
17
+ }
18
+
19
+ /** `@deprecated reason`: the reason (graphql-js' default one without text), `undefined` without the tag. */
20
+ export function deprecationOf(doc: DocComment | undefined): string | undefined {
21
+ const tag = tagOf(doc, 'deprecated')
22
+ if (tag === undefined) return undefined
23
+ const reason = tag.text.trim()
24
+ return reason === '' ? DEFAULT_DEPRECATION_REASON : reason
25
+ }
26
+
27
+ /** `true` with a `@gqlHidden` tag. */
28
+ export function isHidden(doc: DocComment | undefined): boolean {
29
+ return tagOf(doc, 'gqlHidden') !== undefined
30
+ }
31
+
32
+ /**
33
+ * The text of `@param name text` (also `@param {Type} name - text`), `undefined` when there is no
34
+ * such tag or it has no text.
35
+ */
36
+ export function paramDescription(doc: DocComment | undefined, name: string): string | undefined {
37
+ for (const tag of doc?.tags ?? []) {
38
+ if (tag.name !== 'param') continue
39
+ const match = /^(?:\{[^}]*\}\s*)?\[?([\w$]+)(?:=[^\]]*)?\]?(?:\s+-)?(?:\s+([\s\S]*))?$/.exec(tag.text.trim())
40
+ if (match?.[1] !== name) continue
41
+ const text = match[2]?.trim()
42
+ return text === undefined || text === '' ? undefined : text
43
+ }
44
+ return undefined
45
+ }
46
+
47
+ /** A `@default` value: JSON when it parses, the text otherwise (`@default title`). */
48
+ export function defaultTagValue(tag: DocTag): unknown {
49
+ const text = tag.text.trim()
50
+ try {
51
+ return JSON.parse(text) as unknown
52
+ } catch {
53
+ return text
54
+ }
55
+ }