@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,89 @@
1
+ /**
2
+ * GraphQL type references as written in `metadata.extensions.graphql` (`'Book!'`, `'[Book!]!'`,
3
+ * `'[[Int]]'`): parsing and printing.
4
+ *
5
+ * Import-free: shared by the compiler extension (which prints them) and the runtime (which parses
6
+ * them once, at boot, to build the schema). The grammar is the canonical form graphql-js prints
7
+ * (`String(type)`): no whitespace, no comments, names matching `/^[_A-Za-z][_0-9A-Za-z]*$/`.
8
+ */
9
+
10
+ /** A parsed type reference. */
11
+ export type GraphQLTypeRef = NamedTypeRef | ListTypeRef
12
+
13
+ /** A named type (`Book`, `Book!`). */
14
+ export interface NamedTypeRef {
15
+ readonly kind: 'named'
16
+ readonly name: string
17
+ readonly nonNull: boolean
18
+ }
19
+
20
+ /** A list type (`[Book]`, `[Book!]!`). */
21
+ export interface ListTypeRef {
22
+ readonly kind: 'list'
23
+ /** The item type. */
24
+ readonly of: GraphQLTypeRef
25
+ readonly nonNull: boolean
26
+ }
27
+
28
+ /** Result of one parsing step: the reference and the index after it. */
29
+ interface Step {
30
+ readonly ref: GraphQLTypeRef
31
+ readonly end: number
32
+ }
33
+
34
+ /**
35
+ * Parses a type reference. Returns `undefined` when `text` is not a canonical reference, so that
36
+ * callers validating hand-written metadata can collect every problem before failing.
37
+ */
38
+ export function parseTypeRef(text: string): GraphQLTypeRef | undefined {
39
+ const step = parseAt(text, 0)
40
+ return step !== undefined && step.end === text.length ? step.ref : undefined
41
+ }
42
+
43
+ /** Prints a type reference in its canonical form (the form graphql-js prints). */
44
+ export function printTypeRef(ref: GraphQLTypeRef): string {
45
+ const inner = ref.kind === 'named' ? ref.name : `[${printTypeRef(ref.of)}]`
46
+ return ref.nonNull ? `${inner}!` : inner
47
+ }
48
+
49
+ /** The innermost type name of a reference (`'[Book!]!'` → `'Book'`), `undefined` when malformed. */
50
+ export function namedTypeOf(ref: string | GraphQLTypeRef): string | undefined {
51
+ let current = typeof ref === 'string' ? parseTypeRef(ref) : ref
52
+ while (current !== undefined && current.kind === 'list') current = current.of
53
+ return current?.name
54
+ }
55
+
56
+ const OPEN_BRACKET = 0x5b // [
57
+ const CLOSE_BRACKET = 0x5d // ]
58
+ const BANG = 0x21 // !
59
+
60
+ function parseAt(text: string, start: number): Step | undefined {
61
+ let ref: GraphQLTypeRef
62
+ let index: number
63
+ if (text.charCodeAt(start) === OPEN_BRACKET) {
64
+ const item = parseAt(text, start + 1)
65
+ if (item === undefined || text.charCodeAt(item.end) !== CLOSE_BRACKET) return undefined
66
+ index = item.end + 1
67
+ ref = { kind: 'list', of: item.ref, nonNull: false }
68
+ } else {
69
+ index = start
70
+ if (!isNameStart(text.charCodeAt(index))) return undefined
71
+ index++
72
+ while (isNameContinue(text.charCodeAt(index))) index++
73
+ ref = { kind: 'named', name: text.slice(start, index), nonNull: false }
74
+ }
75
+ if (text.charCodeAt(index) === BANG) {
76
+ return { ref: { ...ref, nonNull: true }, end: index + 1 }
77
+ }
78
+ return { ref, end: index }
79
+ }
80
+
81
+ /** `[_A-Za-z]`. `NaN` (past the end) is never a name character. */
82
+ function isNameStart(code: number): boolean {
83
+ return code === 0x5f || (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a)
84
+ }
85
+
86
+ /** `[_0-9A-Za-z]`. */
87
+ function isNameContinue(code: number): boolean {
88
+ return isNameStart(code) || (code >= 0x30 && code <= 0x39)
89
+ }
package/src/types.ts ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Type-level contracts of `@znight/graphql` (RFC 0005 §5.5, §5.8–§5.10, §7.1): what resolvers,
3
+ * guards, loaders and error mappers implement or receive. The runtime implements them; the compiler
4
+ * extension recognises some of them by identity (`compiler/ids.ts`).
5
+ *
6
+ * Types only: this module emits no code and imports types only (`graphql` and `@znight/http` are
7
+ * required peers), so both the runtime and the compiler extension may reference it.
8
+ */
9
+ import type { Token } from '@znight/core'
10
+ import type { GuardEnforcement, HttpContext, RouteMetaCarrier } from '@znight/http'
11
+ import type { ConstValueNode, DocumentNode, GraphQLResolveInfo } from 'graphql'
12
+ import type { GraphQLException } from './errors.ts'
13
+
14
+ /** Arguments of a field, after GraphQL coercion (and null handling, §7.3), by argument name. */
15
+ export type GraphQLArgs = Readonly<Record<string, unknown>>
16
+
17
+ /** Kind of a GraphQL operation. */
18
+ export type GraphQLOperationKind = 'query' | 'mutation' | 'subscription'
19
+
20
+ /** How an operation reached the server: GraphQL over HTTP, GraphQL over SSE, or `plugin.execute()`. */
21
+ export type GraphQLTransport = 'http' | 'sse' | 'direct'
22
+
23
+ /**
24
+ * Options of a loader (§5.5), read once per loader class: the `options` property of a
25
+ * {@link BatchLoad} provider. They are DataLoader's, except `batchScheduleFn` (Znight batches per
26
+ * tick of the execution).
27
+ */
28
+ export interface LoaderOptions<K = unknown> {
29
+ /** Largest number of keys passed to one `load()` call. Default: unlimited. */
30
+ readonly maxBatchSize?: number
31
+ /** Cache values per execution (by key). Default `true`. */
32
+ readonly cache?: boolean
33
+ /** Cache key of a key, for object keys (`key => key.id`). Default: the key itself. */
34
+ cacheKeyFn?(key: K): unknown
35
+ }
36
+
37
+ /**
38
+ * A batch loader (§5.5): a provider that loads many keys at once. Inject its per-execution
39
+ * DataLoader with `@Loader() authors: Loader<AuthorsById>`.
40
+ *
41
+ * ```ts
42
+ * @Injectable()
43
+ * export class AuthorsById implements BatchLoad<ID, Author> {
44
+ * constructor(private readonly authors: AuthorRepository) {}
45
+ *
46
+ * async load(ids: readonly ID[]): Promise<readonly (Author | Error)[]> {
47
+ * return orderByKeys(ids, await this.authors.findMany(ids), author => author.id)
48
+ * }
49
+ * }
50
+ * ```
51
+ */
52
+ export interface BatchLoad<K, V> {
53
+ /**
54
+ * Loads the values of `keys`: one value (or an `Error` failing that key only) per key, in the
55
+ * order of the keys. Another number of values fails every key of the batch (ZNIGHT065).
56
+ */
57
+ load(keys: readonly K[]): Promise<readonly (V | Error)[]>
58
+ /** DataLoader options, read once per loader class. */
59
+ readonly options?: LoaderOptions<K>
60
+ }
61
+
62
+ /** Key type of a {@link BatchLoad} provider. */
63
+ export type LoaderKey<L> = L extends { load(keys: readonly (infer K)[]): unknown } ? K : never
64
+
65
+ /** Value type of a {@link BatchLoad} provider (`Error`s excluded). */
66
+ export type LoaderValue<L> = L extends { load(keys: never): Promise<readonly (infer V)[]> } ? Exclude<V, Error> : never
67
+
68
+ /**
69
+ * The DataLoader of one execution for a loader (§5.5): the part of the `dataloader` API resolvers
70
+ * use. `Loader<L>` (exported with the `@Loader()` decorator) is this view for the provider `L`.
71
+ */
72
+ export interface BatchLoader<K, V> {
73
+ /** The value of `key`, batched with the other keys requested in the same tick. */
74
+ load(key: K): Promise<V>
75
+ /** The values of `keys`: a value or an `Error` per key, never a rejection. */
76
+ loadMany(keys: readonly K[]): Promise<(V | Error)[]>
77
+ /** Forgets the cached value of `key`. */
78
+ clear(key: K): this
79
+ /** Forgets every cached value. */
80
+ clearAll(): this
81
+ /** Caches a value for `key` (no effect when `key` is already cached). */
82
+ prime(key: K, value: V | PromiseLike<V> | Error): this
83
+ }
84
+
85
+ /** A class of {@link BatchLoad} providers. */
86
+ export type BatchLoadClass<L extends BatchLoad<unknown, unknown> = BatchLoad<unknown, unknown>> = abstract new (
87
+ ...args: never[]
88
+ ) => L
89
+
90
+ /**
91
+ * The context of one GraphQL operation (§5.8): a request-scoped external
92
+ * (`@znight/graphql#GraphQLContext`), injected with `@Ctx()` in resolvers, in request-scoped
93
+ * providers, or as `Lazy<GraphQLContext>` in singletons. It is not a bag for user data: the
94
+ * authenticated user is a request-scoped provider filled by a guard.
95
+ */
96
+ export interface GraphQLContext {
97
+ /** Id of the request carrying the operation (`HttpContext.requestId` over HTTP). */
98
+ readonly requestId: string
99
+ readonly transport: GraphQLTransport
100
+ /** The HTTP request carrying the operation; `undefined` for `plugin.execute()`. */
101
+ readonly http: HttpContext | undefined
102
+ readonly operationType: GraphQLOperationKind
103
+ readonly operationName: string | undefined
104
+ /** The parsed (and validated) document, shared through the document cache: never mutate it. */
105
+ readonly document: DocumentNode
106
+ readonly variables: Readonly<Record<string, unknown>>
107
+ /**
108
+ * Over HTTP, the request's `ctx.signal` (RFC 0006): client gone, SSE stream closed on shutdown or
109
+ * `maxDuration`. Passed to graphql-js as `abortSignal`. Direct executions get the plugin's own.
110
+ * A subscription's also aborts when its stream ends (the source ended, or `return()`).
111
+ */
112
+ readonly signal: AbortSignal
113
+ /** Per-operation storage. */
114
+ readonly state: Map<string | symbol, unknown>
115
+ /** The DataLoader of the current execution for a loader class (`ctx.loader(AuthorsById)`). */
116
+ loader<L extends BatchLoad<unknown, unknown>>(loader: BatchLoadClass<L>): BatchLoader<LoaderKey<L>, LoaderValue<L>>
117
+ /**
118
+ * The DataLoader of the current execution for a loader provider token: what the generated
119
+ * `invoke` functions call (`c.loader('src/a.loader.ts#AuthorsById')`), with no class lookup.
120
+ */
121
+ loader(token: Token): BatchLoader<unknown, unknown>
122
+ }
123
+
124
+ /**
125
+ * What a {@link GraphQLGuard} receives for one resolver call (§5.9). It satisfies the
126
+ * `RouteMetaCarrier` of `@znight/http`, so route decorators read it: `Roles.get(ctx)`.
127
+ */
128
+ export interface ResolverContext<TParent = unknown, TArgs extends GraphQLArgs = GraphQLArgs> extends RouteMetaCarrier {
129
+ /** Effective metadata of the field (method decisions over class decisions) and its schema coordinate. */
130
+ readonly route: {
131
+ readonly meta: Readonly<Record<string, unknown>>
132
+ /** `Query.books`, `Book.author`. */
133
+ readonly coordinate: string
134
+ }
135
+ /** The operation. */
136
+ readonly operation: GraphQLContext
137
+ /** Name of the type owning the field (`Query`, `Book`). */
138
+ readonly parentType: string
139
+ /** Name of the field. */
140
+ readonly field: string
141
+ readonly args: TArgs
142
+ /** The parent object (`undefined` for root fields, unless a root value is configured). */
143
+ readonly parent: TParent
144
+ readonly info: GraphQLResolveInfo
145
+ /** `operation.http`. */
146
+ readonly http: HttpContext | undefined
147
+ }
148
+
149
+ /**
150
+ * A guard of GraphQL fields (§5.9). Guards run global (plugin option) → class `@UseGuard` → method
151
+ * `@UseGuard`; `false` fails the field with a `FORBIDDEN` error, a thrown error is mapped (§5.10).
152
+ * One class may also implement `CanActivate` (HTTP routes).
153
+ */
154
+ export interface GraphQLGuard {
155
+ canResolve(ctx: ResolverContext): boolean | Promise<boolean>
156
+ }
157
+
158
+ /** A class producing {@link GraphQLGuard}s, with the route decorators it enforces (`static enforces`). */
159
+ export type GraphQLGuardClass = (abstract new (...args: never[]) => GraphQLGuard) & GuardEnforcement
160
+
161
+ /**
162
+ * Converts application errors to GraphQL errors (§5.10). Mappers are tried in order for every
163
+ * error of `errors[]` that is not a `GraphQLException` already; the first one returning an
164
+ * exception wins, `undefined` passes. Synchronous: errors are formatted once the execution
165
+ * finished, without an extra await per error.
166
+ */
167
+ export interface GraphQLErrorMapper {
168
+ map(error: unknown, ctx: GraphQLContext): GraphQLException | undefined
169
+ }
170
+
171
+ /** A class producing {@link GraphQLErrorMapper}s. */
172
+ export type GraphQLErrorMapperClass = abstract new (...args: never[]) => GraphQLErrorMapper
173
+
174
+ /**
175
+ * Turns the errors of a field into values of the field (errors as data, RFC 0008). It applies to
176
+ * the resolver-backed fields whose type is a union holding one of the object types it declares
177
+ * (`static types`), decided at boot; every other field keeps its errors in `errors[]`.
178
+ *
179
+ * ```ts
180
+ * @Injectable()
181
+ * export class Rejections implements GraphQLResultMapper {
182
+ * static readonly types = ['NotFoundRejection']
183
+ *
184
+ * toResult(error: unknown): NotFoundRejection | undefined {
185
+ * if (error instanceof NotFoundException) return { __typename: 'NotFoundRejection', message: error.message }
186
+ * return undefined
187
+ * }
188
+ * }
189
+ * ```
190
+ */
191
+ export interface GraphQLResultMapper {
192
+ /**
193
+ * The value of the field for `error`: an object of a member of the field's union, or `undefined`
194
+ * to leave the error to the next mapper, then to `errors[]`. Synchronous. `error` is the raw
195
+ * error, a `GraphQLException` included (a guard returning `false` is `FORBIDDEN`, a Standard
196
+ * Schema failure of the arguments `BAD_USER_INPUT` with `extensions.issues`).
197
+ */
198
+ toResult(error: unknown, ctx: ResolverContext): unknown
199
+ }
200
+
201
+ /** The object types a result mapper may produce: names of object types of the schema. */
202
+ export interface GraphQLResultTypes {
203
+ readonly types: readonly string[]
204
+ }
205
+
206
+ /** A class producing {@link GraphQLResultMapper}s, with the object types it produces (`static types`). */
207
+ export type GraphQLResultMapperClass = (abstract new (...args: never[]) => GraphQLResultMapper) & GraphQLResultTypes
208
+
209
+ /**
210
+ * The implementation of a custom scalar (§5.7), given to `scalar<T>(name, definition)`. The
211
+ * runtime maps it to graphql-js 17 `GraphQLScalarType` coercers: `serialize` → `coerceOutputValue`,
212
+ * `parseValue` → `coerceInputValue`, `parseLiteral` → `coerceInputLiteral` (the deprecated
213
+ * graphql-js names are never passed on).
214
+ */
215
+ export interface ScalarDefinition<T> {
216
+ readonly description?: string
217
+ /** URL of the specification of the scalar (`@specifiedBy`). */
218
+ readonly specifiedByURL?: string
219
+ /** Internal value → response value. */
220
+ serialize(value: T): unknown
221
+ /** Variable value (JSON) → internal value; throw to reject it. */
222
+ parseValue(value: unknown): T
223
+ /** Literal of the document → internal value. Default: `parseValue` of the literal's JSON value. */
224
+ parseLiteral?(literal: ConstValueNode): T
225
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `validateArgs()`: Standard Schema validation of resolver arguments (RFC 0005 §5.4, §7.3), called
3
+ * by the generated `invoke` functions (README, "Compiler contract").
4
+ */
5
+ import type { InferSchemaOutput, StandardSchemaV1, StandardSchemaV1Result } from '@znight/http'
6
+
7
+ import { isThenable, toIssueDetail } from '@znight/http'
8
+
9
+ import { GraphQLErrorCode, GraphQLException } from './errors.ts'
10
+
11
+ /**
12
+ * Validates an argument value (after GraphQL coercion) with a [Standard Schema](https://standardschema.dev)
13
+ * (Zod, Valibot, ArkType...) and returns the schema **output** (transforms run, defaults applied).
14
+ *
15
+ * Synchronous when the schema validates synchronously (no promise, no microtask), a promise
16
+ * otherwise: generated code `await`s the result, which is free for a plain value.
17
+ *
18
+ * @param label what is validated, for the message: the argument name (`'input'`), or `'arguments'`
19
+ * for `@Args(Schema)` without name.
20
+ * @throws GraphQLException `BAD_USER_INPUT`, `extensions.issues: [{ path, message }]`, when the
21
+ * value is invalid.
22
+ * @throws TypeError when `schema` is not a Standard Schema v1 (a programming error, masked).
23
+ */
24
+ export function validateArgs<S extends StandardSchemaV1>(
25
+ schema: S,
26
+ value: unknown,
27
+ label: string,
28
+ ): InferSchemaOutput<S> | Promise<InferSchemaOutput<S>> {
29
+ const props =
30
+ schema !== null && (typeof schema === 'object' || typeof schema === 'function')
31
+ ? (schema as { readonly '~standard'?: { readonly version?: unknown; readonly validate?: unknown } })['~standard']
32
+ : undefined
33
+ if (props === null || typeof props !== 'object' || props.version !== 1 || typeof props.validate !== 'function') {
34
+ throw new TypeError(`The schema of ${label} is not a Standard Schema v1 (missing "~standard.validate")`)
35
+ }
36
+ const result = (props.validate as (value: unknown) => StandardSchemaV1Result<InferSchemaOutput<S>>)(value)
37
+ // sync fast path: most schemas validate synchronously
38
+ if (isThenable(result)) return Promise.resolve(result).then(settled => unwrap(settled, label))
39
+ return unwrap(result, label)
40
+ }
41
+
42
+ function unwrap<T>(result: StandardSchemaV1Result<T>, label: string): T {
43
+ if (result.issues === undefined) return result.value
44
+ throw new GraphQLException(`Invalid ${label}`, {
45
+ code: GraphQLErrorCode.BadUserInput,
46
+ extensions: { issues: result.issues.map(toIssueDetail) },
47
+ })
48
+ }