@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +1280 -2
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/Assembly.d.ts +24 -0
  5. package/dist/Assembly.d.ts.map +1 -0
  6. package/dist/Assembly.js +236 -0
  7. package/dist/Assembly.js.map +1 -0
  8. package/dist/Builder.d.ts +27 -0
  9. package/dist/Builder.d.ts.map +1 -0
  10. package/dist/Builder.js +40 -0
  11. package/dist/Builder.js.map +1 -0
  12. package/dist/Connections.d.ts +41 -0
  13. package/dist/Connections.d.ts.map +1 -0
  14. package/dist/Connections.js +243 -0
  15. package/dist/Connections.js.map +1 -0
  16. package/dist/Definitions.d.ts +139 -0
  17. package/dist/Definitions.d.ts.map +1 -0
  18. package/dist/Definitions.js +117 -0
  19. package/dist/Definitions.js.map +1 -0
  20. package/dist/Errors.d.ts +39 -0
  21. package/dist/Errors.d.ts.map +1 -0
  22. package/dist/Errors.js +103 -0
  23. package/dist/Errors.js.map +1 -0
  24. package/dist/Execution.d.ts +52 -0
  25. package/dist/Execution.d.ts.map +1 -0
  26. package/dist/Execution.js +174 -0
  27. package/dist/Execution.js.map +1 -0
  28. package/dist/FieldRuntime.d.ts +5 -0
  29. package/dist/FieldRuntime.d.ts.map +1 -0
  30. package/dist/FieldRuntime.js +27 -0
  31. package/dist/FieldRuntime.js.map +1 -0
  32. package/dist/FieldTypes.d.ts +232 -0
  33. package/dist/FieldTypes.d.ts.map +1 -0
  34. package/dist/FieldTypes.js +6 -0
  35. package/dist/FieldTypes.js.map +1 -0
  36. package/dist/Fields.d.ts +8 -0
  37. package/dist/Fields.d.ts.map +1 -0
  38. package/dist/Fields.js +72 -0
  39. package/dist/Fields.js.map +1 -0
  40. package/dist/Formatting.d.ts +25 -0
  41. package/dist/Formatting.d.ts.map +1 -0
  42. package/dist/Formatting.js +29 -0
  43. package/dist/Formatting.js.map +1 -0
  44. package/dist/GraphQL.d.ts +10 -0
  45. package/dist/GraphQL.d.ts.map +1 -0
  46. package/dist/GraphQL.js +5 -0
  47. package/dist/GraphQL.js.map +1 -0
  48. package/dist/InputTypes.d.ts +27 -0
  49. package/dist/InputTypes.d.ts.map +1 -0
  50. package/dist/InputTypes.js +203 -0
  51. package/dist/InputTypes.js.map +1 -0
  52. package/dist/Registry.d.ts +70 -0
  53. package/dist/Registry.d.ts.map +1 -0
  54. package/dist/Registry.js +3 -0
  55. package/dist/Registry.js.map +1 -0
  56. package/dist/Resolver.d.ts +30 -0
  57. package/dist/Resolver.d.ts.map +1 -0
  58. package/dist/Resolver.js +76 -0
  59. package/dist/Resolver.js.map +1 -0
  60. package/dist/Results.d.ts +20 -0
  61. package/dist/Results.d.ts.map +1 -0
  62. package/dist/Results.js +79 -0
  63. package/dist/Results.js.map +1 -0
  64. package/dist/ScalarBoundary.d.ts +9 -0
  65. package/dist/ScalarBoundary.d.ts.map +1 -0
  66. package/dist/ScalarBoundary.js +31 -0
  67. package/dist/ScalarBoundary.js.map +1 -0
  68. package/dist/Scalars.d.ts +9 -0
  69. package/dist/Scalars.d.ts.map +1 -0
  70. package/dist/Scalars.js +104 -0
  71. package/dist/Scalars.js.map +1 -0
  72. package/dist/SchemaTypes.d.ts +55 -0
  73. package/dist/SchemaTypes.d.ts.map +1 -0
  74. package/dist/SchemaTypes.js +47 -0
  75. package/dist/SchemaTypes.js.map +1 -0
  76. package/dist/Subscription.d.ts +6 -0
  77. package/dist/Subscription.d.ts.map +1 -0
  78. package/dist/Subscription.js +60 -0
  79. package/dist/Subscription.js.map +1 -0
  80. package/dist/Testing.d.ts +52 -0
  81. package/dist/Testing.d.ts.map +1 -0
  82. package/dist/Testing.js +74 -0
  83. package/dist/Testing.js.map +1 -0
  84. package/dist/index.d.ts +2 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +2 -0
  87. package/dist/index.js.map +1 -0
  88. package/dist/internal/SubscriptionRule.d.ts +8 -0
  89. package/dist/internal/SubscriptionRule.d.ts.map +1 -0
  90. package/dist/internal/SubscriptionRule.js +73 -0
  91. package/dist/internal/SubscriptionRule.js.map +1 -0
  92. package/package.json +55 -4
  93. package/src/Assembly.ts +352 -0
  94. package/src/Builder.ts +110 -0
  95. package/src/Connections.ts +374 -0
  96. package/src/Definitions.ts +465 -0
  97. package/src/Errors.ts +188 -0
  98. package/src/Execution.ts +354 -0
  99. package/src/FieldRuntime.ts +32 -0
  100. package/src/FieldTypes.ts +667 -0
  101. package/src/Fields.ts +117 -0
  102. package/src/Formatting.ts +54 -0
  103. package/src/GraphQL.ts +50 -0
  104. package/src/InputTypes.ts +266 -0
  105. package/src/Registry.ts +157 -0
  106. package/src/Resolver.ts +176 -0
  107. package/src/Results.ts +128 -0
  108. package/src/ScalarBoundary.ts +30 -0
  109. package/src/Scalars.ts +143 -0
  110. package/src/SchemaTypes.ts +90 -0
  111. package/src/Subscription.ts +93 -0
  112. package/src/Testing.ts +144 -0
  113. package/src/index.ts +1 -0
  114. 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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -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
+ }