@model-ts/graphql 0.0.0-stage → 7.0.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 +21 -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,354 @@
1
+ import { formatResult, type FormattedExecutionResult } from "./Formatting.ts"
2
+ import { ScalarInputFailure } from "./ScalarBoundary.ts"
3
+ export type { FormattedExecutionResult } from "./Formatting.ts"
4
+ import type { Context } from "effect"
5
+ import { Cause, Effect, Stream } from "effect"
6
+ import {
7
+ GraphQLError,
8
+ type GraphQLSchema,
9
+ type DocumentNode,
10
+ execute as graphqlExecute,
11
+ subscribe as graphqlSubscribe,
12
+ parse,
13
+ validate,
14
+ specifiedRules,
15
+ SingleFieldSubscriptionsRule,
16
+ type ExecutionResult as GraphQLExecutionResult,
17
+ } from "graphql"
18
+ import {
19
+ type ErrorPolicy,
20
+ defaultErrorPolicy,
21
+ isBoundaryError,
22
+ recoverCause,
23
+ } from "./Errors.ts"
24
+ import { type RequestContext, type Contextual } from "./FieldTypes.ts"
25
+ import { singleRootFieldSubscriptionsRule } from "./internal/SubscriptionRule.ts"
26
+
27
+ // Replace the native rule: v15 counts selections, while v16 evaluates root
28
+ // directives without variables. Both execution paths must use the same semantics.
29
+ const validationRules = specifiedRules.map((rule) =>
30
+ rule === SingleFieldSubscriptionsRule
31
+ ? singleRootFieldSubscriptionsRule
32
+ : rule,
33
+ )
34
+
35
+ export const schemaRequirements: unique symbol = Symbol(
36
+ "GraphQL schema requirements",
37
+ )
38
+ export const schemaErrorPolicy: unique symbol = Symbol(
39
+ "GraphQL schema error policy",
40
+ )
41
+
42
+ export interface BuiltSchema<out R, C = {}> extends Contextual<C> {
43
+ readonly schema: GraphQLSchema
44
+ readonly [schemaRequirements]: R | undefined
45
+ readonly [schemaErrorPolicy]?: ErrorPolicy
46
+ }
47
+
48
+ export type ExecutionResult = GraphQLExecutionResult<
49
+ { readonly [key: string]: unknown },
50
+ { readonly [key: string]: unknown }
51
+ >
52
+
53
+ export interface Request {
54
+ readonly source: string
55
+ readonly variableValues?: { readonly [name: string]: unknown }
56
+ readonly operationName?: string
57
+ }
58
+
59
+ export type Json =
60
+ | null
61
+ | boolean
62
+ | number
63
+ | string
64
+ | readonly Json[]
65
+ | { readonly [key: string]: Json }
66
+
67
+ /** Structural transport contract: no dependency on a particular HTTP/WS adapter. */
68
+ export interface Execution<C> {
69
+ readonly document: DocumentNode
70
+ readonly request: {
71
+ readonly query: string
72
+ readonly variables?: Record<string, Json> | null
73
+ readonly operationName?: string | null
74
+ readonly extensions?: Record<string, Json> | null
75
+ }
76
+ readonly context: C
77
+ }
78
+
79
+ export interface Executor<C, E = never, R = never> {
80
+ /** Standard schema validation runs here so scalar failures use the executor error policy. */
81
+ readonly validation: "executor"
82
+ readonly schema: GraphQLSchema
83
+ readonly execute: (
84
+ input: Execution<C>,
85
+ ) => Effect.Effect<FormattedExecutionResult, E, R>
86
+ readonly subscribe: (
87
+ input: Execution<C>,
88
+ ) => Stream.Stream<FormattedExecutionResult, E, R>
89
+ }
90
+
91
+ /** Required when the builder declares a context type; optional for builders without one. */
92
+ export type ContextArgument<C> = {} extends C ? [context?: C] : [context: C]
93
+
94
+ function bridge<R>(
95
+ services: Context.Context<R>,
96
+ context: unknown,
97
+ signal?: AbortSignal,
98
+ pending?: Set<Promise<unknown>>,
99
+ sources?: Set<AsyncIterator<unknown, unknown>>,
100
+ ): RequestContext {
101
+ // The builder collects heterogeneous resolver requirements in BuiltSchema.R.
102
+ // Restore that union only here, when crossing into graphql-js's Promise API.
103
+ return {
104
+ context,
105
+ stream: (stream) => {
106
+ // GraphQL consumes one iterator per subscription. Own it before handing
107
+ // it off so closing the lifetime can join its actual source scope cleanup.
108
+ const source = Stream.toAsyncIterableWith(
109
+ stream as Stream.Stream<unknown, never, R>,
110
+ services,
111
+ )[Symbol.asyncIterator]()
112
+ sources?.add(source)
113
+ return { [Symbol.asyncIterator]: () => source }
114
+ },
115
+ run: (effect) => {
116
+ const promise = Effect.runPromiseWith(services)(
117
+ effect as Effect.Effect<unknown, never, R>,
118
+ { signal },
119
+ )
120
+ pending?.add(promise)
121
+ return promise.finally(() => pending?.delete(promise))
122
+ },
123
+ }
124
+ }
125
+
126
+ /** graphql-js can also fail in scalar serialization or abstract resolution. */
127
+ const sanitize = (
128
+ result: ExecutionResult,
129
+ policy: ErrorPolicy,
130
+ requestStage = false,
131
+ ): Effect.Effect<ExecutionResult> =>
132
+ Effect.gen(function* () {
133
+ if (!result.errors) return result
134
+ const errors = yield* Effect.forEach(result.errors, (error) =>
135
+ Effect.gen(function* () {
136
+ let original: unknown = error
137
+ let scalarFailure = false
138
+ while (true) {
139
+ if (original instanceof ScalarInputFailure) {
140
+ scalarFailure = true
141
+ original = original.original
142
+ } else if (
143
+ original instanceof GraphQLError &&
144
+ !isBoundaryError(original) &&
145
+ original.originalError
146
+ ) {
147
+ original = original.originalError
148
+ } else break
149
+ }
150
+ // Only request-stage diagnostics without application callback provenance
151
+ // are graphql-js's own messages. Execution errors never gain trust from
152
+ // a missing originalError (prelocated errors are forwarded unchanged).
153
+ if (
154
+ (requestStage || !error.path) &&
155
+ !scalarFailure &&
156
+ original instanceof GraphQLError &&
157
+ !isBoundaryError(original)
158
+ )
159
+ return error
160
+ const mapped = yield* recoverCause(Cause.die(original), [], policy, {
161
+ coordinate: error.path?.join(".") ?? "operation",
162
+ phase: "execute",
163
+ })
164
+ if (!(mapped instanceof GraphQLError))
165
+ throw new Error("Error policy must return a GraphQL error")
166
+ return new GraphQLError(mapped.message, {
167
+ nodes: error.nodes,
168
+ source: error.source,
169
+ positions: error.positions,
170
+ path: error.path,
171
+ originalError: mapped,
172
+ extensions: mapped.extensions,
173
+ })
174
+ }),
175
+ )
176
+ return { ...result, errors }
177
+ })
178
+
179
+ const unexpected = (
180
+ cause: Cause.Cause<unknown>,
181
+ policy: ErrorPolicy,
182
+ ): Effect.Effect<ExecutionResult> =>
183
+ Effect.map(
184
+ recoverCause(cause, [], policy, {
185
+ coordinate: "operation",
186
+ phase: "execute",
187
+ }),
188
+ (error) => {
189
+ if (!(error instanceof GraphQLError))
190
+ throw new Error("Error policy must return a GraphQL error")
191
+ return { errors: [error] }
192
+ },
193
+ )
194
+
195
+ function executeDocument<R, C>(
196
+ built: BuiltSchema<R, C>,
197
+ document: DocumentNode,
198
+ request: Request,
199
+ context: unknown,
200
+ ): Effect.Effect<ExecutionResult, never, R> {
201
+ const policy = built[schemaErrorPolicy] ?? defaultErrorPolicy
202
+ return Effect.gen(function* () {
203
+ const errors = validate(built.schema, document, validationRules)
204
+ if (errors.length) return yield* sanitize({ errors }, policy, true)
205
+ const services = yield* Effect.context<R>()
206
+ const result = yield* Effect.promise((signal) =>
207
+ Promise.resolve(
208
+ graphqlExecute({
209
+ schema: built.schema,
210
+ document,
211
+ variableValues: request.variableValues,
212
+ operationName: request.operationName,
213
+ contextValue: bridge(services, context, signal),
214
+ }),
215
+ ),
216
+ )
217
+ return yield* sanitize(result, policy)
218
+ }).pipe(
219
+ Effect.catchCause((cause) => unexpected(cause, policy)),
220
+ Effect.withSpan("GraphQL.execute"),
221
+ )
222
+ }
223
+
224
+ function subscribeDocument<R, C>(
225
+ built: BuiltSchema<R, C>,
226
+ document: DocumentNode,
227
+ request: Request,
228
+ context: unknown,
229
+ ): Stream.Stream<ExecutionResult, never, R> {
230
+ const policy = built[schemaErrorPolicy] ?? defaultErrorPolicy
231
+ return Stream.unwrap(
232
+ Effect.gen(function* () {
233
+ const errors = validate(built.schema, document, validationRules)
234
+ if (errors.length)
235
+ return Stream.fromEffect(sanitize({ errors }, policy, true))
236
+ const lifetime = yield* Effect.acquireRelease(
237
+ Effect.sync(() => ({
238
+ controller: new AbortController(),
239
+ pending: new Set<Promise<unknown>>(),
240
+ sources: new Set<AsyncIterator<unknown, unknown>>(),
241
+ })),
242
+ ({ controller, pending, sources }) =>
243
+ Effect.promise(async () => {
244
+ controller.abort()
245
+ // fromAsyncIterable does not await return() during a pending pull.
246
+ // Closing the owned Effect iterators joins their scope finalizers,
247
+ // including any close already started by graphql-js or the consumer.
248
+ const closing = Array.from(sources, (source) => source.return?.())
249
+ await Promise.allSettled([...pending, ...closing])
250
+ }),
251
+ )
252
+ const services = yield* Effect.context<R>()
253
+ const result: ExecutionResult | AsyncIterable<ExecutionResult> =
254
+ yield* Effect.promise(() =>
255
+ graphqlSubscribe({
256
+ schema: built.schema,
257
+ document,
258
+ variableValues: request.variableValues,
259
+ operationName: request.operationName,
260
+ contextValue: bridge(
261
+ services,
262
+ context,
263
+ lifetime.controller.signal,
264
+ lifetime.pending,
265
+ lifetime.sources,
266
+ ),
267
+ }),
268
+ )
269
+ return (
270
+ Symbol.asyncIterator in result
271
+ ? Stream.fromAsyncIterable(result, (error) => error)
272
+ : Stream.succeed(result)
273
+ ).pipe(Stream.mapEffect((result) => sanitize(result, policy)))
274
+ }),
275
+ ).pipe(
276
+ Stream.catchCause((cause) => Stream.fromEffect(unexpected(cause, policy))),
277
+ Stream.withSpan("GraphQL.subscribe"),
278
+ )
279
+ }
280
+
281
+ function parsed(source: string): DocumentNode | ExecutionResult {
282
+ try {
283
+ return parse(source)
284
+ } catch (error) {
285
+ if (!(error instanceof GraphQLError)) throw error
286
+ return { errors: [error] }
287
+ }
288
+ }
289
+
290
+ export const execute = <R, C>(
291
+ built: BuiltSchema<R, C>,
292
+ request: Request,
293
+ ...context: ContextArgument<NoInfer<C>>
294
+ ): Effect.Effect<ExecutionResult, never, R> =>
295
+ Effect.suspend(() => {
296
+ const document = parsed(request.source)
297
+ return "kind" in document
298
+ ? executeDocument(
299
+ built,
300
+ document,
301
+ request,
302
+ context.length === 0 ? {} : context[0],
303
+ )
304
+ : Effect.succeed(document)
305
+ })
306
+
307
+ export const subscribe = <R, C>(
308
+ built: BuiltSchema<R, C>,
309
+ request: Request,
310
+ ...context: ContextArgument<NoInfer<C>>
311
+ ): Stream.Stream<ExecutionResult, never, R> =>
312
+ Stream.suspend(() => {
313
+ const document = parsed(request.source)
314
+ return "kind" in document
315
+ ? subscribeDocument(
316
+ built,
317
+ document,
318
+ request,
319
+ context.length === 0 ? {} : context[0],
320
+ )
321
+ : Stream.succeed(document)
322
+ })
323
+
324
+ export const executor = <R, C>(
325
+ built: BuiltSchema<R, C>,
326
+ ): Executor<C, never, R> => {
327
+ const request = (input: Execution<C>): Request => ({
328
+ source: input.request.query,
329
+ ...(input.request.variables
330
+ ? { variableValues: input.request.variables }
331
+ : {}),
332
+ ...(input.request.operationName != null
333
+ ? { operationName: input.request.operationName }
334
+ : {}),
335
+ })
336
+ return {
337
+ validation: "executor",
338
+ schema: built.schema,
339
+ execute: (input) =>
340
+ executeDocument(
341
+ built,
342
+ input.document,
343
+ request(input),
344
+ input.context,
345
+ ).pipe(Effect.flatMap((result) => formatResult(result))),
346
+ subscribe: (input) =>
347
+ subscribeDocument(
348
+ built,
349
+ input.document,
350
+ request(input),
351
+ input.context,
352
+ ).pipe(Stream.mapEffect((result) => formatResult(result))),
353
+ }
354
+ }
@@ -0,0 +1,32 @@
1
+ import { Option } from "effect"
2
+ import type { RuntimeRef, OutputRef, Description } from "./FieldTypes.ts"
3
+
4
+ export function wrap<A>(
5
+ kind: "list" | "nullable",
6
+ of: RuntimeRef,
7
+ ): OutputRef<A> {
8
+ // A is a phantom value tracked by the field overloads, never runtime data.
9
+ return { kind, of } as OutputRef<A>
10
+ }
11
+
12
+ export function normalize(ref: RuntimeRef, value: unknown): unknown {
13
+ if (typeof ref === "string") return value
14
+ if (ref.kind === "nullable") {
15
+ const unwrapped = Option.isOption(value) ? Option.getOrNull(value) : value
16
+ return unwrapped == null ? null : normalize(ref.of, unwrapped)
17
+ }
18
+ return Array.isArray(value)
19
+ ? value.map((item) => normalize(ref.of, item))
20
+ : value
21
+ }
22
+
23
+ export function describe(options: Description): Description {
24
+ return {
25
+ ...(options.description === undefined
26
+ ? {}
27
+ : { description: options.description }),
28
+ ...(options.deprecationReason === undefined
29
+ ? {}
30
+ : { deprecationReason: options.deprecationReason }),
31
+ }
32
+ }