@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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source locations of diagnostics. Computed only when a diagnostic is reported: the happy path
|
|
3
|
+
* never reads source text.
|
|
4
|
+
*/
|
|
5
|
+
import type { Location, MethodInfo, Project } from '@znight/compiler'
|
|
6
|
+
|
|
7
|
+
/** From the member name: `name?: ` or `name(): ` (a getter), up to the annotation. */
|
|
8
|
+
const MEMBER_HEAD = /^(?:[\w$]+|'[^']*'|"[^"]*")\s*\??\s*(?:\(\s*\))?\s*:\s*/
|
|
9
|
+
|
|
10
|
+
const OPENING = new Set(['(', '[', '{', '<'])
|
|
11
|
+
const CLOSING = new Set([')', ']', '}', '>'])
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The type annotation of a property, a property signature or a getter (`get x(): T`), from the
|
|
15
|
+
* location of the member (its name); the member itself when the annotation cannot be found.
|
|
16
|
+
*/
|
|
17
|
+
export function annotationLocation(project: Project, member: Location | undefined): Location | undefined {
|
|
18
|
+
if (member === undefined) return undefined
|
|
19
|
+
const file = project.getFile(member.file)
|
|
20
|
+
if (file === undefined) return member
|
|
21
|
+
const text = file.text
|
|
22
|
+
const head = MEMBER_HEAD.exec(text.slice(member.start, member.start + 256))
|
|
23
|
+
if (head === null) return member
|
|
24
|
+
const start = member.start + head[0].length
|
|
25
|
+
let depth = 0
|
|
26
|
+
let end = start
|
|
27
|
+
for (let index = start; index < text.length; index++) {
|
|
28
|
+
const char = text[index] as string
|
|
29
|
+
if (char === '=' && text[index + 1] === '>') {
|
|
30
|
+
index++
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
// a getter body or an initializer after the annotation
|
|
34
|
+
if (depth === 0 && end > start && (char === '{' || char === '=')) break
|
|
35
|
+
if (OPENING.has(char)) depth++
|
|
36
|
+
else if (CLOSING.has(char)) {
|
|
37
|
+
if (depth === 0) break
|
|
38
|
+
depth--
|
|
39
|
+
} else if (depth === 0 && (char === ';' || char === ',')) break
|
|
40
|
+
else if (depth === 0 && char === '\n' && !/^\s*[|&]/.test(text.slice(index + 1, index + 64))) break
|
|
41
|
+
if (!/\s/.test(char)) end = index + 1
|
|
42
|
+
}
|
|
43
|
+
return end > start ? file.location({ start, end }) : member
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The return type annotation of a method, from the location of its name: after the parameter list
|
|
48
|
+
* (balanced brackets), `: T` up to the body; `undefined` when it cannot be found.
|
|
49
|
+
*/
|
|
50
|
+
export function returnTypeLocation(project: Project, method: MethodInfo): Location | undefined {
|
|
51
|
+
const file = project.getFile(method.location.file)
|
|
52
|
+
if (file === undefined) return undefined
|
|
53
|
+
const text = file.text
|
|
54
|
+
let index = text.indexOf('(', method.location.end)
|
|
55
|
+
if (index === -1) return undefined
|
|
56
|
+
let depth = 0
|
|
57
|
+
for (; index < text.length; index++) {
|
|
58
|
+
const char = text[index] as string
|
|
59
|
+
if (OPENING.has(char)) depth++
|
|
60
|
+
else if (CLOSING.has(char) && --depth === 0) break
|
|
61
|
+
}
|
|
62
|
+
const head = /^\s*:\s*/.exec(text.slice(index + 1, index + 64))
|
|
63
|
+
if (head === null) return undefined
|
|
64
|
+
const start = index + 1 + head[0].length
|
|
65
|
+
let end = start
|
|
66
|
+
depth = 0
|
|
67
|
+
for (let cursor = start; cursor < text.length; cursor++) {
|
|
68
|
+
const char = text[cursor] as string
|
|
69
|
+
if (char === '=' && text[cursor + 1] === '>') {
|
|
70
|
+
cursor++
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (depth === 0 && (char === '{' || char === ';') && end > start) break
|
|
74
|
+
if (OPENING.has(char)) depth++
|
|
75
|
+
else if (CLOSING.has(char)) {
|
|
76
|
+
if (depth === 0) break
|
|
77
|
+
depth--
|
|
78
|
+
}
|
|
79
|
+
if (!/\s/.test(char)) end = cursor + 1
|
|
80
|
+
}
|
|
81
|
+
return end > start ? file.location({ start, end }) : undefined
|
|
82
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
/** Implemented interfaces (schema-first, RFC 0007), in declaration order; empty in code-first. */
|
|
59
|
+
readonly interfaces: readonly string[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** An interface type (schema-first, RFC 0007). Its fields have no binding. */
|
|
63
|
+
export interface InterfaceModel extends BaseModel {
|
|
64
|
+
readonly kind: 'interface'
|
|
65
|
+
readonly fields: FieldModel[]
|
|
66
|
+
readonly interfaces: readonly string[]
|
|
67
|
+
discriminator: DiscriminatorModel | undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A property of an object type's declaration, as union discriminators see it. */
|
|
71
|
+
export interface DiscriminantProperty {
|
|
72
|
+
readonly name: string
|
|
73
|
+
readonly optional: boolean
|
|
74
|
+
/** The literal value of a literal-typed property. */
|
|
75
|
+
readonly literal: string | number | boolean | undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** An input object type. */
|
|
79
|
+
export interface InputModel extends BaseModel {
|
|
80
|
+
readonly kind: 'input'
|
|
81
|
+
readonly fields: ArgModel[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** An enum type. */
|
|
85
|
+
export interface EnumModel extends BaseModel {
|
|
86
|
+
readonly kind: 'enum'
|
|
87
|
+
readonly values: readonly EnumValueModel[]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A value of an enum. */
|
|
91
|
+
export interface EnumValueModel {
|
|
92
|
+
readonly name: string
|
|
93
|
+
readonly value: string | number
|
|
94
|
+
readonly description: string | undefined
|
|
95
|
+
readonly deprecationReason: string | undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A union type. */
|
|
99
|
+
export interface UnionModel extends BaseModel {
|
|
100
|
+
readonly kind: 'union'
|
|
101
|
+
members: readonly string[]
|
|
102
|
+
discriminator: DiscriminatorModel | undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** How the runtime tells union members apart. */
|
|
106
|
+
export type DiscriminatorModel =
|
|
107
|
+
| { readonly by: 'property'; readonly property: string; readonly values: Readonly<Record<string, string>> }
|
|
108
|
+
| { readonly by: 'class'; readonly classes: readonly { readonly identity: string; readonly name: string }[] }
|
|
109
|
+
| { readonly by: 'typename' }
|
|
110
|
+
|
|
111
|
+
/** A scalar: built in (`DateTime`, `BigInt`, `JSON`) or declared with `scalar<T>()`. */
|
|
112
|
+
export interface ScalarModel extends BaseModel {
|
|
113
|
+
readonly kind: 'scalar'
|
|
114
|
+
/** Identity of the exported `scalar<T>()` constant; `undefined` for a built-in scalar. */
|
|
115
|
+
readonly definition: string | undefined
|
|
116
|
+
readonly specifiedByURL: string | undefined
|
|
117
|
+
/**
|
|
118
|
+
* Schema-first: the description and `specifiedByURL` come from the SDL and are emitted in the
|
|
119
|
+
* metadata, where they win over the definition's (RFC 0007 §5.2).
|
|
120
|
+
*/
|
|
121
|
+
readonly fromSdl?: true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A named type of the model. */
|
|
125
|
+
export type TypeModel = ObjectModel | InterfaceModel | InputModel | EnumModel | UnionModel | ScalarModel
|
|
126
|
+
|
|
127
|
+
/** The schema of the application, built once per compile. */
|
|
128
|
+
export interface GraphQLModel {
|
|
129
|
+
readonly roots: { readonly query: string; readonly mutation?: string; readonly subscription?: string }
|
|
130
|
+
/** Sorted by name. */
|
|
131
|
+
readonly types: readonly TypeModel[]
|
|
132
|
+
/** Route metadata entries of every field (effective), for `groups`. */
|
|
133
|
+
readonly meta: readonly MetaEntry[]
|
|
134
|
+
/** The SDL (`printSdl`), without trailing newline. */
|
|
135
|
+
readonly sdl: string
|
|
136
|
+
readonly schemaHash: string
|
|
137
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options of the compiler extension (RFC 0005 §6.7, RFC 0007 §4.1), given with
|
|
3
|
+
* `graphql.configure({ ... })` in `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. Code-first only. */
|
|
14
|
+
readonly number?: 'Float' | 'Int'
|
|
15
|
+
/**
|
|
16
|
+
* Schema-first mode (RFC 0007): globs of the `.graphql` files that define the schema, relative to
|
|
17
|
+
* the project root (`'src/**\/*.graphql'`, `!` negations allowed). Resolvers are bound to their
|
|
18
|
+
* fields by name and checked against the SDL. Default `false`: code-first.
|
|
19
|
+
*/
|
|
20
|
+
readonly schema?: string | readonly string[] | false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** {@link GraphQLCompilerOptions} with their defaults. */
|
|
24
|
+
export interface ResolvedGraphQLCompilerOptions {
|
|
25
|
+
readonly sdl: string | false
|
|
26
|
+
readonly number: 'Float' | 'Int'
|
|
27
|
+
/** The schema-first globs; `false`: code-first. */
|
|
28
|
+
readonly schema: readonly string[] | false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The defaults. */
|
|
32
|
+
export const DEFAULT_OPTIONS: ResolvedGraphQLCompilerOptions = Object.freeze({
|
|
33
|
+
sdl: false,
|
|
34
|
+
number: 'Float',
|
|
35
|
+
schema: false,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const KNOWN = new Set(['sdl', 'number', 'schema'])
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Merges `options` onto `base`. Throws a `TypeError` naming the option on an invalid value: options
|
|
42
|
+
* are written in `znight.config.ts`, the error surfaces when the configuration is loaded.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveOptions(
|
|
45
|
+
base: ResolvedGraphQLCompilerOptions,
|
|
46
|
+
options: GraphQLCompilerOptions,
|
|
47
|
+
): ResolvedGraphQLCompilerOptions {
|
|
48
|
+
if (typeof options !== 'object' || options === null || Array.isArray(options)) {
|
|
49
|
+
throw new TypeError('[znight] graphql.configure() expects an options object: { sdl?, number?, schema? }')
|
|
50
|
+
}
|
|
51
|
+
for (const key of Object.keys(options)) {
|
|
52
|
+
if (!KNOWN.has(key))
|
|
53
|
+
throw new TypeError(`[znight] graphql.configure(): unknown option "${key}" (options: sdl, number, schema)`)
|
|
54
|
+
}
|
|
55
|
+
const sdl = options.sdl === undefined ? base.sdl : options.sdl
|
|
56
|
+
if (sdl !== false && (typeof sdl !== 'string' || sdl.trim() === '')) {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
'[znight] graphql.configure(): "sdl" must be a path relative to the project root (\'schema.graphql\') or false',
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
const number = options.number === undefined ? base.number : options.number
|
|
62
|
+
if (number !== 'Float' && number !== 'Int') {
|
|
63
|
+
throw new TypeError(`[znight] graphql.configure(): "number" must be 'Float' or 'Int'`)
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze({ sdl, number, schema: resolveSchema(base.schema, options.schema) })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveSchema(
|
|
69
|
+
base: readonly string[] | false,
|
|
70
|
+
schema: GraphQLCompilerOptions['schema'],
|
|
71
|
+
): readonly string[] | false {
|
|
72
|
+
if (schema === undefined) return base
|
|
73
|
+
if (schema === false) return false
|
|
74
|
+
const globs = typeof schema === 'string' ? [schema] : schema
|
|
75
|
+
if (
|
|
76
|
+
!Array.isArray(globs) ||
|
|
77
|
+
globs.length === 0 ||
|
|
78
|
+
globs.some(glob => typeof glob !== 'string' || glob.trim() === '')
|
|
79
|
+
) {
|
|
80
|
+
throw new TypeError(
|
|
81
|
+
'[znight] graphql.configure(): "schema" must be a glob or a non-empty list of globs relative to the project root (\'src/**/*.graphql\'), or false',
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
return Object.freeze([...globs])
|
|
85
|
+
}
|
|
@@ -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
|
+
}
|