@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.
- package/LICENSE +21 -0
- package/README.md +1063 -0
- package/dist/compiler.d.mts +93 -0
- package/dist/compiler.mjs +2832 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/index.d.mts +542 -0
- package/dist/index.mjs +128 -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-BdBxr5NQ.mjs +3222 -0
- package/dist/plugin-BdBxr5NQ.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-CgbV9LjD.d.mts +234 -0
- package/package.json +97 -1
- package/src/compiler/analyze.ts +608 -0
- package/src/compiler/builder.ts +1640 -0
- package/src/compiler/constants.ts +74 -0
- package/src/compiler/data.ts +73 -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 +405 -0
- package/src/compiler/ids.ts +128 -0
- package/src/compiler/index.ts +93 -0
- package/src/compiler/locate.ts +44 -0
- package/src/compiler/model.ts +122 -0
- package/src/compiler/options.ts +54 -0
- package/src/compiler/scalars.ts +138 -0
- package/src/compiler/sdl-view.ts +59 -0
- package/src/context.ts +181 -0
- package/src/decisions.ts +169 -0
- package/src/decorators.ts +196 -0
- package/src/errors.ts +89 -0
- package/src/execution.ts +1064 -0
- package/src/format.ts +285 -0
- package/src/index.ts +84 -0
- package/src/limits.ts +177 -0
- package/src/loaders.ts +149 -0
- package/src/memory-pubsub.ts +133 -0
- package/src/metadata.ts +195 -0
- package/src/names.ts +47 -0
- package/src/options.ts +309 -0
- package/src/pipeline.ts +277 -0
- package/src/plugin.ts +557 -0
- package/src/pubsub.ts +36 -0
- package/src/scalars.ts +131 -0
- package/src/schema-hash.ts +18 -0
- package/src/schema.ts +557 -0
- package/src/sdl.ts +515 -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 +190 -0
- package/src/validate-args.ts +48 -0
package/src/decisions.ts
ADDED
|
@@ -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,196 @@
|
|
|
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
|
+
* ```ts
|
|
64
|
+
* @Resolver()
|
|
65
|
+
* export class BookResolver {
|
|
66
|
+
* constructor(private readonly catalog: CatalogService) {}
|
|
67
|
+
*
|
|
68
|
+
* @Query()
|
|
69
|
+
* book(@Args('id') id: ID): Promise<Book | null> {
|
|
70
|
+
* return this.catalog.find(id)
|
|
71
|
+
* }
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @param options static: `{ scope? }`.
|
|
76
|
+
*/
|
|
77
|
+
export function Resolver(_options?: ResolverOptions): ClassMarker {
|
|
78
|
+
return classMarker
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A field of the `Query` root type. The method's declared return type is the field type
|
|
83
|
+
* (`Promise<T>` unwrapped), its `@Args` the arguments, its JSDoc the description.
|
|
84
|
+
*
|
|
85
|
+
* @param name static: field name. Default: the method name.
|
|
86
|
+
*/
|
|
87
|
+
export function Query(_name?: string): MethodMarker {
|
|
88
|
+
return methodMarker
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A field of the `Mutation` root type.
|
|
93
|
+
*
|
|
94
|
+
* @param name static: field name. Default: the method name.
|
|
95
|
+
*/
|
|
96
|
+
export function Mutation(_name?: string): MethodMarker {
|
|
97
|
+
return methodMarker
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A field of the `Subscription` root type. The method returns an `AsyncIterable<T>` (an async
|
|
102
|
+
* generator, or `pubsub.subscribe(...)`), each value being the field value of one event.
|
|
103
|
+
*
|
|
104
|
+
* @param name static: field name. Default: the method name.
|
|
105
|
+
*/
|
|
106
|
+
export function Subscription(_name?: string): MethodMarker {
|
|
107
|
+
return methodMarker
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A field resolver: adds a field to the type of the `@Parent()` parameter (mandatory), or replaces
|
|
112
|
+
* the property field of the same name.
|
|
113
|
+
*
|
|
114
|
+
* @param name static: field name. Default: the method name.
|
|
115
|
+
*/
|
|
116
|
+
export function ResolveField(_name?: string): MethodMarker {
|
|
117
|
+
return methodMarker
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Field arguments. Without a name, the properties of the parameter type become arguments
|
|
122
|
+
* (`@Args() filter: BookFilter`); with a name, one argument (`@Args('id') id: ID`). With a Standard
|
|
123
|
+
* Schema, the value is validated (and transformed) after GraphQL coerced it; a failure is a
|
|
124
|
+
* `BAD_USER_INPUT` error listing the issues.
|
|
125
|
+
*
|
|
126
|
+
* @param name static: argument name.
|
|
127
|
+
* @param schema value: a Standard Schema validating the value (its output is injected).
|
|
128
|
+
*/
|
|
129
|
+
export function Args(): ParameterMarker
|
|
130
|
+
export function Args(name: string, schema?: StandardSchemaV1): ParameterMarker
|
|
131
|
+
export function Args(schema: StandardSchemaV1): ParameterMarker
|
|
132
|
+
export function Args(_nameOrSchema?: string | StandardSchemaV1, _schema?: StandardSchemaV1): ParameterMarker {
|
|
133
|
+
return parameterMarker
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The parent object of a field resolver (`@ResolveField` only; its type is the parent type). */
|
|
137
|
+
export function Parent(): ParameterMarker {
|
|
138
|
+
return parameterMarker
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The `GraphQLContext` of the operation. */
|
|
142
|
+
export function Ctx(): ParameterMarker {
|
|
143
|
+
return parameterMarker
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The graphql-js `GraphQLResolveInfo` of the call (escape hatch). */
|
|
147
|
+
export function Info(): ParameterMarker {
|
|
148
|
+
return parameterMarker
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The DataLoader of the current execution for the `Loader<L>` parameter type (see {@link Loader}). */
|
|
152
|
+
export function Loader(): ParameterMarker {
|
|
153
|
+
return parameterMarker
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Guards of the fields of a resolver class or of one field, run after the global guards (class
|
|
158
|
+
* guards, then method guards). Guards are providers implementing `GraphQLGuard`.
|
|
159
|
+
*
|
|
160
|
+
* @param guards value: guard classes.
|
|
161
|
+
*/
|
|
162
|
+
export function UseGuard(..._guards: [GraphQLGuardClass, ...GraphQLGuardClass[]]): ClassMarker & MethodMarker {
|
|
163
|
+
return classOrMethodMarker
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Maps the TypeScript type `T` to the GraphQL scalar `name` wherever it appears. `name` is static;
|
|
168
|
+
* the returned definition must be exported (`export const`): the generated module imports it.
|
|
169
|
+
* Returns `definition` unchanged.
|
|
170
|
+
*
|
|
171
|
+
* ```ts
|
|
172
|
+
* export type EmailAddress = string & { readonly __brand: 'EmailAddress' }
|
|
173
|
+
*
|
|
174
|
+
* export const EmailAddressScalar = scalar<EmailAddress>('EmailAddress', {
|
|
175
|
+
* serialize: value => value,
|
|
176
|
+
* parseValue: value => {
|
|
177
|
+
* if (typeof value !== 'string' || !value.includes('@')) throw new TypeError('Expected an e-mail address')
|
|
178
|
+
* return value.toLowerCase() as EmailAddress
|
|
179
|
+
* },
|
|
180
|
+
* })
|
|
181
|
+
*
|
|
182
|
+
* export const UrlScalar = scalar<URL>('URL', GraphQLURL) // a graphql-js GraphQLScalarType
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @param name static: the GraphQL name of the scalar.
|
|
186
|
+
* @param definition value: a {@link ScalarDefinition} or a graphql-js `GraphQLScalarType` (whose
|
|
187
|
+
* name must be `name`, ZNIGHT066 at boot otherwise).
|
|
188
|
+
*/
|
|
189
|
+
export function scalar<T>(name: string, definition: ScalarDefinition<T>): ScalarDefinition<T>
|
|
190
|
+
export function scalar<T>(name: string, definition: GraphQLScalarType): GraphQLScalarType<T>
|
|
191
|
+
export function scalar<T>(
|
|
192
|
+
_name: string,
|
|
193
|
+
definition: ScalarDefinition<T> | GraphQLScalarType,
|
|
194
|
+
): ScalarDefinition<T> | GraphQLScalarType {
|
|
195
|
+
return definition
|
|
196
|
+
}
|
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
|
+
}
|