@model-ts/graphql 0.0.0-stage → 7.1.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/CHANGELOG.md +23 -0
- package/README.md +1280 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/Assembly.d.ts +24 -0
- package/dist/Assembly.d.ts.map +1 -0
- package/dist/Assembly.js +236 -0
- package/dist/Assembly.js.map +1 -0
- package/dist/Builder.d.ts +27 -0
- package/dist/Builder.d.ts.map +1 -0
- package/dist/Builder.js +40 -0
- package/dist/Builder.js.map +1 -0
- package/dist/Connections.d.ts +41 -0
- package/dist/Connections.d.ts.map +1 -0
- package/dist/Connections.js +243 -0
- package/dist/Connections.js.map +1 -0
- package/dist/Definitions.d.ts +139 -0
- package/dist/Definitions.d.ts.map +1 -0
- package/dist/Definitions.js +117 -0
- package/dist/Definitions.js.map +1 -0
- package/dist/Errors.d.ts +39 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +103 -0
- package/dist/Errors.js.map +1 -0
- package/dist/Execution.d.ts +52 -0
- package/dist/Execution.d.ts.map +1 -0
- package/dist/Execution.js +174 -0
- package/dist/Execution.js.map +1 -0
- package/dist/FieldRuntime.d.ts +5 -0
- package/dist/FieldRuntime.d.ts.map +1 -0
- package/dist/FieldRuntime.js +27 -0
- package/dist/FieldRuntime.js.map +1 -0
- package/dist/FieldTypes.d.ts +232 -0
- package/dist/FieldTypes.d.ts.map +1 -0
- package/dist/FieldTypes.js +6 -0
- package/dist/FieldTypes.js.map +1 -0
- package/dist/Fields.d.ts +8 -0
- package/dist/Fields.d.ts.map +1 -0
- package/dist/Fields.js +72 -0
- package/dist/Fields.js.map +1 -0
- package/dist/Formatting.d.ts +25 -0
- package/dist/Formatting.d.ts.map +1 -0
- package/dist/Formatting.js +29 -0
- package/dist/Formatting.js.map +1 -0
- package/dist/GraphQL.d.ts +10 -0
- package/dist/GraphQL.d.ts.map +1 -0
- package/dist/GraphQL.js +5 -0
- package/dist/GraphQL.js.map +1 -0
- package/dist/InputTypes.d.ts +27 -0
- package/dist/InputTypes.d.ts.map +1 -0
- package/dist/InputTypes.js +203 -0
- package/dist/InputTypes.js.map +1 -0
- package/dist/Registry.d.ts +70 -0
- package/dist/Registry.d.ts.map +1 -0
- package/dist/Registry.js +3 -0
- package/dist/Registry.js.map +1 -0
- package/dist/Resolver.d.ts +30 -0
- package/dist/Resolver.d.ts.map +1 -0
- package/dist/Resolver.js +76 -0
- package/dist/Resolver.js.map +1 -0
- package/dist/Results.d.ts +20 -0
- package/dist/Results.d.ts.map +1 -0
- package/dist/Results.js +79 -0
- package/dist/Results.js.map +1 -0
- package/dist/ScalarBoundary.d.ts +9 -0
- package/dist/ScalarBoundary.d.ts.map +1 -0
- package/dist/ScalarBoundary.js +31 -0
- package/dist/ScalarBoundary.js.map +1 -0
- package/dist/Scalars.d.ts +9 -0
- package/dist/Scalars.d.ts.map +1 -0
- package/dist/Scalars.js +104 -0
- package/dist/Scalars.js.map +1 -0
- package/dist/SchemaTypes.d.ts +55 -0
- package/dist/SchemaTypes.d.ts.map +1 -0
- package/dist/SchemaTypes.js +47 -0
- package/dist/SchemaTypes.js.map +1 -0
- package/dist/Subscription.d.ts +6 -0
- package/dist/Subscription.d.ts.map +1 -0
- package/dist/Subscription.js +60 -0
- package/dist/Subscription.js.map +1 -0
- package/dist/Testing.d.ts +52 -0
- package/dist/Testing.d.ts.map +1 -0
- package/dist/Testing.js +74 -0
- package/dist/Testing.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/SubscriptionRule.d.ts +8 -0
- package/dist/internal/SubscriptionRule.d.ts.map +1 -0
- package/dist/internal/SubscriptionRule.js +73 -0
- package/dist/internal/SubscriptionRule.js.map +1 -0
- package/package.json +55 -4
- package/src/Assembly.ts +352 -0
- package/src/Builder.ts +110 -0
- package/src/Connections.ts +374 -0
- package/src/Definitions.ts +465 -0
- package/src/Errors.ts +188 -0
- package/src/Execution.ts +354 -0
- package/src/FieldRuntime.ts +32 -0
- package/src/FieldTypes.ts +667 -0
- package/src/Fields.ts +117 -0
- package/src/Formatting.ts +54 -0
- package/src/GraphQL.ts +50 -0
- package/src/InputTypes.ts +266 -0
- package/src/Registry.ts +157 -0
- package/src/Resolver.ts +176 -0
- package/src/Results.ts +128 -0
- package/src/ScalarBoundary.ts +30 -0
- package/src/Scalars.ts +143 -0
- package/src/SchemaTypes.ts +90 -0
- package/src/Subscription.ts +93 -0
- package/src/Testing.ts +144 -0
- package/src/index.ts +1 -0
- package/src/internal/SubscriptionRule.ts +90 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { GraphQLError, Kind, isAbstractType, typeFromAST, } from "graphql";
|
|
2
|
+
/**
|
|
3
|
+
* Replaces graphql-js's SingleFieldSubscriptionsRule, which evaluates root
|
|
4
|
+
* skip/include without variables and throws for a variable condition. Collect
|
|
5
|
+
* root response keys through fragments and reject root conditionals instead.
|
|
6
|
+
*/
|
|
7
|
+
export const singleRootFieldSubscriptionsRule = (context) => ({
|
|
8
|
+
OperationDefinition(operation) {
|
|
9
|
+
if (operation.operation !== "subscription")
|
|
10
|
+
return;
|
|
11
|
+
const schema = context.getSchema();
|
|
12
|
+
const root = schema.getSubscriptionType();
|
|
13
|
+
if (!root)
|
|
14
|
+
return;
|
|
15
|
+
const fields = new Map();
|
|
16
|
+
const visited = new Set();
|
|
17
|
+
const forbidden = [];
|
|
18
|
+
const matches = (fragment) => {
|
|
19
|
+
if (!fragment.typeCondition)
|
|
20
|
+
return true;
|
|
21
|
+
const type = typeFromAST(schema, fragment.typeCondition);
|
|
22
|
+
return (type === root || (isAbstractType(type) && schema.isSubType(type, root)));
|
|
23
|
+
};
|
|
24
|
+
const collect = (selectionSet) => {
|
|
25
|
+
for (const selection of selectionSet.selections) {
|
|
26
|
+
// Validation cannot evaluate variables. Like GraphQL 17, root selections are
|
|
27
|
+
// unconditional and skip/include are forbidden; nested fields are not visited.
|
|
28
|
+
for (const directive of selection.directives ?? [])
|
|
29
|
+
if (directive.name.value === "skip" ||
|
|
30
|
+
directive.name.value === "include")
|
|
31
|
+
forbidden.push(directive);
|
|
32
|
+
if (selection.kind === Kind.FIELD) {
|
|
33
|
+
const key = selection.alias?.value ?? selection.name.value;
|
|
34
|
+
const group = fields.get(key);
|
|
35
|
+
if (group)
|
|
36
|
+
group.push(selection);
|
|
37
|
+
else
|
|
38
|
+
fields.set(key, [selection]);
|
|
39
|
+
}
|
|
40
|
+
else if (selection.kind === Kind.INLINE_FRAGMENT) {
|
|
41
|
+
if (matches(selection))
|
|
42
|
+
collect(selection.selectionSet);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const name = selection.name.value;
|
|
46
|
+
if (visited.has(name))
|
|
47
|
+
continue;
|
|
48
|
+
visited.add(name);
|
|
49
|
+
const fragment = context.getFragment(name);
|
|
50
|
+
// Standard validation owns missing/cyclic fragments; this traversal must terminate.
|
|
51
|
+
if (fragment && matches(fragment))
|
|
52
|
+
collect(fragment.selectionSet);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
collect(operation.selectionSet);
|
|
57
|
+
const name = operation.name
|
|
58
|
+
? `Subscription "${operation.name.value}"`
|
|
59
|
+
: "Anonymous Subscription";
|
|
60
|
+
if (forbidden.length > 0) {
|
|
61
|
+
context.reportError(new GraphQLError(`${name} must not use \`@skip\` or \`@include\` directives in the top level selection.`, { nodes: forbidden }));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (fields.size > 1)
|
|
65
|
+
context.reportError(new GraphQLError(`${name} must select only one top level field.`, {
|
|
66
|
+
nodes: [...fields.values()].slice(1).flat(),
|
|
67
|
+
}));
|
|
68
|
+
for (const nodes of fields.values())
|
|
69
|
+
if (nodes[0].name.value.startsWith("__"))
|
|
70
|
+
context.reportError(new GraphQLError(`${name} must not select an introspection top level field.`, { nodes }));
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
//# sourceMappingURL=SubscriptionRule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubscriptionRule.js","sourceRoot":"","sources":["../../src/internal/SubscriptionRule.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@model-ts/graphql",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "7.1.0",
|
|
4
|
+
"description": "Effect-native GraphQL schemas, typed model registration and resolver services.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"effect",
|
|
7
|
+
"graphql",
|
|
8
|
+
"model"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": "Fabian Finke <finkef@icloud.com>",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/finkef/model-ts.git",
|
|
15
|
+
"directory": "packages/graphql"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./*": {
|
|
31
|
+
"types": "./dist/*.d.ts",
|
|
32
|
+
"default": "./dist/*.js"
|
|
33
|
+
},
|
|
34
|
+
"./internal/*": null,
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -b",
|
|
39
|
+
"test": "vitest run --project graphql --root ../..",
|
|
40
|
+
"test:compiler": "bun test/compiler.ts",
|
|
41
|
+
"test:scale": "bun test/scale.ts",
|
|
42
|
+
"test:types": "vitest run --project graphql:types --root ../.."
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@model-ts/core": "*",
|
|
46
|
+
"@model-ts/dynamodb": "*",
|
|
47
|
+
"effect": "4.0.5",
|
|
48
|
+
"graphql": "16.11.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"effect": "^4.0.5",
|
|
52
|
+
"graphql": "^16.11.0"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/Assembly.ts
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import type { Schema } from "effect"
|
|
2
|
+
import {
|
|
3
|
+
GraphQLBoolean,
|
|
4
|
+
GraphQLFloat,
|
|
5
|
+
GraphQLID,
|
|
6
|
+
GraphQLInt,
|
|
7
|
+
GraphQLList,
|
|
8
|
+
GraphQLNonNull,
|
|
9
|
+
GraphQLObjectType,
|
|
10
|
+
GraphQLScalarType,
|
|
11
|
+
GraphQLEnumType,
|
|
12
|
+
GraphQLInterfaceType,
|
|
13
|
+
GraphQLSchema,
|
|
14
|
+
GraphQLString,
|
|
15
|
+
GraphQLUnionType,
|
|
16
|
+
validateSchema,
|
|
17
|
+
type GraphQLNamedType,
|
|
18
|
+
type GraphQLFieldConfigMap,
|
|
19
|
+
} from "graphql"
|
|
20
|
+
import { makeConnectionTypes } from "./Connections.ts"
|
|
21
|
+
import {
|
|
22
|
+
mappedEnumeration,
|
|
23
|
+
type EnumOptions,
|
|
24
|
+
type NamedDefinition,
|
|
25
|
+
type AbstractOptions,
|
|
26
|
+
} from "./Definitions.ts"
|
|
27
|
+
import { describe } from "./FieldRuntime.ts"
|
|
28
|
+
import {
|
|
29
|
+
type BuildContext,
|
|
30
|
+
type RuntimeField,
|
|
31
|
+
type RequestContext,
|
|
32
|
+
} from "./FieldTypes.ts"
|
|
33
|
+
import { InputTypes } from "./InputTypes.ts"
|
|
34
|
+
import { makeResultTypes } from "./Results.ts"
|
|
35
|
+
import { guardScalar } from "./ScalarBoundary.ts"
|
|
36
|
+
import { dateTime, scalar, jsonScalar, enumeration } from "./Scalars.ts"
|
|
37
|
+
import { invalid, unwrap } from "./SchemaTypes.ts"
|
|
38
|
+
|
|
39
|
+
export interface Collected {
|
|
40
|
+
readonly name: string
|
|
41
|
+
readonly description?: string
|
|
42
|
+
readonly fields: Record<string, RuntimeField>
|
|
43
|
+
readonly interfaces?: readonly string[]
|
|
44
|
+
readonly kind?: "object" | "interface"
|
|
45
|
+
readonly resolveType?: (value: unknown) => string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AnyDefinition {
|
|
49
|
+
readonly "~kind":
|
|
50
|
+
| "object"
|
|
51
|
+
| "interface"
|
|
52
|
+
| "query"
|
|
53
|
+
| "mutation"
|
|
54
|
+
| "subscription"
|
|
55
|
+
readonly "~name"?: string
|
|
56
|
+
readonly "~model"?: Schema.Top
|
|
57
|
+
readonly "~implements"?: readonly string[]
|
|
58
|
+
readonly "~resolveType"?: (value: unknown) => string | null
|
|
59
|
+
readonly "~description"?: string
|
|
60
|
+
readonly fields: Readonly<Record<string, RuntimeField>>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isDefinition(value: unknown): value is AnyDefinition {
|
|
64
|
+
return (
|
|
65
|
+
typeof value === "object" &&
|
|
66
|
+
value !== null &&
|
|
67
|
+
"~kind" in value &&
|
|
68
|
+
"fields" in value &&
|
|
69
|
+
typeof value.fields === "object"
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isFieldMap(
|
|
74
|
+
value: unknown,
|
|
75
|
+
): value is Readonly<Record<string, RuntimeField>> {
|
|
76
|
+
return (
|
|
77
|
+
typeof value === "object" &&
|
|
78
|
+
value !== null &&
|
|
79
|
+
Object.values(value).every(
|
|
80
|
+
(field) =>
|
|
81
|
+
typeof field === "object" &&
|
|
82
|
+
field !== null &&
|
|
83
|
+
"build" in field &&
|
|
84
|
+
typeof field.build === "function",
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function assemble(
|
|
90
|
+
module: Readonly<Record<string, unknown>>,
|
|
91
|
+
): GraphQLSchema {
|
|
92
|
+
const objects = new Map<string, Collected>()
|
|
93
|
+
const roots = new Map<string, Collected>([
|
|
94
|
+
["Query", { name: "Query", fields: {} }],
|
|
95
|
+
["Mutation", { name: "Mutation", fields: {} }],
|
|
96
|
+
["Subscription", { name: "Subscription", fields: {} }],
|
|
97
|
+
])
|
|
98
|
+
for (const [key, value] of Object.entries(module)) {
|
|
99
|
+
if (!isDefinition(value)) continue
|
|
100
|
+
const fields = value.fields
|
|
101
|
+
if (!isFieldMap(fields))
|
|
102
|
+
return invalid(
|
|
103
|
+
`${key}: fields must be an object of builder field helpers such as GraphQL.string()`,
|
|
104
|
+
)
|
|
105
|
+
if (value["~kind"] === "object" || value["~kind"] === "interface") {
|
|
106
|
+
const name = value["~name"] ?? invalid(`${key} has no GraphQL name`)
|
|
107
|
+
if (objects.has(name)) invalid(`Duplicate GraphQL object: ${name}`)
|
|
108
|
+
const description = value["~description"]
|
|
109
|
+
objects.set(name, {
|
|
110
|
+
name,
|
|
111
|
+
...(value["~resolveType"]
|
|
112
|
+
? { resolveType: value["~resolveType"] }
|
|
113
|
+
: {}),
|
|
114
|
+
kind: value["~kind"],
|
|
115
|
+
interfaces: value["~implements"] ?? [],
|
|
116
|
+
fields: { ...fields },
|
|
117
|
+
...(description === undefined ? {} : { description }),
|
|
118
|
+
})
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
const root = roots.get(
|
|
122
|
+
value["~kind"] === "query"
|
|
123
|
+
? "Query"
|
|
124
|
+
: value["~kind"] === "mutation"
|
|
125
|
+
? "Mutation"
|
|
126
|
+
: "Subscription",
|
|
127
|
+
)!
|
|
128
|
+
for (const [field, config] of Object.entries(fields)) {
|
|
129
|
+
if (Object.hasOwn(root.fields, field))
|
|
130
|
+
invalid(`Duplicate field: ${root.name}.${field}`)
|
|
131
|
+
root.fields[field] = config
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const named = new Map<string, GraphQLNamedType>([
|
|
136
|
+
["ID", GraphQLID],
|
|
137
|
+
["String", GraphQLString],
|
|
138
|
+
["Boolean", GraphQLBoolean],
|
|
139
|
+
["Int", GraphQLInt],
|
|
140
|
+
["Float", GraphQLFloat],
|
|
141
|
+
])
|
|
142
|
+
const add = <T extends GraphQLNamedType>(type: T): T => {
|
|
143
|
+
if (named.has(type.name)) invalid(`Duplicate GraphQL name: ${type.name}`)
|
|
144
|
+
named.set(type.name, type)
|
|
145
|
+
return type
|
|
146
|
+
}
|
|
147
|
+
const inputs = new InputTypes(named, add)
|
|
148
|
+
for (const value of Object.values(module)) {
|
|
149
|
+
if (typeof value !== "object" || value === null || !("~kind" in value))
|
|
150
|
+
continue
|
|
151
|
+
if (value["~kind"] === "scalar" || value["~kind"] === "enum") {
|
|
152
|
+
// Named constructors store the schema and name together; assembly erases
|
|
153
|
+
// their heterogeneous value types only after checking the definition kind.
|
|
154
|
+
const def = value as NamedDefinition<
|
|
155
|
+
"scalar" | "enum",
|
|
156
|
+
string,
|
|
157
|
+
string
|
|
158
|
+
> & {
|
|
159
|
+
readonly "~scalar"?: GraphQLScalarType
|
|
160
|
+
readonly "~enum"?: EnumOptions<string>
|
|
161
|
+
}
|
|
162
|
+
const type = add(
|
|
163
|
+
def["~kind"] === "scalar"
|
|
164
|
+
? guardScalar(
|
|
165
|
+
def["~scalar"] ??
|
|
166
|
+
scalar(
|
|
167
|
+
def["~name"],
|
|
168
|
+
def["~model"] as Schema.Codec<string, unknown>,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
: def["~enum"]
|
|
172
|
+
? mappedEnumeration(def["~name"], def["~model"], def["~enum"])
|
|
173
|
+
: enumeration(def["~name"], def["~model"]),
|
|
174
|
+
)
|
|
175
|
+
inputs.register(def["~model"].ast, type)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const objectType = (name: string, coordinate: string): GraphQLObjectType => {
|
|
179
|
+
const type = named.get(name) ?? derived(name)
|
|
180
|
+
return type instanceof GraphQLObjectType
|
|
181
|
+
? type
|
|
182
|
+
: invalid(`Missing object ${name} at ${coordinate}`)
|
|
183
|
+
}
|
|
184
|
+
const nodeType = (name: string, coordinate: string) => {
|
|
185
|
+
const type = named.get(name)
|
|
186
|
+
return type instanceof GraphQLObjectType ||
|
|
187
|
+
type instanceof GraphQLInterfaceType ||
|
|
188
|
+
type instanceof GraphQLUnionType
|
|
189
|
+
? type
|
|
190
|
+
: invalid(
|
|
191
|
+
`Missing connection node ${name} at ${coordinate}; register an object, interface or union`,
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
const { derived, connectionType } = makeConnectionTypes(
|
|
195
|
+
named,
|
|
196
|
+
add,
|
|
197
|
+
nodeType,
|
|
198
|
+
(name) => {
|
|
199
|
+
const type = named.get(name)
|
|
200
|
+
return (
|
|
201
|
+
type instanceof GraphQLObjectType ||
|
|
202
|
+
type instanceof GraphQLInterfaceType ||
|
|
203
|
+
type instanceof GraphQLUnionType
|
|
204
|
+
)
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const context: BuildContext = {
|
|
209
|
+
input: inputs,
|
|
210
|
+
output(ref, coordinate) {
|
|
211
|
+
if (typeof ref !== "string") {
|
|
212
|
+
const inner = context.output(ref.of, coordinate)
|
|
213
|
+
return ref.kind === "list"
|
|
214
|
+
? new GraphQLNonNull(new GraphQLList(inner))
|
|
215
|
+
: unwrap(inner)
|
|
216
|
+
}
|
|
217
|
+
const scalar =
|
|
218
|
+
named.get(ref) ??
|
|
219
|
+
(ref === "DateTime"
|
|
220
|
+
? add(dateTime())
|
|
221
|
+
: ref === "JSON"
|
|
222
|
+
? add(jsonScalar())
|
|
223
|
+
: undefined)
|
|
224
|
+
return new GraphQLNonNull(
|
|
225
|
+
scalar instanceof GraphQLScalarType ||
|
|
226
|
+
scalar instanceof GraphQLEnumType ||
|
|
227
|
+
scalar instanceof GraphQLUnionType ||
|
|
228
|
+
scalar instanceof GraphQLInterfaceType
|
|
229
|
+
? scalar
|
|
230
|
+
: objectType(ref, coordinate),
|
|
231
|
+
)
|
|
232
|
+
},
|
|
233
|
+
result: makeResultTypes(inputs, add, objectType, named),
|
|
234
|
+
connection(node, name, coordinate, counted, edgeName) {
|
|
235
|
+
nodeType(node, coordinate)
|
|
236
|
+
return new GraphQLNonNull(
|
|
237
|
+
connectionType(
|
|
238
|
+
node,
|
|
239
|
+
name ?? `${node}${counted ? "Counted" : ""}Connection`,
|
|
240
|
+
counted,
|
|
241
|
+
edgeName,
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
const fieldConfigs = ({ fields, name, interfaces = [] }: Collected) => {
|
|
247
|
+
const inherited: GraphQLFieldConfigMap<unknown, RequestContext> = {}
|
|
248
|
+
for (const name of interfaces) {
|
|
249
|
+
const definition = named.get(name)
|
|
250
|
+
if (!(definition instanceof GraphQLInterfaceType))
|
|
251
|
+
return invalid(`Missing interface ${name}`)
|
|
252
|
+
// Reuse built configs so inherited result unions retain their interface's type identity.
|
|
253
|
+
Object.assign(inherited, definition.toConfig().fields)
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
...inherited,
|
|
257
|
+
...Object.fromEntries(
|
|
258
|
+
Object.entries(fields).map(([field, config]) => [
|
|
259
|
+
field,
|
|
260
|
+
config.build(context, field, `${name}.${field}`),
|
|
261
|
+
]),
|
|
262
|
+
),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const build = (object: Collected) =>
|
|
267
|
+
add(
|
|
268
|
+
new GraphQLObjectType({
|
|
269
|
+
name: object.name,
|
|
270
|
+
...describe(object),
|
|
271
|
+
fields: () => fieldConfigs(object),
|
|
272
|
+
interfaces: () =>
|
|
273
|
+
(object.interfaces ?? []).map((name) => {
|
|
274
|
+
const type = named.get(name)
|
|
275
|
+
return type instanceof GraphQLInterfaceType
|
|
276
|
+
? type
|
|
277
|
+
: invalid(`Missing interface ${name}`)
|
|
278
|
+
}),
|
|
279
|
+
}),
|
|
280
|
+
)
|
|
281
|
+
for (const object of objects.values()) {
|
|
282
|
+
if (object.kind === "interface")
|
|
283
|
+
add(
|
|
284
|
+
new GraphQLInterfaceType({
|
|
285
|
+
name: object.name,
|
|
286
|
+
...describe(object),
|
|
287
|
+
fields: () => fieldConfigs(object),
|
|
288
|
+
resolveType: (value) => {
|
|
289
|
+
if (object.resolveType)
|
|
290
|
+
return object.resolveType(value) ?? undefined
|
|
291
|
+
if (typeof value !== "object" || value === null) return undefined
|
|
292
|
+
const name: unknown =
|
|
293
|
+
Reflect.get(value, "__typename") ?? Reflect.get(value, "_tag")
|
|
294
|
+
return typeof name === "string" ? name : undefined
|
|
295
|
+
},
|
|
296
|
+
}),
|
|
297
|
+
)
|
|
298
|
+
else build(object)
|
|
299
|
+
}
|
|
300
|
+
for (const value of Object.values(module)) {
|
|
301
|
+
if (
|
|
302
|
+
typeof value !== "object" ||
|
|
303
|
+
value === null ||
|
|
304
|
+
!("~kind" in value) ||
|
|
305
|
+
value["~kind"] !== "union"
|
|
306
|
+
)
|
|
307
|
+
continue
|
|
308
|
+
// union() validates member registrations before storing this metadata.
|
|
309
|
+
const def = value as NamedDefinition<"union", string, unknown> & {
|
|
310
|
+
readonly options: AbstractOptions<unknown, readonly string[]>
|
|
311
|
+
}
|
|
312
|
+
add(
|
|
313
|
+
new GraphQLUnionType({
|
|
314
|
+
name: def["~name"],
|
|
315
|
+
...describe(def.options),
|
|
316
|
+
types: () =>
|
|
317
|
+
def.options.members.map((name) => objectType(name, def["~name"])),
|
|
318
|
+
resolveType: (value) => {
|
|
319
|
+
const name: unknown = def.options.resolveType
|
|
320
|
+
? def.options.resolveType(value)
|
|
321
|
+
: (Reflect.get(Object(value), "__typename") ??
|
|
322
|
+
Reflect.get(Object(value), "_tag"))
|
|
323
|
+
return typeof name === "string" && def.options.members.includes(name)
|
|
324
|
+
? name
|
|
325
|
+
: undefined
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
const query = build(roots.get("Query")!)
|
|
331
|
+
const mutationRoot = roots.get("Mutation")!
|
|
332
|
+
const mutation =
|
|
333
|
+
Object.keys(mutationRoot.fields).length > 0
|
|
334
|
+
? build(mutationRoot)
|
|
335
|
+
: undefined
|
|
336
|
+
if (Object.keys(roots.get("Query")!.fields).length === 0)
|
|
337
|
+
invalid("Missing Query fields")
|
|
338
|
+
const subscriptionRoot = roots.get("Subscription")!
|
|
339
|
+
const subscription = Object.keys(subscriptionRoot.fields).length
|
|
340
|
+
? build(subscriptionRoot)
|
|
341
|
+
: undefined
|
|
342
|
+
const schema = new GraphQLSchema({
|
|
343
|
+
query,
|
|
344
|
+
...(subscription ? { subscription } : {}),
|
|
345
|
+
...(mutation ? { mutation } : {}),
|
|
346
|
+
types: [...named.values()],
|
|
347
|
+
})
|
|
348
|
+
const problems = validateSchema(schema)
|
|
349
|
+
if (problems.length)
|
|
350
|
+
invalid(problems.map((error) => error.message).join("\n"))
|
|
351
|
+
return schema
|
|
352
|
+
}
|
package/src/Builder.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Effect, Stream } from "effect"
|
|
2
|
+
import { assemble } from "./Assembly.ts"
|
|
3
|
+
import {
|
|
4
|
+
makeDefinitions,
|
|
5
|
+
type DefinitionBuilder,
|
|
6
|
+
type CheckedInterfaces,
|
|
7
|
+
} from "./Definitions.ts"
|
|
8
|
+
import { type ErrorPolicyOptions, type ErrorPolicy } from "./Errors.ts"
|
|
9
|
+
import {
|
|
10
|
+
schemaRequirements,
|
|
11
|
+
schemaErrorPolicy,
|
|
12
|
+
type BuiltSchema,
|
|
13
|
+
type ExecutionResult,
|
|
14
|
+
type Request,
|
|
15
|
+
type ContextArgument,
|
|
16
|
+
subscribe,
|
|
17
|
+
execute,
|
|
18
|
+
} from "./Execution.ts"
|
|
19
|
+
import { makeFields } from "./Fields.ts"
|
|
20
|
+
import { type Fields, type Requirements } from "./FieldTypes.ts"
|
|
21
|
+
import { type Complete, type CompatibleContexts } from "./Registry.ts"
|
|
22
|
+
import { type ErrorSchema, type ErrorsOf } from "./Results.ts"
|
|
23
|
+
import { input, defaulted, invalid } from "./SchemaTypes.ts"
|
|
24
|
+
|
|
25
|
+
export interface BuilderOptions {
|
|
26
|
+
/** Largest "first"/"last" a connection accepts. Defaults to 100. */
|
|
27
|
+
readonly maxPageSize?: number
|
|
28
|
+
/** Size for nodes-based slicing when neither first nor last is supplied. Defaults to 20 (capped at maxPageSize). */
|
|
29
|
+
readonly defaultPageSize?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Definitions, field helpers and execution for one registry and request context. */
|
|
33
|
+
export interface Builder<Types, G, C = {}>
|
|
34
|
+
extends DefinitionBuilder<Types, G, C>, Fields<Types, G, C> {
|
|
35
|
+
input: typeof input
|
|
36
|
+
default: typeof defaulted
|
|
37
|
+
/** Errors any resolver may fail with, and how each reaches the response. */
|
|
38
|
+
withErrorPolicy<
|
|
39
|
+
const I extends readonly ErrorSchema[] = readonly [],
|
|
40
|
+
const P extends readonly ErrorSchema[] = readonly [],
|
|
41
|
+
>(
|
|
42
|
+
policy: ErrorPolicyOptions<I, P>,
|
|
43
|
+
): Builder<Types, G | ErrorsOf<I> | ErrorsOf<P>, C>
|
|
44
|
+
schema<const M extends Readonly<Record<string, unknown>>>(
|
|
45
|
+
module: M &
|
|
46
|
+
Complete<Types, M> &
|
|
47
|
+
CompatibleContexts<M, C> &
|
|
48
|
+
CheckedInterfaces<Types, M>,
|
|
49
|
+
): BuiltSchema<Requirements<M>, C>
|
|
50
|
+
/** The request context is per request: it reaches every resolver of this execution and nothing else. */
|
|
51
|
+
subscribe<R>(
|
|
52
|
+
schema: BuiltSchema<R, C>,
|
|
53
|
+
request: Request,
|
|
54
|
+
...context: ContextArgument<C>
|
|
55
|
+
): Stream.Stream<ExecutionResult, never, R>
|
|
56
|
+
execute<R>(
|
|
57
|
+
schema: BuiltSchema<R, C>,
|
|
58
|
+
request: Request,
|
|
59
|
+
...context: ContextArgument<C>
|
|
60
|
+
): Effect.Effect<ExecutionResult, never, R>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createBuilder<Types, C = {}>(
|
|
64
|
+
options: BuilderOptions = {},
|
|
65
|
+
): Builder<Types, never, C> {
|
|
66
|
+
const max = options.maxPageSize ?? 100
|
|
67
|
+
if (!Number.isSafeInteger(max) || max < 0)
|
|
68
|
+
invalid("maxPageSize must be a non-negative safe integer")
|
|
69
|
+
const size = options.defaultPageSize ?? Math.min(20, max)
|
|
70
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > max)
|
|
71
|
+
invalid(
|
|
72
|
+
"defaultPageSize must be a non-negative safe integer no larger than maxPageSize",
|
|
73
|
+
)
|
|
74
|
+
return make<Types, never, C>({ internal: [], public: [] }, max, size)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function make<Types, G, C>(
|
|
78
|
+
policy: ErrorPolicy,
|
|
79
|
+
maxPageSize: number,
|
|
80
|
+
defaultPageSize: number,
|
|
81
|
+
): Builder<Types, G, C> {
|
|
82
|
+
const { fields, rootField } = makeFields<Types, G, C>(
|
|
83
|
+
policy,
|
|
84
|
+
maxPageSize,
|
|
85
|
+
defaultPageSize,
|
|
86
|
+
)
|
|
87
|
+
return {
|
|
88
|
+
...fields,
|
|
89
|
+
...makeDefinitions<Types, G, C>(rootField),
|
|
90
|
+
input,
|
|
91
|
+
default: defaulted,
|
|
92
|
+
withErrorPolicy: (options) =>
|
|
93
|
+
make(
|
|
94
|
+
{
|
|
95
|
+
internal: [...policy.internal, ...(options.internal ?? [])],
|
|
96
|
+
public: [...policy.public, ...(options.public ?? [])],
|
|
97
|
+
publicCodes: { ...policy.publicCodes, ...options.publicCodes },
|
|
98
|
+
},
|
|
99
|
+
maxPageSize,
|
|
100
|
+
defaultPageSize,
|
|
101
|
+
),
|
|
102
|
+
schema: (module) => ({
|
|
103
|
+
schema: assemble(module),
|
|
104
|
+
[schemaErrorPolicy]: policy,
|
|
105
|
+
[schemaRequirements]: undefined,
|
|
106
|
+
}),
|
|
107
|
+
subscribe,
|
|
108
|
+
execute,
|
|
109
|
+
}
|
|
110
|
+
}
|