@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.
- package/LICENSE +21 -0
- package/README.md +1346 -0
- package/dist/compiler.d.mts +114 -0
- package/dist/compiler.mjs +4599 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/index.d.mts +583 -0
- package/dist/index.mjs +134 -0
- package/dist/index.mjs.map +1 -0
- package/dist/names-BlzDM5cF.mjs +111 -0
- package/dist/names-BlzDM5cF.mjs.map +1 -0
- package/dist/plugin-CR_wVfRU.mjs +3445 -0
- package/dist/plugin-CR_wVfRU.mjs.map +1 -0
- package/dist/testing.d.mts +101 -0
- package/dist/testing.mjs +306 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-DQ713UgI.d.mts +266 -0
- package/package.json +97 -1
- package/src/compiler/analyze.ts +727 -0
- package/src/compiler/builder.ts +1561 -0
- package/src/compiler/checks.ts +105 -0
- package/src/compiler/conformance.ts +1068 -0
- package/src/compiler/constants.ts +80 -0
- package/src/compiler/data.ts +83 -0
- package/src/compiler/defaults.ts +92 -0
- package/src/compiler/discriminators.ts +53 -0
- package/src/compiler/docs.ts +55 -0
- package/src/compiler/generate.ts +419 -0
- package/src/compiler/graphql-js.ts +42 -0
- package/src/compiler/ids.ts +128 -0
- package/src/compiler/index.ts +98 -0
- package/src/compiler/locate.ts +82 -0
- package/src/compiler/model.ts +137 -0
- package/src/compiler/options.ts +85 -0
- package/src/compiler/scalars.ts +138 -0
- package/src/compiler/schema-first.ts +595 -0
- package/src/compiler/sdl-source.ts +529 -0
- package/src/compiler/sdl-view.ts +61 -0
- package/src/compiler/suggest.ts +39 -0
- package/src/context.ts +181 -0
- package/src/decisions.ts +169 -0
- package/src/decorators.ts +202 -0
- package/src/errors.ts +89 -0
- package/src/execution.ts +1064 -0
- package/src/format.ts +285 -0
- package/src/index.ts +89 -0
- package/src/limits.ts +177 -0
- package/src/loaders.ts +149 -0
- package/src/memory-pubsub.ts +133 -0
- package/src/metadata.ts +222 -0
- package/src/names.ts +47 -0
- package/src/options.ts +331 -0
- package/src/pipeline.ts +338 -0
- package/src/plugin.ts +579 -0
- package/src/pubsub.ts +36 -0
- package/src/results.ts +206 -0
- package/src/scalars.ts +138 -0
- package/src/schema-hash.ts +18 -0
- package/src/schema.ts +635 -0
- package/src/sdl.ts +530 -0
- package/src/testing.ts +454 -0
- package/src/tokens.ts +22 -0
- package/src/transport/abort.ts +80 -0
- package/src/transport/accept.ts +155 -0
- package/src/transport/graphiql.ts +106 -0
- package/src/transport/http.ts +373 -0
- package/src/transport/sse.ts +45 -0
- package/src/type-ref.ts +89 -0
- package/src/types.ts +225 -0
- package/src/validate-args.ts +48 -0
package/src/type-ref.ts
ADDED
|
@@ -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
|
+
}
|