@znight/graphql 0.0.0 → 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 +1063 -0
  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 +97 -1
  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,122 @@
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
+ }
59
+
60
+ /** A property of an object type's declaration, as union discriminators see it. */
61
+ export interface DiscriminantProperty {
62
+ readonly name: string
63
+ readonly optional: boolean
64
+ /** The literal value of a literal-typed property. */
65
+ readonly literal: string | number | boolean | undefined
66
+ }
67
+
68
+ /** An input object type. */
69
+ export interface InputModel extends BaseModel {
70
+ readonly kind: 'input'
71
+ readonly fields: ArgModel[]
72
+ }
73
+
74
+ /** An enum type. */
75
+ export interface EnumModel extends BaseModel {
76
+ readonly kind: 'enum'
77
+ readonly values: readonly EnumValueModel[]
78
+ }
79
+
80
+ /** A value of an enum. */
81
+ export interface EnumValueModel {
82
+ readonly name: string
83
+ readonly value: string | number
84
+ readonly description: string | undefined
85
+ readonly deprecationReason: string | undefined
86
+ }
87
+
88
+ /** A union type. */
89
+ export interface UnionModel extends BaseModel {
90
+ readonly kind: 'union'
91
+ members: readonly string[]
92
+ discriminator: DiscriminatorModel | undefined
93
+ }
94
+
95
+ /** How the runtime tells union members apart. */
96
+ export type DiscriminatorModel =
97
+ | { readonly by: 'property'; readonly property: string; readonly values: Readonly<Record<string, string>> }
98
+ | { readonly by: 'class'; readonly classes: readonly { readonly identity: string; readonly name: string }[] }
99
+ | { readonly by: 'typename' }
100
+
101
+ /** A scalar: built in (`DateTime`, `BigInt`, `JSON`) or declared with `scalar<T>()`. */
102
+ export interface ScalarModel extends BaseModel {
103
+ readonly kind: 'scalar'
104
+ /** Identity of the exported `scalar<T>()` constant; `undefined` for a built-in scalar. */
105
+ readonly definition: string | undefined
106
+ readonly specifiedByURL: string | undefined
107
+ }
108
+
109
+ /** A named type of the model. */
110
+ export type TypeModel = ObjectModel | InputModel | EnumModel | UnionModel | ScalarModel
111
+
112
+ /** The schema of the application, built once per compile. */
113
+ export interface GraphQLModel {
114
+ readonly roots: { readonly query: string; readonly mutation?: string; readonly subscription?: string }
115
+ /** Sorted by name. */
116
+ readonly types: readonly TypeModel[]
117
+ /** Route metadata entries of every field (effective), for `groups`. */
118
+ readonly meta: readonly MetaEntry[]
119
+ /** The SDL (`printSdl`), without trailing newline. */
120
+ readonly sdl: string
121
+ readonly schemaHash: string
122
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Options of the compiler extension (RFC 0005 §6.7), given with `graphql.configure({ ... })` in
3
+ * `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. */
14
+ readonly number?: 'Float' | 'Int'
15
+ }
16
+
17
+ /** {@link GraphQLCompilerOptions} with their defaults. */
18
+ export interface ResolvedGraphQLCompilerOptions {
19
+ readonly sdl: string | false
20
+ readonly number: 'Float' | 'Int'
21
+ }
22
+
23
+ /** The defaults. */
24
+ export const DEFAULT_OPTIONS: ResolvedGraphQLCompilerOptions = Object.freeze({ sdl: false, number: 'Float' })
25
+
26
+ const KNOWN = new Set(['sdl', 'number'])
27
+
28
+ /**
29
+ * Merges `options` onto `base`. Throws a `TypeError` naming the option on an invalid value: options
30
+ * are written in `znight.config.ts`, the error surfaces when the configuration is loaded.
31
+ */
32
+ export function resolveOptions(
33
+ base: ResolvedGraphQLCompilerOptions,
34
+ options: GraphQLCompilerOptions,
35
+ ): ResolvedGraphQLCompilerOptions {
36
+ if (typeof options !== 'object' || options === null || Array.isArray(options)) {
37
+ throw new TypeError('[znight] graphql.configure() expects an options object: { sdl?, number? }')
38
+ }
39
+ for (const key of Object.keys(options)) {
40
+ if (!KNOWN.has(key))
41
+ throw new TypeError(`[znight] graphql.configure(): unknown option "${key}" (options: sdl, number)`)
42
+ }
43
+ const sdl = options.sdl === undefined ? base.sdl : options.sdl
44
+ if (sdl !== false && (typeof sdl !== 'string' || sdl.trim() === '')) {
45
+ throw new TypeError(
46
+ '[znight] graphql.configure(): "sdl" must be a path relative to the project root (\'schema.graphql\') or false',
47
+ )
48
+ }
49
+ const number = options.number === undefined ? base.number : options.number
50
+ if (number !== 'Float' && number !== 'Int') {
51
+ throw new TypeError(`[znight] graphql.configure(): "number" must be 'Float' or 'Int'`)
52
+ }
53
+ return Object.freeze({ sdl, number })
54
+ }
@@ -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
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The SDL view of the model (`sdl.ts` prints it), from which `schemaHash` and the SDL artifact
3
+ * derive.
4
+ */
5
+ import type { SdlInputValue, SdlType } from '../sdl.ts'
6
+ import type { ArgModel, TypeModel } from './model.ts'
7
+
8
+ /** The SDL view of a model type. */
9
+ export function sdlType(model: TypeModel): SdlType {
10
+ const base = model.description === undefined ? {} : { description: model.description }
11
+ switch (model.kind) {
12
+ case 'object':
13
+ return {
14
+ kind: 'object',
15
+ name: model.name,
16
+ ...base,
17
+ fields: model.fields.map(field => ({
18
+ name: field.name,
19
+ type: field.type,
20
+ ...(field.description === undefined ? {} : { description: field.description }),
21
+ ...(field.deprecationReason === undefined ? {} : { deprecationReason: field.deprecationReason }),
22
+ ...(field.args.length === 0 ? {} : { args: field.args.map(sdlInputValue) }),
23
+ })),
24
+ }
25
+ case 'input':
26
+ return { kind: 'input', name: model.name, ...base, fields: model.fields.map(sdlInputValue) }
27
+ case 'enum':
28
+ return {
29
+ kind: 'enum',
30
+ name: model.name,
31
+ ...base,
32
+ values: model.values.map(value => ({
33
+ name: value.name,
34
+ ...(value.description === undefined ? {} : { description: value.description }),
35
+ ...(value.deprecationReason === undefined ? {} : { deprecationReason: value.deprecationReason }),
36
+ })),
37
+ }
38
+ case 'union':
39
+ return { kind: 'union', name: model.name, ...base, members: model.members }
40
+ case 'scalar':
41
+ return {
42
+ kind: 'scalar',
43
+ name: model.name,
44
+ ...base,
45
+ ...(model.definition === undefined ? { builtin: true } : {}),
46
+ ...(model.specifiedByURL === undefined ? {} : { specifiedByURL: model.specifiedByURL }),
47
+ }
48
+ }
49
+ }
50
+
51
+ function sdlInputValue(arg: ArgModel): SdlInputValue {
52
+ return {
53
+ name: arg.name,
54
+ type: arg.type,
55
+ ...(arg.description === undefined ? {} : { description: arg.description }),
56
+ ...(arg.deprecationReason === undefined ? {} : { deprecationReason: arg.deprecationReason }),
57
+ ...(arg.defaultValue === undefined ? {} : { defaultValue: arg.defaultValue }),
58
+ }
59
+ }
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
+ }