@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
package/src/context.ts ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * The implementation of `GraphQLContext` (RFC 0005 §5.8): one per operation, passed to graphql-js
3
+ * as `contextValue` and stored in the request scope (`@znight/graphql#GraphQLContext`).
4
+ *
5
+ * Everything that is not needed by every operation is computed on first read: the request id (a
6
+ * UUID for direct executions), the HTTP context of an enclosing request, the signal, the state map
7
+ * and the loader registry.
8
+ */
9
+ import type { Container, ScopeContext, Token } from '@znight/core'
10
+ import type { HttpContext } from '@znight/http'
11
+ import type { DocumentNode, OperationDefinitionNode } from 'graphql'
12
+ import type { LoaderFactory } from './loaders.ts'
13
+ import type {
14
+ BatchLoad,
15
+ BatchLoadClass,
16
+ BatchLoader,
17
+ GraphQLContext,
18
+ GraphQLOperationKind,
19
+ GraphQLTransport,
20
+ LoaderKey,
21
+ LoaderValue,
22
+ } from './types.ts'
23
+
24
+ import { randomUUID } from 'node:crypto'
25
+ import { ErrorCode, ZnightError } from '@znight/core'
26
+ import { HttpTokens } from '@znight/http'
27
+
28
+ /** What every operation context of a plugin shares. */
29
+ export interface ContextRuntime {
30
+ readonly container: Container
31
+ readonly loaders: LoaderFactory
32
+ /** The plugin's own signal (aborted by `onStop`): the signal of direct executions. */
33
+ readonly signal: AbortSignal
34
+ }
35
+
36
+ /** What a request carries that the context exposes (a `GraphQLRequest`). */
37
+ export interface ContextRequest {
38
+ readonly variables?: Readonly<Record<string, unknown>> | null | undefined
39
+ }
40
+
41
+ /** How the request reached the executor (`ExecuteOptions`). */
42
+ export interface ContextOptions {
43
+ readonly transport?: GraphQLTransport
44
+ readonly http?: HttpContext
45
+ readonly signal?: AbortSignal
46
+ readonly requestId?: string
47
+ }
48
+
49
+ const NO_VARIABLES: Readonly<Record<string, unknown>> = Object.freeze({})
50
+
51
+ /**
52
+ * The `GraphQLContext` of one operation. Built from references the executor already holds (the
53
+ * operation node, the request, the options, the enclosing scope): one allocation per operation,
54
+ * every field read on demand.
55
+ */
56
+ export class OperationContext implements GraphQLContext {
57
+ readonly document: DocumentNode
58
+ readonly #runtime: ContextRuntime
59
+ readonly #operation: OperationDefinitionNode
60
+ readonly #request: ContextRequest
61
+ readonly #options: ContextOptions
62
+ readonly #scope: ScopeContext | undefined
63
+ #requestId: string | undefined
64
+ #http: HttpContext | null | undefined
65
+ #signal: AbortSignal | undefined
66
+ #ended: AbortSignal | undefined
67
+ #state: Map<string | symbol, unknown> | undefined
68
+ #loaders: Map<Token, BatchLoader<unknown, unknown>> | undefined
69
+
70
+ constructor(
71
+ runtime: ContextRuntime,
72
+ document: DocumentNode,
73
+ operation: OperationDefinitionNode,
74
+ request: ContextRequest,
75
+ options: ContextOptions,
76
+ scope: ScopeContext | undefined,
77
+ ) {
78
+ this.#runtime = runtime
79
+ this.document = document
80
+ this.#operation = operation
81
+ this.#request = request
82
+ this.#options = options
83
+ this.#scope = scope
84
+ this.#requestId = options.requestId
85
+ }
86
+
87
+ get transport(): GraphQLTransport {
88
+ return this.#options.transport ?? 'direct'
89
+ }
90
+
91
+ get operationType(): GraphQLOperationKind {
92
+ return this.#operation.operation
93
+ }
94
+
95
+ get operationName(): string | undefined {
96
+ return this.#operation.name?.value
97
+ }
98
+
99
+ get requestId(): string {
100
+ let id = this.#requestId
101
+ if (id === undefined) {
102
+ const http = this.http
103
+ const scoped = http === undefined ? this.#scope?.get(HttpTokens.RequestId) : undefined
104
+ id = http?.requestId ?? (typeof scoped === 'string' ? scoped : randomUUID())
105
+ this.#requestId = id
106
+ }
107
+ return id
108
+ }
109
+
110
+ get http(): HttpContext | undefined {
111
+ let http = this.#http
112
+ if (http === undefined) {
113
+ // a controller calling plugin.execute(): the HTTP request of the enclosing scope
114
+ http = this.#options.http ?? this.#scope?.get(HttpTokens.Context) ?? null
115
+ this.#http = http
116
+ }
117
+ return http ?? undefined
118
+ }
119
+
120
+ get variables(): Readonly<Record<string, unknown>> {
121
+ return this.#request.variables ?? NO_VARIABLES
122
+ }
123
+
124
+ get signal(): AbortSignal {
125
+ let signal = this.#signal
126
+ if (signal === undefined) {
127
+ // HttpContext.signal is created on first read: only read it when someone asks
128
+ const base = this.#options.signal ?? this.http?.signal ?? this.#runtime.signal
129
+ const ended = this.#ended
130
+ signal = ended === undefined ? base : AbortSignal.any([base, ended])
131
+ this.#signal = signal
132
+ }
133
+ return signal
134
+ }
135
+
136
+ /**
137
+ * Ties {@link signal} to the end of a subscription: it also aborts when `ended` does (the stream
138
+ * ended by itself or with `return()`). Called before any resolver runs.
139
+ */
140
+ endsWith(ended: AbortSignal): void {
141
+ this.#ended = ended
142
+ }
143
+
144
+ get state(): Map<string | symbol, unknown> {
145
+ this.#state ??= new Map()
146
+ return this.#state
147
+ }
148
+
149
+ loader<L extends BatchLoad<unknown, unknown>>(loader: BatchLoadClass<L>): BatchLoader<LoaderKey<L>, LoaderValue<L>>
150
+ loader(token: Token): BatchLoader<unknown, unknown>
151
+ loader(key: Token | BatchLoadClass): BatchLoader<unknown, unknown> {
152
+ const token = typeof key === 'string' ? key : this.#tokenOf(key)
153
+ let loaders = this.#loaders
154
+ if (loaders === undefined) {
155
+ loaders = new Map()
156
+ this.#loaders = loaders
157
+ }
158
+ let loader = loaders.get(token)
159
+ if (loader === undefined) {
160
+ loader = this.#runtime.loaders.create(token)
161
+ loaders.set(token, loader)
162
+ }
163
+ return loader
164
+ }
165
+
166
+ /** Starts a new execution (a subscription event): loaders and their caches are renewed. */
167
+ resetLoaders(): void {
168
+ this.#loaders = undefined
169
+ }
170
+
171
+ #tokenOf(loader: BatchLoadClass): Token {
172
+ const token = typeof loader === 'function' ? this.#runtime.container.tokenOf(loader) : undefined
173
+ if (token === undefined) {
174
+ const name = typeof loader === 'function' ? loader.name || '<anonymous>' : String(loader)
175
+ throw new ZnightError(ErrorCode.MissingProvider, `GraphQLContext.loader(): ${name} is not a provider`, {
176
+ hints: ['Add @Injectable() to the loader class (it implements BatchLoad<K, V>)'],
177
+ })
178
+ }
179
+ return token
180
+ }
181
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The boot check of route decisions on GraphQL fields (RFC 0001 §5.2, RFC 0005 §5.9): every root
3
+ * field decides every required group, and every decision of a required or enforced group is
4
+ * enforced by a guard of the field's chain (`static enforces`). Problems are one ZNIGHT023 error
5
+ * (`enforcement: 'error'`) or one warning per group.
6
+ *
7
+ * A small re-implementation of `@znight/http`'s internal checker for fields instead of routes.
8
+ */
9
+ import type { CoreLogger } from '@znight/core'
10
+ import type { HttpRouteGroupDefinition, RouteDecoratorInfo } from '@znight/http'
11
+
12
+ import { ErrorCode, ZnightError } from '@znight/core'
13
+
14
+ /** A resolver-backed field as seen by the check. */
15
+ export interface CheckedField {
16
+ /** `Query.books (BookResolver.books)`. */
17
+ readonly description: string
18
+ /** Root fields must decide every required group. */
19
+ readonly root: boolean
20
+ /** Effective metadata. */
21
+ readonly meta: Readonly<Record<string, unknown>>
22
+ /** Guard classes of its chain (global when they apply, class, method); instances give their constructor. */
23
+ readonly guards: readonly { readonly cls: unknown; readonly name: string }[]
24
+ }
25
+
26
+ interface GroupInfo {
27
+ readonly group: string
28
+ readonly required: boolean
29
+ }
30
+
31
+ /** The groups listed by `static enforces` of a guard class (ZNIGHT005 when malformed). */
32
+ export function enforcedBy(guard: unknown, name: string): readonly RouteDecoratorInfo[] {
33
+ if (typeof guard !== 'function') return []
34
+ let enforces: unknown
35
+ try {
36
+ enforces = (guard as { enforces?: unknown }).enforces
37
+ } catch (error) {
38
+ throw invalidEnforces(name, 'reading it threw', error)
39
+ }
40
+ if (enforces === undefined) return []
41
+ if (!Array.isArray(enforces)) throw invalidEnforces(name, 'expected an array of route decorators')
42
+ for (const entry of enforces as unknown[]) {
43
+ const info = (typeof entry === 'function' || (typeof entry === 'object' && entry !== null) ? entry : {}) as {
44
+ key?: unknown
45
+ group?: unknown
46
+ required?: unknown
47
+ }
48
+ if (typeof info.key !== 'string' || typeof info.group !== 'string' || typeof info.required !== 'boolean') {
49
+ throw invalidEnforces(name, 'every entry must be a route decorator ({ key, group, required })')
50
+ }
51
+ }
52
+ return enforces as readonly RouteDecoratorInfo[]
53
+ }
54
+
55
+ function invalidEnforces(name: string, reason: string, cause?: unknown): ZnightError {
56
+ return new ZnightError(
57
+ ErrorCode.InvalidDecorator,
58
+ `GraphQLPlugin: invalid static enforces of the guard ${name}: ${reason}`,
59
+ {
60
+ hints: [
61
+ 'static readonly enforces = [Public, Authenticated, Roles] (decorators created with createRouteDecorator)',
62
+ ],
63
+ ...(cause === undefined ? {} : { cause }),
64
+ },
65
+ )
66
+ }
67
+
68
+ /** Runs the check. `groups`: `metadata.extensions.graphql.groups`. */
69
+ export function checkFieldDecisions(
70
+ fields: readonly CheckedField[],
71
+ groups: Readonly<Record<string, HttpRouteGroupDefinition>> | undefined,
72
+ mode: 'error' | 'warn',
73
+ logger: CoreLogger,
74
+ ): void {
75
+ const keys = new Map<string, GroupInfo>()
76
+ const required = new Set<string>()
77
+ for (const [group, definition] of Object.entries(groups ?? {})) {
78
+ for (const key of definition.keys) keys.set(key, { group, required: definition.required })
79
+ if (definition.required) required.add(group)
80
+ }
81
+ const enforcedOf = new Map<unknown, ReadonlySet<string>>()
82
+ const enforced = (guard: { readonly cls: unknown; readonly name: string }): ReadonlySet<string> => {
83
+ let groupsOf = enforcedOf.get(guard.cls)
84
+ if (groupsOf === undefined) {
85
+ const infos = enforcedBy(guard.cls, guard.name)
86
+ for (const info of infos) {
87
+ if (!keys.has(info.key)) keys.set(info.key, { group: info.group, required: info.required })
88
+ if (info.required) required.add(info.group)
89
+ }
90
+ groupsOf = new Set(infos.map(info => info.group))
91
+ enforcedOf.set(guard.cls, groupsOf)
92
+ }
93
+ return groupsOf
94
+ }
95
+ const chains = fields.map(field => {
96
+ const set = new Set<string>()
97
+ for (const guard of field.guards) for (const group of enforced(guard)) set.add(group)
98
+ return set
99
+ })
100
+ const known = new Set<string>()
101
+ for (const chain of chains) for (const group of chain) known.add(group)
102
+ const groupOf = (key: string): string => keys.get(key)?.group ?? key
103
+
104
+ const problems = new Map<string, { missing: string[]; unenforced: string[] }>()
105
+ const problem = (group: string): { missing: string[]; unenforced: string[] } => {
106
+ let entry = problems.get(group)
107
+ if (entry === undefined) {
108
+ entry = { missing: [], unenforced: [] }
109
+ problems.set(group, entry)
110
+ }
111
+ return entry
112
+ }
113
+ fields.forEach((field, index) => {
114
+ const decided = new Set(Object.keys(field.meta).map(groupOf))
115
+ if (field.root) {
116
+ for (const group of required) if (!decided.has(group)) problem(group).missing.push(field.description)
117
+ }
118
+ const chain = chains[index] as Set<string>
119
+ for (const group of decided) {
120
+ if ((required.has(group) || known.has(group)) && !chain.has(group))
121
+ problem(group).unenforced.push(field.description)
122
+ }
123
+ })
124
+ if (problems.size === 0) return
125
+ const messages: string[] = []
126
+ const hints: string[] = []
127
+ for (const group of [...problems.keys()].sort()) {
128
+ const entry = problems.get(group) as { missing: string[]; unenforced: string[] }
129
+ const members = [...keys]
130
+ .filter(([, info]) => info.group === group)
131
+ .map(([key]) => key)
132
+ .sort()
133
+ const parts: string[] = []
134
+ if (entry.missing.length > 0) {
135
+ parts.push(
136
+ `${count(entry.missing.length)} ${entry.missing.length === 1 ? 'has' : 'have'} no "${group}" decision: ${entry.missing.join(', ')}`,
137
+ )
138
+ hints.push(
139
+ `Add a decision of the "${group}" group (${members.join(', ') || group}) to the resolver method or to its class`,
140
+ )
141
+ }
142
+ if (entry.unenforced.length > 0) {
143
+ parts.push(
144
+ `no guard enforces the "${group}" group on ${count(entry.unenforced.length)}: ${entry.unenforced.join(', ')}`,
145
+ )
146
+ hints.push(
147
+ `Register a GraphQL guard declaring \`static enforces\` with the decorators of the "${group}" group: GraphQLPlugin.configure({ guards: [...] }) or @UseGuard(...)`,
148
+ )
149
+ }
150
+ const message = parts.join('; ')
151
+ if (mode === 'warn') logger.warn(`GraphQLPlugin: ${message}`, { code: ErrorCode.MissingRouteDecision, group })
152
+ else messages.push(message)
153
+ }
154
+ if (mode === 'error') {
155
+ hints.push("Tests that deliberately run without guards can set enforcement: 'warn'")
156
+ const text = messages.join('; ')
157
+ throw new ZnightError(
158
+ ErrorCode.MissingRouteDecision,
159
+ `GraphQLPlugin: ${text.charAt(0).toUpperCase()}${text.slice(1)}`,
160
+ {
161
+ hints,
162
+ },
163
+ )
164
+ }
165
+ }
166
+
167
+ function count(fields: number): string {
168
+ return fields === 1 ? '1 field' : `${fields} fields`
169
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Marker decorators of `@znight/graphql` (RFC 0005 §5.14). Like every Znight decorator they do
3
+ * nothing at runtime: the compiler extension (`@znight/graphql/compiler`) reads them from the source
4
+ * code, derives the schema from the TypeScript types they reach and generates
5
+ * `metadata.extensions.graphql`. Arguments marked _static_ must be statically evaluable (literals,
6
+ * `const` references); _value_ arguments may also be references to exported bindings (schemas,
7
+ * guard classes). See docs/internals/architecture.md §4.6.
8
+ *
9
+ * Also here: `scalar<T>()`, the scalar aliases (`ID`, `Int`, `Float`, `JSONValue`) and `Loader<L>`,
10
+ * which shares its name with the `@Loader()` decorator (one identity, `@znight/graphql#Loader`, for
11
+ * the parameter decorator and the parameter type).
12
+ */
13
+ import type { ClassMarker, MethodMarker, ParameterMarker, Scope } from '@znight/core'
14
+ import type { StandardSchemaV1 } from '@znight/http'
15
+ import type { GraphQLScalarType } from 'graphql'
16
+ import type { BatchLoad, BatchLoader, GraphQLGuardClass, LoaderKey, LoaderValue, ScalarDefinition } from './types.ts'
17
+
18
+ function marker(): void {
19
+ // intentionally empty: decorators are compile-time markers
20
+ }
21
+
22
+ const classMarker: ClassMarker = marker
23
+ const methodMarker: MethodMarker = marker
24
+ const parameterMarker: ParameterMarker = marker
25
+ const classOrMethodMarker: ClassMarker & MethodMarker = marker
26
+
27
+ /** GraphQL `ID`: a string on the wire and in TypeScript. Recognised by the compiler through the alias. */
28
+ export type ID = string
29
+
30
+ /** GraphQL `Int` (32-bit signed integer). A `number` in TypeScript; plain `number` maps to `Float`. */
31
+ export type Int = number
32
+
33
+ /** GraphQL `Float`. Same as `number`, for symmetry with {@link Int}. */
34
+ export type Float = number
35
+
36
+ /** Any JSON value: the `JSON` scalar. */
37
+ export type JSONValue = string | number | boolean | null | readonly JSONValue[] | { readonly [key: string]: JSONValue }
38
+
39
+ /**
40
+ * The DataLoader of the current execution for the loader provider `L`, injected with `@Loader()`:
41
+ * created on first use, cached for the operation (or for one subscription event), then dropped.
42
+ *
43
+ * ```ts
44
+ * @ResolveField()
45
+ * author(@Parent() book: Book, @Loader() authors: Loader<AuthorsById>): Promise<Author> {
46
+ * return authors.load(book.authorId)
47
+ * }
48
+ * ```
49
+ */
50
+ export type Loader<L extends BatchLoad<unknown, unknown>> = BatchLoader<LoaderKey<L>, LoaderValue<L>>
51
+
52
+ /** Options of {@link Resolver} (static). */
53
+ export interface ResolverOptions {
54
+ /** Provider scope. Default: `singleton`; `request` creates one resolver per operation. */
55
+ readonly scope?: Scope
56
+ }
57
+
58
+ /**
59
+ * Declares a resolver: a provider (singleton by default) whose `@Query`, `@Mutation`,
60
+ * `@Subscription` and `@ResolveField` methods are fields of the schema. Route decorators of
61
+ * `@znight/http` (`createRouteDecorator`) on the class are the default decisions of its fields.
62
+ *
63
+ * Schema-first (RFC 0007): `@Resolver('Event')` names the SDL object type its `@ResolveField`
64
+ * methods resolve fields of (`@Resolver('Event', { scope })` with options). Code-first rejects a
65
+ * type name (ZNIGHT005): the type is the one of the `@Parent()` parameter.
66
+ *
67
+ * ```ts
68
+ * @Resolver()
69
+ * export class BookResolver {
70
+ * constructor(private readonly catalog: CatalogService) {}
71
+ *
72
+ * @Query()
73
+ * book(@Args('id') id: ID): Promise<Book | null> {
74
+ * return this.catalog.find(id)
75
+ * }
76
+ * }
77
+ * ```
78
+ *
79
+ * @param typeOrOptions static: the SDL type name (schema-first), or `{ scope? }`.
80
+ * @param options static: `{ scope? }`, after a type name.
81
+ */
82
+ export function Resolver(_typeOrOptions?: string | ResolverOptions, _options?: ResolverOptions): ClassMarker {
83
+ return classMarker
84
+ }
85
+
86
+ /**
87
+ * A field of the `Query` root type. The method's declared return type is the field type
88
+ * (`Promise<T>` unwrapped), its `@Args` the arguments, its JSDoc the description.
89
+ *
90
+ * @param name static: field name. Default: the method name.
91
+ */
92
+ export function Query(_name?: string): MethodMarker {
93
+ return methodMarker
94
+ }
95
+
96
+ /**
97
+ * A field of the `Mutation` root type.
98
+ *
99
+ * @param name static: field name. Default: the method name.
100
+ */
101
+ export function Mutation(_name?: string): MethodMarker {
102
+ return methodMarker
103
+ }
104
+
105
+ /**
106
+ * A field of the `Subscription` root type. The method returns an `AsyncIterable<T>` (an async
107
+ * generator, or `pubsub.subscribe(...)`), each value being the field value of one event.
108
+ *
109
+ * @param name static: field name. Default: the method name.
110
+ */
111
+ export function Subscription(_name?: string): MethodMarker {
112
+ return methodMarker
113
+ }
114
+
115
+ /**
116
+ * A field resolver: adds a field to the type of the `@Parent()` parameter (mandatory), or replaces
117
+ * the property field of the same name. Schema-first (RFC 0007): resolves the field of the type named
118
+ * by `@Resolver('Type')`, `@Parent()` optional.
119
+ *
120
+ * @param name static: field name. Default: the method name.
121
+ */
122
+ export function ResolveField(_name?: string): MethodMarker {
123
+ return methodMarker
124
+ }
125
+
126
+ /**
127
+ * Field arguments. Without a name, the properties of the parameter type become arguments
128
+ * (`@Args() filter: BookFilter`); with a name, one argument (`@Args('id') id: ID`). With a Standard
129
+ * Schema, the value is validated (and transformed) after GraphQL coerced it; a failure is a
130
+ * `BAD_USER_INPUT` error listing the issues.
131
+ *
132
+ * @param name static: argument name.
133
+ * @param schema value: a Standard Schema validating the value (its output is injected).
134
+ */
135
+ export function Args(): ParameterMarker
136
+ export function Args(name: string, schema?: StandardSchemaV1): ParameterMarker
137
+ export function Args(schema: StandardSchemaV1): ParameterMarker
138
+ export function Args(_nameOrSchema?: string | StandardSchemaV1, _schema?: StandardSchemaV1): ParameterMarker {
139
+ return parameterMarker
140
+ }
141
+
142
+ /** The parent object of a field resolver (`@ResolveField` only; its type is the parent type). */
143
+ export function Parent(): ParameterMarker {
144
+ return parameterMarker
145
+ }
146
+
147
+ /** The `GraphQLContext` of the operation. */
148
+ export function Ctx(): ParameterMarker {
149
+ return parameterMarker
150
+ }
151
+
152
+ /** The graphql-js `GraphQLResolveInfo` of the call (escape hatch). */
153
+ export function Info(): ParameterMarker {
154
+ return parameterMarker
155
+ }
156
+
157
+ /** The DataLoader of the current execution for the `Loader<L>` parameter type (see {@link Loader}). */
158
+ export function Loader(): ParameterMarker {
159
+ return parameterMarker
160
+ }
161
+
162
+ /**
163
+ * Guards of the fields of a resolver class or of one field, run after the global guards (class
164
+ * guards, then method guards). Guards are providers implementing `GraphQLGuard`.
165
+ *
166
+ * @param guards value: guard classes.
167
+ */
168
+ export function UseGuard(..._guards: [GraphQLGuardClass, ...GraphQLGuardClass[]]): ClassMarker & MethodMarker {
169
+ return classOrMethodMarker
170
+ }
171
+
172
+ /**
173
+ * Maps the TypeScript type `T` to the GraphQL scalar `name` wherever it appears. `name` is static;
174
+ * the returned definition must be exported (`export const`): the generated module imports it.
175
+ * Returns `definition` unchanged.
176
+ *
177
+ * ```ts
178
+ * export type EmailAddress = string & { readonly __brand: 'EmailAddress' }
179
+ *
180
+ * export const EmailAddressScalar = scalar<EmailAddress>('EmailAddress', {
181
+ * serialize: value => value,
182
+ * parseValue: value => {
183
+ * if (typeof value !== 'string' || !value.includes('@')) throw new TypeError('Expected an e-mail address')
184
+ * return value.toLowerCase() as EmailAddress
185
+ * },
186
+ * })
187
+ *
188
+ * export const UrlScalar = scalar<URL>('URL', GraphQLURL) // a graphql-js GraphQLScalarType
189
+ * ```
190
+ *
191
+ * @param name static: the GraphQL name of the scalar.
192
+ * @param definition value: a {@link ScalarDefinition} or a graphql-js `GraphQLScalarType` (whose
193
+ * name must be `name`, ZNIGHT066 at boot otherwise).
194
+ */
195
+ export function scalar<T>(name: string, definition: ScalarDefinition<T>): ScalarDefinition<T>
196
+ export function scalar<T>(name: string, definition: GraphQLScalarType): GraphQLScalarType<T>
197
+ export function scalar<T>(
198
+ _name: string,
199
+ definition: ScalarDefinition<T> | GraphQLScalarType,
200
+ ): ScalarDefinition<T> | GraphQLScalarType {
201
+ return definition
202
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * GraphQL errors shown to clients (RFC 0005 §5.10, §8.8): `GraphQLException` and the
3
+ * `extensions.code` values the plugin produces (Apollo conventions, which clients already know).
4
+ *
5
+ * Import-free: the exception is a plain `Error` subclass, rendered by the runtime as
6
+ * `{ message, locations, path, extensions: { code, ...extensions } }`.
7
+ */
8
+
9
+ /** `extensions.code` values produced by `@znight/graphql`. Applications may use their own codes too. */
10
+ export const GraphQLErrorCode: {
11
+ /** The document does not parse (syntax error, `limits.maxTokens`). */
12
+ readonly ParseFailed: 'GRAPHQL_PARSE_FAILED'
13
+ /** The document is not valid against the schema. */
14
+ readonly ValidationFailed: 'GRAPHQL_VALIDATION_FAILED'
15
+ /** Invalid variables or arguments (Standard Schema issues in `extensions.issues`), HTTP 400. */
16
+ readonly BadUserInput: 'BAD_USER_INPUT'
17
+ /** The document exceeds `limits.maxDepth`, `maxAliases` or `maxDirectives`. */
18
+ readonly QueryTooComplex: 'QUERY_TOO_COMPLEX'
19
+ /** HTTP 401. */
20
+ readonly Unauthenticated: 'UNAUTHENTICATED'
21
+ /** A guard returned `false`, or HTTP 403. */
22
+ readonly Forbidden: 'FORBIDDEN'
23
+ /** HTTP 404, or a key missing from `orderByKeys`. */
24
+ readonly NotFound: 'NOT_FOUND'
25
+ /** HTTP 409. */
26
+ readonly Conflict: 'CONFLICT'
27
+ /** HTTP 429. */
28
+ readonly TooManyRequests: 'TOO_MANY_REQUESTS'
29
+ /** Any other HTTP 4xx. */
30
+ readonly BadRequest: 'BAD_REQUEST'
31
+ /** A subscriber fell behind its buffer: its stream ends with this error. */
32
+ readonly SubscriptionOverflow: 'SUBSCRIPTION_OVERFLOW'
33
+ /** Too many active subscriptions (`subscriptions.maxActive`), HTTP 503. */
34
+ readonly ServiceUnavailable: 'SERVICE_UNAVAILABLE'
35
+ /** An unexpected error, masked (`Unexpected error.`) unless `exposeErrors`. */
36
+ readonly InternalServerError: 'INTERNAL_SERVER_ERROR'
37
+ } = Object.freeze({
38
+ ParseFailed: 'GRAPHQL_PARSE_FAILED',
39
+ ValidationFailed: 'GRAPHQL_VALIDATION_FAILED',
40
+ BadUserInput: 'BAD_USER_INPUT',
41
+ QueryTooComplex: 'QUERY_TOO_COMPLEX',
42
+ Unauthenticated: 'UNAUTHENTICATED',
43
+ Forbidden: 'FORBIDDEN',
44
+ NotFound: 'NOT_FOUND',
45
+ Conflict: 'CONFLICT',
46
+ TooManyRequests: 'TOO_MANY_REQUESTS',
47
+ BadRequest: 'BAD_REQUEST',
48
+ SubscriptionOverflow: 'SUBSCRIPTION_OVERFLOW',
49
+ ServiceUnavailable: 'SERVICE_UNAVAILABLE',
50
+ InternalServerError: 'INTERNAL_SERVER_ERROR',
51
+ })
52
+
53
+ /** One of the {@link GraphQLErrorCode} values. */
54
+ export type GraphQLErrorCodeValue = (typeof GraphQLErrorCode)[keyof typeof GraphQLErrorCode]
55
+
56
+ /** Options of {@link GraphQLException}. */
57
+ export interface GraphQLExceptionOptions {
58
+ /** `extensions.code` (`'NOT_FOUND'`, `'CONFLICT'`, or an application code). */
59
+ readonly code: GraphQLErrorCodeValue | (string & Record<never, never>)
60
+ /** Extra `extensions` entries, sent to the client (`code` always wins). */
61
+ readonly extensions?: Readonly<Record<string, unknown>>
62
+ /**
63
+ * The original error: never sent, logged once at `error` level with the request id, the schema
64
+ * coordinate and the path when the exception is shown for a field.
65
+ */
66
+ readonly cause?: unknown
67
+ }
68
+
69
+ /**
70
+ * An error whose message and extensions are sent to the client as they are (§5.10): throw it from a
71
+ * resolver or return it from a `GraphQLErrorMapper`.
72
+ *
73
+ * ```ts
74
+ * throw new GraphQLException(`No book ${id}`, { code: 'NOT_FOUND', extensions: { bookId: id } })
75
+ * ```
76
+ */
77
+ export class GraphQLException extends Error {
78
+ /** `extensions.code`. */
79
+ readonly code: string
80
+ /** Extra `extensions` entries. */
81
+ readonly extensions: Readonly<Record<string, unknown>> | undefined
82
+
83
+ constructor(message: string, options: GraphQLExceptionOptions) {
84
+ super(message, options.cause === undefined ? undefined : { cause: options.cause })
85
+ this.name = 'GraphQLException'
86
+ this.code = options.code
87
+ this.extensions = options.extensions
88
+ }
89
+ }