@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,465 @@
1
+ import type { Schema } from "effect"
2
+ import type { GraphQLScalarType } from "graphql"
3
+ import { GraphQLEnumType } from "graphql"
4
+ import { type AnyDefinition } from "./Assembly.ts"
5
+ import type { RuntimeRef, ConnectionRef, Field } from "./FieldTypes.ts"
6
+ import {
7
+ type FieldMap,
8
+ type ResolverFields,
9
+ type EnumFields,
10
+ type SubscriptionFields,
11
+ type CheckedFields,
12
+ type SourceIssue,
13
+ type SourceSlot,
14
+ } from "./FieldTypes.ts"
15
+ import { type Description, type Contextual } from "./FieldTypes.ts"
16
+ import {
17
+ type Tag,
18
+ type NamesOfKind,
19
+ type RegistrationCheck,
20
+ type CheckedMembers,
21
+ } from "./Registry.ts"
22
+ import { type DefinitionKind } from "./Registry.ts"
23
+ import type { AnyResolve, AnyConfig } from "./Resolver.ts"
24
+ import { input } from "./SchemaTypes.ts"
25
+ import { invalid, metadata } from "./SchemaTypes.ts"
26
+
27
+ export interface ObjectDefinition<
28
+ N extends string,
29
+ A,
30
+ F,
31
+ O extends ObjectOptions = {},
32
+ C = {},
33
+ > extends Contextual<C> {
34
+ readonly "~kind": "object"
35
+ readonly "~name": N
36
+ readonly "~model": Schema.Schema<A>
37
+ readonly "~description"?: string
38
+ readonly "~implements"?: O["implements"]
39
+ readonly fields: F
40
+ }
41
+
42
+ export interface NamedDefinition<
43
+ K extends DefinitionKind,
44
+ N extends string,
45
+ A,
46
+ > {
47
+ readonly "~kind": K
48
+ readonly "~name": N
49
+ readonly "~model": Schema.Schema<A>
50
+ }
51
+
52
+ export interface ScalarDefinition<
53
+ K extends "scalar" | "enum",
54
+ N extends string,
55
+ S extends Schema.Top,
56
+ > extends NamedDefinition<K, N, S["Type"]> {
57
+ readonly "~scalar"?: GraphQLScalarType
58
+ readonly schema: S["Rebuild"]
59
+ }
60
+
61
+ export interface InterfaceDefinition<N extends string, A, F, C = {}>
62
+ extends NamedDefinition<"interface", N, A>, Contextual<C> {
63
+ readonly "~description"?: string
64
+ readonly "~resolveType"?: (value: A) => string | null
65
+ readonly fields: F
66
+ }
67
+
68
+ export interface ObjectOptions extends Description {
69
+ readonly implements?: readonly string[]
70
+ }
71
+
72
+ export interface AbstractOptions<
73
+ A,
74
+ Members extends readonly string[],
75
+ > extends Description {
76
+ readonly members: Members
77
+ readonly resolveType?: (value: A) => NoInfer<Members[number]> | null
78
+ }
79
+
80
+ export interface UnionDefinition<
81
+ N extends string,
82
+ A,
83
+ Members extends readonly string[],
84
+ > extends NamedDefinition<"union", N, A> {
85
+ readonly options: AbstractOptions<A, Members>
86
+ }
87
+
88
+ export interface RootDefinition<
89
+ K extends "query" | "mutation" | "subscription",
90
+ F,
91
+ C = {},
92
+ > extends Contextual<C> {
93
+ readonly "~kind": K
94
+ readonly fields: F
95
+ }
96
+
97
+ /** Only the literal tag is read for implicit naming; output fields never inspect model ASTs. */
98
+ export function tagName(model: {
99
+ readonly fields?: { readonly _tag?: Schema.Constraint }
100
+ }): string {
101
+ const tag = model.fields?._tag?.ast
102
+ return tag?._tag === "Literal" && typeof tag.literal === "string"
103
+ ? tag.literal
104
+ : invalid("Object model needs a literal _tag; use an explicit name")
105
+ }
106
+
107
+ export interface InterfaceOptions<Types, A> extends Description {
108
+ readonly resolveType?: (value: A) => NamesOfKind<Types, "object"> | null
109
+ }
110
+
111
+ export type EnumValue<A> = A | ({ readonly value: A } & Description)
112
+ export interface EnumOptions<A> extends Description {
113
+ readonly values: Readonly<Record<string, EnumValue<A>>>
114
+ }
115
+
116
+ type MappedValues<V> = V[keyof V] extends infer A
117
+ ? A extends { readonly value: infer Value }
118
+ ? Value
119
+ : A
120
+ : never
121
+
122
+ type CheckedEnumValues<A, V> = {
123
+ readonly [K in keyof V]: [MappedValues<Pick<V, K>>] extends [A]
124
+ ? unknown
125
+ : "Use a literal from the registered enum model"
126
+ }
127
+
128
+ export type CompleteEnum<A, V> = [Exclude<A, MappedValues<V>>] extends [never]
129
+ ? unknown
130
+ : {
131
+ readonly error: `Missing enum values: ${Exclude<A, MappedValues<V>> & string}; map every model literal in values`
132
+ }
133
+
134
+ export interface EnumDefinition<
135
+ N extends string,
136
+ S extends Schema.Top,
137
+ > extends ScalarDefinition<"enum", N, S> {
138
+ readonly "~enum"?: EnumOptions<S["Type"]>
139
+ }
140
+
141
+ /** Explicit member names are checked against the corresponding model, in order. */
142
+ type NamedMembers<
143
+ Types,
144
+ M extends readonly Schema.Top[],
145
+ Names extends readonly string[],
146
+ > = Names["length"] extends M["length"]
147
+ ? {
148
+ readonly [I in keyof Names]: I extends keyof M
149
+ ? Names[I] & RegistrationCheck<Types, Names[I], M[I]["Type"], "object">
150
+ : never
151
+ }
152
+ : {
153
+ readonly error: "Provide one registered object name per union model, in the same order"
154
+ }
155
+
156
+ type Implemented<O> = O extends {
157
+ readonly implements: readonly (infer I extends string)[]
158
+ }
159
+ ? I
160
+ : never
161
+
162
+ /** Interface fields are known when the barrel is assembled, not at model registration. */
163
+ export type CheckedInterfaces<Types, M> = {
164
+ readonly [K in keyof M]: M[K] extends ObjectDefinition<
165
+ infer N,
166
+ infer A,
167
+ infer F,
168
+ infer O,
169
+ infer _C
170
+ >
171
+ ? {
172
+ readonly [I in Implemented<O>]: Extract<
173
+ M[keyof M],
174
+ { readonly "~kind": "interface"; readonly "~name": I }
175
+ > extends { readonly fields: infer IF }
176
+ ? {
177
+ readonly [P in Exclude<keyof IF, keyof F>]: [
178
+ SourceIssue<Types, A, P, IF[P]>,
179
+ ] extends [never]
180
+ ? IF[P] extends SourceSlot<A>
181
+ ? never
182
+ : P
183
+ : P
184
+ }[Exclude<keyof IF, keyof F>] extends infer Bad
185
+ ? [Bad] extends [never]
186
+ ? never
187
+ : `${N}.${Bad & string}: cannot inherit ${I} field; supply a compatible source property or override with a resolver`
188
+ : never
189
+ : never
190
+ }[Implemented<O>] extends infer Issue
191
+ ? [Issue] extends [never]
192
+ ? unknown
193
+ : { readonly error: Issue }
194
+ : never
195
+ : unknown
196
+ }
197
+
198
+ export function mappedEnumeration(
199
+ name: string,
200
+ schema: Schema.Schema<string>,
201
+ options: EnumOptions<string>,
202
+ ): GraphQLEnumType {
203
+ const ast = schema.ast
204
+ const literals = (ast._tag === "Union" ? ast.types : [ast]).map((value) =>
205
+ value._tag === "Literal" && typeof value.literal === "string"
206
+ ? value.literal
207
+ : invalid(`${name}: enums require string Schema.Literals`),
208
+ )
209
+ const values = Object.entries(options.values).map(([key, entry]) => {
210
+ const config = typeof entry === "string" ? { value: entry } : entry
211
+ if (!literals.includes(config.value))
212
+ invalid(`${name}.${key}: value must be a model literal`)
213
+ return [key, config] as const
214
+ })
215
+ if (
216
+ literals.some(
217
+ (literal) => !values.some(([, config]) => config.value === literal),
218
+ )
219
+ )
220
+ invalid(`${name}: map every model literal in values`)
221
+ // Aliases are ambiguous on serialization: require one external name per internal value.
222
+ if (new Set(values.map(([, config]) => config.value)).size !== values.length)
223
+ invalid(
224
+ `${name}: enum values must be unique; use one name per model literal`,
225
+ )
226
+ return new GraphQLEnumType({
227
+ name,
228
+ ...metadata(ast),
229
+ ...options,
230
+ values: Object.fromEntries(values),
231
+ })
232
+ }
233
+
234
+ export interface DefinitionBuilder<Types, G, C> {
235
+ object<
236
+ M extends Schema.Schema<{ readonly _tag: string }> & {
237
+ readonly fields: { readonly _tag: Schema.Constraint }
238
+ },
239
+ const F extends FieldMap<M["Type"], C>,
240
+ >(
241
+ model: M & RegistrationCheck<Types, Tag<M>, NoInfer<M["Type"]>, "object">,
242
+ fields: F & CheckedFields<Types, Tag<M>, M["Type"], NoInfer<F>>,
243
+ ): ObjectDefinition<Tag<M>, M["Type"], F, {}, C>
244
+ object<
245
+ M extends Schema.Schema<{ readonly _tag: string }> & {
246
+ readonly fields: { readonly _tag: Schema.Constraint }
247
+ },
248
+ const F extends FieldMap<M["Type"], C>,
249
+ const O extends ObjectOptions & {
250
+ readonly implements?: readonly NamesOfKind<Types, "interface">[]
251
+ },
252
+ >(
253
+ model: M & RegistrationCheck<Types, Tag<M>, NoInfer<M["Type"]>, "object">,
254
+ options: O,
255
+ fields: F & CheckedFields<Types, Tag<M>, M["Type"], NoInfer<F>>,
256
+ ): ObjectDefinition<Tag<M>, M["Type"], F, O, C>
257
+ object<
258
+ const N extends string,
259
+ A,
260
+ const F extends FieldMap<A, C>,
261
+ const O extends ObjectOptions & {
262
+ readonly implements?: readonly NamesOfKind<Types, "interface">[]
263
+ } = {},
264
+ >(
265
+ name: N & RegistrationCheck<Types, N, NoInfer<A>, "object">,
266
+ model: Schema.Schema<A>,
267
+ options: O,
268
+ fields: F & CheckedFields<Types, N, A, NoInfer<F>>,
269
+ ): ObjectDefinition<N, A, F, O, C>
270
+ interface<const N extends string, A, const F extends FieldMap<A, C>>(
271
+ name: N & RegistrationCheck<Types, N, NoInfer<A>, "interface">,
272
+ model: Schema.Schema<A>,
273
+ fields: F & CheckedFields<Types, N, A, NoInfer<F>>,
274
+ ): InterfaceDefinition<N, A, F, C>
275
+ interface<const N extends string, A, const F extends FieldMap<A, C>>(
276
+ name: N & RegistrationCheck<Types, N, NoInfer<A>, "interface">,
277
+ model: Schema.Schema<A>,
278
+ options: InterfaceOptions<Types, NoInfer<A>>,
279
+ fields: F & CheckedFields<Types, N, A, NoInfer<F>>,
280
+ ): InterfaceDefinition<N, A, F, C>
281
+ union<const N extends string, S extends Schema.Union<readonly Schema.Top[]>>(
282
+ name: N & RegistrationCheck<Types, N, NoInfer<S["Type"]>, "union">,
283
+ model: S & CheckedMembers<Types, NoInfer<S["members"]>>,
284
+ options?: Description & {
285
+ readonly resolveType?: (
286
+ value: S["Type"],
287
+ ) => NoInfer<Tag<S["members"][number]>> | null
288
+ },
289
+ ): UnionDefinition<N, S["Type"], readonly Tag<S["members"][number]>[]>
290
+ union<
291
+ const N extends string,
292
+ S extends Schema.Union<readonly Schema.Top[]>,
293
+ const Members extends readonly string[],
294
+ >(
295
+ name: N & RegistrationCheck<Types, N, NoInfer<S["Type"]>, "union">,
296
+ model: S,
297
+ options: AbstractOptions<NoInfer<S["Type"]>, Members> & {
298
+ readonly members: Members &
299
+ NamedMembers<Types, NoInfer<S["members"]>, Members>
300
+ },
301
+ ): UnionDefinition<N, S["Type"], Members>
302
+ scalar<
303
+ const N extends string,
304
+ S extends Schema.Top & Schema.Codec<unknown, unknown>,
305
+ >(
306
+ name: N & RegistrationCheck<Types, N, NoInfer<S["Type"]>, "scalar">,
307
+ codec: S,
308
+ scalarType?: GraphQLScalarType,
309
+ ): ScalarDefinition<"scalar", N, S>
310
+ enum<const N extends string, S extends Schema.Literals<readonly string[]>>(
311
+ name: N & RegistrationCheck<Types, N, NoInfer<S["Type"]>, "enum">,
312
+ schema: S,
313
+ ): EnumDefinition<N, S>
314
+ enum<
315
+ const N extends string,
316
+ S extends Schema.Literals<readonly string[]>,
317
+ const V extends Readonly<Record<string, EnumValue<string>>>,
318
+ >(
319
+ name: N & RegistrationCheck<Types, N, NoInfer<S["Type"]>, "enum">,
320
+ schema: S,
321
+ options: Description & {
322
+ readonly values: V &
323
+ CompleteEnum<NoInfer<S["Type"]>, V> &
324
+ CheckedEnumValues<NoInfer<S["Type"]>, V>
325
+ },
326
+ ): EnumDefinition<N, S>
327
+ readonly queryField: EnumFields<Types, C, "query"> &
328
+ ResolverFields<Types, G, C, "query">
329
+ readonly mutationField: EnumFields<Types, C, "mutation"> &
330
+ ResolverFields<Types, G, C, "mutation">
331
+ readonly subscriptionField: SubscriptionFields<Types, G, C, "subscription">
332
+ }
333
+
334
+ export function makeDefinitions<Types, G, C>(
335
+ rootField: (
336
+ ref: RuntimeRef | ConnectionRef<string>,
337
+ input: AnyResolve | AnyConfig,
338
+ ) => Field<unknown>,
339
+ ): DefinitionBuilder<Types, G, C> {
340
+ const object = (...args: readonly unknown[]) => {
341
+ const explicit = typeof args[0] === "string"
342
+ // Public overloads validate each positional argument before this boundary.
343
+ const model = (explicit ? args[1] : args[0]) as Schema.Top & {
344
+ readonly fields?: { readonly _tag?: Schema.Constraint }
345
+ }
346
+ const name = explicit ? args[0] : tagName(model)
347
+ const rest = args.slice(explicit ? 2 : 1)
348
+ const options = (rest.length === 1 ? {} : rest[0]) as ObjectOptions
349
+ return {
350
+ "~kind": "object",
351
+ "~name": name,
352
+ "~model": model,
353
+ "~implements": options.implements ?? [],
354
+ ...(options.description === undefined
355
+ ? {}
356
+ : { "~description": options.description }),
357
+ fields: rest.at(-1),
358
+ }
359
+ }
360
+ // The overloads enforce registration and field contracts before this erased
361
+ // construction boundary; the runtime stores their validated metadata unchanged.
362
+ return {
363
+ object,
364
+ interface: (
365
+ name: string,
366
+ model: Schema.Top,
367
+ ...rest:
368
+ | [AnyDefinition["fields"]]
369
+ | [
370
+ Description & {
371
+ readonly resolveType?: (value: unknown) => string | null
372
+ },
373
+ AnyDefinition["fields"],
374
+ ]
375
+ ) => {
376
+ const [options, fieldMap] =
377
+ rest.length === 1 ? [undefined, rest[0]] : rest
378
+ return {
379
+ "~kind": "interface",
380
+ "~name": name,
381
+ "~model": model,
382
+ "~description": options?.description,
383
+ "~resolveType": options?.resolveType,
384
+ fields: fieldMap,
385
+ }
386
+ },
387
+ union: (
388
+ name: string,
389
+ model: Schema.Union<readonly Schema.Top[]>,
390
+ options?: Description & {
391
+ readonly members?: readonly string[]
392
+ readonly resolveType?: (value: unknown) => string | null
393
+ },
394
+ ) => ({
395
+ "~kind": "union",
396
+ "~name": name,
397
+ "~model": model,
398
+ options: {
399
+ ...options,
400
+ members:
401
+ options?.members ??
402
+ model.members.map((member) =>
403
+ // The implicit-members overload requires tagged models; explicit members bypass this branch.
404
+ tagName(
405
+ member as Schema.Top & {
406
+ readonly fields?: { readonly _tag?: Schema.Constraint }
407
+ },
408
+ ),
409
+ ),
410
+ },
411
+ }),
412
+ scalar: (
413
+ name: string,
414
+ model: Schema.Top,
415
+ scalarType?: GraphQLScalarType,
416
+ ) => {
417
+ if (scalarType && scalarType.name !== name)
418
+ invalid(
419
+ `Scalar name ${scalarType.name} does not match ${name}; register the scalar under its existing name`,
420
+ )
421
+ return {
422
+ "~kind": "scalar",
423
+ "~name": name,
424
+ "~model": model,
425
+ schema: input(name, model),
426
+ "~scalar": scalarType,
427
+ }
428
+ },
429
+ enum: (
430
+ name: string,
431
+ model: Schema.Literals<readonly string[]>,
432
+ options?: EnumOptions<string>,
433
+ ) => ({
434
+ "~enum": options,
435
+ "~kind": "enum",
436
+ "~name": name,
437
+ "~model": model,
438
+ schema: input(name, model),
439
+ }),
440
+ queryField: (
441
+ name: string,
442
+ ref: RuntimeRef | ConnectionRef<string>,
443
+ input: AnyResolve | AnyConfig,
444
+ ) => ({
445
+ "~kind": "query",
446
+ fields: { [name]: rootField(ref, input) },
447
+ }),
448
+ mutationField: (
449
+ name: string,
450
+ ref: RuntimeRef | ConnectionRef<string>,
451
+ input: AnyResolve | AnyConfig,
452
+ ) => ({
453
+ "~kind": "mutation",
454
+ fields: { [name]: rootField(ref, input) },
455
+ }),
456
+ subscriptionField: (
457
+ name: string,
458
+ ref: RuntimeRef | ConnectionRef<string>,
459
+ input: AnyResolve | AnyConfig,
460
+ ) => ({
461
+ "~kind": "subscription",
462
+ fields: { [name]: rootField(ref, input) },
463
+ }),
464
+ } as unknown as DefinitionBuilder<Types, G, C>
465
+ }
package/src/Errors.ts ADDED
@@ -0,0 +1,188 @@
1
+ import { Cause, Context, Effect, Option, Schema } from "effect"
2
+ import { GraphQLError } from "graphql"
3
+ import {
4
+ type ErrorSchema,
5
+ type ErrorsOf,
6
+ errorMember,
7
+ errorName,
8
+ } from "./Results.ts"
9
+
10
+ export interface ErrorPolicyOptions<
11
+ I extends readonly ErrorSchema[],
12
+ P extends readonly ErrorSchema[],
13
+ > {
14
+ /** Infrastructure failures are always masked. */
15
+ readonly internal?: I
16
+ /** Only declared fields of these errors may reach clients. */
17
+ readonly public?: P
18
+ /** Preserve an existing API's codes; unspecified tags use their own name. */
19
+ readonly publicCodes?: [P[number]] extends [never]
20
+ ? never
21
+ : Partial<Record<ErrorsOf<NoInfer<P>>["_tag"], string>>
22
+ }
23
+
24
+ export interface ErrorPolicy {
25
+ readonly internal: readonly ErrorSchema[]
26
+ readonly public: readonly ErrorSchema[]
27
+ readonly publicCodes?: Readonly<Record<string, string>>
28
+ }
29
+
30
+ export const defaultErrorPolicy: ErrorPolicy = { internal: [], public: [] }
31
+
32
+ export interface ErrorReport {
33
+ /** Unmodified Effect cause, including defects and tracing annotations. */
34
+ readonly cause: Cause.Cause<unknown>
35
+ readonly coordinate: string
36
+ readonly phase: "resolve" | "authorize" | "subscribe" | "execute"
37
+ }
38
+
39
+ /** Optional per-runtime telemetry. Capture dependencies when constructing its layer. */
40
+ export class ErrorReporter extends Context.Service<
41
+ ErrorReporter,
42
+ {
43
+ readonly report: (event: ErrorReport) => Effect.Effect<void>
44
+ }
45
+ >()("@model-ts/graphql/ErrorReporter") {}
46
+
47
+ // A GraphQLError from user code is not permission to disclose its message.
48
+ // Only errors created by our boundaries, or declared in a policy, are trusted.
49
+ const trusted = new WeakSet<GraphQLError>()
50
+ export const isBoundaryError = (error: unknown): boolean =>
51
+ error instanceof GraphQLError && trusted.has(error)
52
+
53
+ export const fieldError = (
54
+ message: string,
55
+ extensions: Readonly<Record<string, unknown>>,
56
+ cause?: Cause.Cause<unknown>,
57
+ ): GraphQLError => {
58
+ const error = new GraphQLError(message, {
59
+ ...(cause ? { originalError: new Error(message, { cause }) } : {}),
60
+ extensions,
61
+ })
62
+ trusted.add(error)
63
+ return error
64
+ }
65
+
66
+ export const forbidden = (field: string): GraphQLError =>
67
+ fieldError(`You don't have permission to access field "${field}"`, {
68
+ code: "FORBIDDEN",
69
+ })
70
+
71
+ // Wrappers are not Errors (graphql-js treats Error values as failures). Keep the
72
+ // class instance private while the enumerable projection exposes only declared fields.
73
+ const resultInstances = new WeakMap<object, object>()
74
+ export const resultSource = (value: unknown): unknown =>
75
+ typeof value === "object" && value !== null
76
+ ? (resultInstances.get(value) ?? value)
77
+ : value
78
+
79
+ /** Maps declared typed failures; undefined means the failure must be masked. */
80
+ function visibleFailure(
81
+ error: unknown,
82
+ members: readonly ErrorSchema[],
83
+ policy: ErrorPolicy,
84
+ ): unknown {
85
+ if (isBoundaryError(error)) return error
86
+ if (typeof error !== "object" || error === null) return undefined
87
+ const matches = (schema: ErrorSchema) => Schema.is(schema)(error)
88
+ if (policy.internal.some(matches)) return undefined
89
+ const member = members.find(matches)
90
+ if (member) {
91
+ const wrapper = {
92
+ ...declaredFields(member, error),
93
+ [errorMember]: errorName(member),
94
+ }
95
+ resultInstances.set(wrapper, error)
96
+ return wrapper
97
+ }
98
+ const visible = policy.public.find(matches)
99
+ if (visible) {
100
+ const { _tag, message, ...extensions } = declaredFields(visible, error)
101
+ const tag = errorName(visible)
102
+ return fieldError(String(message ?? tag), {
103
+ ...extensions,
104
+ code: policy.publicCodes?.[tag] ?? tag,
105
+ })
106
+ }
107
+ return undefined
108
+ }
109
+
110
+ export const recoverCause = (
111
+ cause: Cause.Cause<unknown>,
112
+ members: readonly ErrorSchema[],
113
+ policy: ErrorPolicy,
114
+ location: Omit<ErrorReport, "cause">,
115
+ ): Effect.Effect<unknown> =>
116
+ Effect.gen(function* () {
117
+ // Do not turn cancellation into an error response or a telemetry incident.
118
+ if (Cause.hasInterruptsOnly(cause)) return yield* Effect.interrupt
119
+ const reason = cause.reasons.length === 1 ? cause.reasons[0] : undefined
120
+ const original =
121
+ reason?._tag === "Fail"
122
+ ? reason.error
123
+ : reason?._tag === "Die"
124
+ ? reason.defect
125
+ : undefined
126
+ const visible = visibleFailure(original, members, policy)
127
+ if (visible !== undefined) return visible
128
+ const reporter = yield* Effect.serviceOption(ErrorReporter)
129
+ if (Option.isSome(reporter)) {
130
+ // Telemetry must never replace the original failure or disclose its own defect.
131
+ yield* Effect.suspend(() =>
132
+ reporter.value.report({ cause, ...location }),
133
+ ).pipe(Effect.catchCause(() => Effect.void))
134
+ }
135
+ return fieldError(
136
+ "Internal server error",
137
+ { code: "INTERNAL_SERVER_ERROR" },
138
+ cause,
139
+ )
140
+ })
141
+
142
+ /** Handles both synchronous throws and Effect failures, before the Promise bridge. */
143
+ export const protect = (
144
+ evaluate: () => unknown,
145
+ members: readonly ErrorSchema[],
146
+ policy: ErrorPolicy,
147
+ location: Omit<ErrorReport, "cause">,
148
+ ): Effect.Effect<unknown, never, unknown> =>
149
+ Effect.suspend(() => {
150
+ const output = evaluate()
151
+ if (output instanceof Error) return Effect.fail(output)
152
+ return Effect.isEffect(output) ? output : Effect.succeed(output)
153
+ }).pipe(
154
+ Effect.flatMap((value) => {
155
+ if (value instanceof Error) return Effect.fail(value)
156
+ // JavaScript callers can bypass the type contract. A truthy Promise must
157
+ // never authorize a request or run outside the Effect cancellation boundary.
158
+ if (
159
+ value !== null &&
160
+ (typeof value === "object" || typeof value === "function") &&
161
+ "then" in value &&
162
+ typeof value.then === "function"
163
+ ) {
164
+ return Effect.die(
165
+ new Error(
166
+ "Return a value or Effect; wrap Promise APIs with Effect.tryPromise",
167
+ ),
168
+ )
169
+ }
170
+ return Effect.succeed(value)
171
+ }),
172
+ Effect.catchCause((cause) =>
173
+ recoverCause(cause, members, policy, location),
174
+ ),
175
+ Effect.withSpan(`GraphQL.${location.phase}`, {
176
+ attributes: { "graphql.field": location.coordinate },
177
+ }),
178
+ )
179
+
180
+ export function declaredFields(
181
+ schema: ErrorSchema,
182
+ error: object,
183
+ ): Record<string, unknown> {
184
+ // Error.message can be non-enumerable; never copy stack/cause/incidental fields.
185
+ return Object.fromEntries(
186
+ Object.keys(schema.fields).map((key) => [key, Reflect.get(error, key)]),
187
+ )
188
+ }