@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,374 @@
1
+ import { Effect } from "effect"
2
+ import {
3
+ GraphQLBoolean,
4
+ GraphQLInt,
5
+ GraphQLString,
6
+ type GraphQLFieldConfigArgumentMap,
7
+ GraphQLList,
8
+ GraphQLNonNull,
9
+ GraphQLObjectType,
10
+ type GraphQLFieldConfigMap,
11
+ type GraphQLNamedType,
12
+ type GraphQLInterfaceType,
13
+ type GraphQLUnionType,
14
+ type GraphQLResolveInfo,
15
+ } from "graphql"
16
+ import { type ErrorPolicy, fieldError, protect } from "./Errors.ts"
17
+ import { type Field, type RequestContext } from "./FieldTypes.ts"
18
+ import {
19
+ type AnyResolve,
20
+ type AnyConfig,
21
+ config,
22
+ resolverField,
23
+ } from "./Resolver.ts"
24
+ import { invalid } from "./SchemaTypes.ts"
25
+
26
+ export interface PageInfo {
27
+ readonly hasNextPage: boolean
28
+ readonly hasPreviousPage: boolean
29
+ readonly startCursor: string | null
30
+ readonly endCursor: string | null
31
+ }
32
+
33
+ export interface Edge<A> {
34
+ readonly cursor: string
35
+ readonly node: A
36
+ }
37
+ /** Structurally identical to model-ts' PaginationResult, so paginate() results pass through. */
38
+
39
+ export interface Connection<A> {
40
+ readonly edges: ReadonlyArray<Edge<A>>
41
+ readonly pageInfo: PageInfo
42
+ }
43
+
44
+ export interface ConnectionArgs {
45
+ readonly first?: number | null
46
+ readonly after?: string | null
47
+ readonly last?: number | null
48
+ readonly before?: string | null
49
+ }
50
+
51
+ export const encodeCursor = (offset: number) => btoa(`cursor:${offset}`)
52
+
53
+ export function decodeCursor(cursor: string): number {
54
+ try {
55
+ const match = /^cursor:(\d+)$/.exec(atob(cursor))
56
+ if (match && Number.isSafeInteger(Number(match[1]))) return Number(match[1])
57
+ } catch {
58
+ /* Reject invalid base64 using the same public error. */
59
+ }
60
+ throw fieldError("Invalid cursor", { code: "BAD_USER_INPUT" })
61
+ }
62
+
63
+ /** graphql-js has already validated the four built-in pagination arguments. */
64
+ export function paginationArgs(
65
+ raw: Readonly<Record<string, unknown>>,
66
+ ): ConnectionArgs {
67
+ const pick = <K extends keyof ConnectionArgs>(key: K) =>
68
+ raw[key] == null ? {} : { [key]: raw[key] }
69
+ return {
70
+ ...pick("first"),
71
+ ...pick("after"),
72
+ ...pick("last"),
73
+ ...pick("before"),
74
+ } as ConnectionArgs
75
+ }
76
+
77
+ export function validatePagination(
78
+ args: ConnectionArgs,
79
+ coordinate: string,
80
+ max: number,
81
+ ): void {
82
+ const reject = (message: string): never => {
83
+ throw fieldError(`${coordinate}: ${message}`, { code: "BAD_USER_INPUT" })
84
+ }
85
+ if (args.after != null && args.before != null)
86
+ reject('use "after" or "before", not both')
87
+ if (args.first != null && args.last != null)
88
+ reject('use "first" or "last", not both')
89
+ if (args.first != null && args.before != null)
90
+ reject('"before" cannot be combined with "first"')
91
+ if (args.last != null && args.after != null)
92
+ reject('"after" cannot be combined with "last"')
93
+ for (const key of ["first", "last"] as const) {
94
+ const size = args[key]
95
+ if (size != null && (size < 0 || size > max))
96
+ reject(`"${key}" must be between 0 and ${max}`)
97
+ }
98
+ }
99
+
100
+ /** Validation sees only caller arguments; a before-only request defaults backward. */
101
+ export function pageDefaults(
102
+ args: ConnectionArgs,
103
+ size: number,
104
+ ): ConnectionArgs {
105
+ if (args.first != null || args.last != null) return args
106
+ return args.before != null
107
+ ? { ...args, last: size }
108
+ : { ...args, first: size }
109
+ }
110
+
111
+ export function slice<A>(
112
+ nodes: ReadonlyArray<A>,
113
+ args: ConnectionArgs,
114
+ ): Connection<A> {
115
+ let start =
116
+ args.after == null
117
+ ? 0
118
+ : Math.min(nodes.length, decodeCursor(args.after) + 1)
119
+ let end =
120
+ args.before == null
121
+ ? nodes.length
122
+ : Math.min(nodes.length, decodeCursor(args.before))
123
+ if (args.first != null) end = Math.min(end, start + args.first)
124
+ if (args.last != null) start = Math.max(start, end - args.last)
125
+ const edges = nodes
126
+ .slice(start, Math.max(start, end))
127
+ .map((node, index) => ({ cursor: encodeCursor(start + index), node }))
128
+ return {
129
+ edges,
130
+ pageInfo: {
131
+ hasPreviousPage: start > 0,
132
+ hasNextPage: end < nodes.length,
133
+ startCursor: edges[0]?.cursor ?? null,
134
+ endCursor: edges.at(-1)?.cursor ?? null,
135
+ },
136
+ }
137
+ }
138
+
139
+ export const connectionArgs: GraphQLFieldConfigArgumentMap = {
140
+ first: {
141
+ type: GraphQLInt,
142
+ description: "Returns the first n elements from the list.",
143
+ },
144
+ after: {
145
+ type: GraphQLString,
146
+ description:
147
+ "Returns the elements in the list that come after the specified cursor",
148
+ },
149
+ last: {
150
+ type: GraphQLInt,
151
+ description: "Returns the last n elements from the list.",
152
+ },
153
+ before: {
154
+ type: GraphQLString,
155
+ description:
156
+ "Returns the elements in the list that come before the specified cursor",
157
+ },
158
+ }
159
+
160
+ export const connectionField = (
161
+ policy: ErrorPolicy,
162
+ maxPageSize: number,
163
+ defaultPageSize: number,
164
+ node: string,
165
+ input: AnyResolve | AnyConfig = {},
166
+ ): Field<unknown> => {
167
+ const options = config(input)
168
+ const { resolve, nodes, totalCount } = options
169
+ for (const key of Object.keys(options.args ?? {}))
170
+ if (Object.hasOwn(connectionArgs, key))
171
+ invalid(`Reserved connection argument: ${key}`)
172
+ return resolverField(policy, options, {
173
+ type: (context, name) =>
174
+ context.connection(
175
+ node,
176
+ options.name,
177
+ name,
178
+ options.totalCount !== undefined,
179
+ options.edgeName,
180
+ ),
181
+ args: options.args ?? {},
182
+ extraArgs: connectionArgs,
183
+ members: [],
184
+ call: (source, raw, decoded, coordinate, context) => {
185
+ const supplied = paginationArgs(raw)
186
+ validatePagination(supplied, coordinate, maxPageSize)
187
+ const page = nodes ? pageDefaults(supplied, defaultPageSize) : supplied
188
+ if (nodes) {
189
+ if (page.after != null) decodeCursor(page.after)
190
+ if (page.before != null) decodeCursor(page.before)
191
+ }
192
+ // Resolver and nodes overloads prove these shapes; this runtime path
193
+ // handles heterogeneous field values after their type parameters are erased.
194
+ const value = resolve
195
+ ? resolve(source, { ...(decoded as object), ...page }, context)
196
+ : nodes
197
+ ? nodes(source, decoded, context)
198
+ : Reflect.get(
199
+ Object(source),
200
+ coordinate.slice(coordinate.lastIndexOf(".") + 1),
201
+ )
202
+ if (!totalCount) return value
203
+ const finish = (result: unknown) => ({
204
+ ...(resolve
205
+ ? (result as object)
206
+ : slice(result as readonly unknown[], page)),
207
+ // graphql-js invokes function-valued fields only when selected, including
208
+ // fragments and directives. Keep the Effect bridge and policy at that boundary.
209
+ totalCount: (
210
+ _args: unknown,
211
+ request: RequestContext,
212
+ info: GraphQLResolveInfo,
213
+ ) =>
214
+ request.run(
215
+ protect(() => totalCount(source, decoded, context), [], policy, {
216
+ coordinate: `${info.parentType.name}.${info.fieldName}`,
217
+ phase: "resolve",
218
+ }),
219
+ ),
220
+ })
221
+ return Effect.isEffect(value) ? Effect.map(value, finish) : finish(value)
222
+ },
223
+ finish: (value, raw) =>
224
+ !nodes || options.totalCount
225
+ ? value
226
+ : slice(
227
+ value as ReadonlyArray<unknown>,
228
+ pageDefaults(paginationArgs(raw), defaultPageSize),
229
+ ),
230
+ })
231
+ }
232
+
233
+ /** Derived names are reserved only when a field actually requests them. */
234
+ export function makeConnectionTypes(
235
+ named: Map<string, GraphQLNamedType>,
236
+ add: <T extends GraphQLNamedType>(type: T) => T,
237
+ nodeType: (
238
+ name: string,
239
+ coordinate: string,
240
+ ) => GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType,
241
+ isNode: (name: string) => boolean,
242
+ ) {
243
+ const generated = new Map<
244
+ string,
245
+ { readonly type: GraphQLObjectType; readonly shape: string }
246
+ >()
247
+ const derivedObject = (
248
+ name: string,
249
+ shape: string,
250
+ fields: () => GraphQLFieldConfigMap<unknown, unknown>,
251
+ description?: string,
252
+ ) => {
253
+ const existing = generated.get(name)
254
+ if (existing) {
255
+ if (existing.shape !== shape)
256
+ invalid(`Incompatible derived type ${name}; use a different edgeName`)
257
+ return existing.type
258
+ }
259
+ // add owns the global namespace: a real object may use a synthetic-looking
260
+ // name, but deriving that same name must report a collision.
261
+ const type = add(new GraphQLObjectType({ name, fields, description }))
262
+ generated.set(name, { type, shape })
263
+ return type
264
+ }
265
+ const pageInfo = () =>
266
+ derivedObject(
267
+ "PageInfo",
268
+ "pageInfo",
269
+ () => ({
270
+ hasNextPage: {
271
+ type: new GraphQLNonNull(GraphQLBoolean),
272
+ description:
273
+ "Used to indicate whether more edges exist following the set defined by the clients arguments.",
274
+ },
275
+ hasPreviousPage: {
276
+ type: new GraphQLNonNull(GraphQLBoolean),
277
+ description:
278
+ "Used to indicate whether more edges exist prior to the set defined by the clients arguments.",
279
+ },
280
+ startCursor: {
281
+ type: GraphQLString,
282
+ description:
283
+ "The cursor corresponding to the first nodes in edges. Null if the connection is empty.",
284
+ },
285
+ endCursor: {
286
+ type: GraphQLString,
287
+ description:
288
+ "The cursor corresponding to the last nodes in edges. Null if the connection is empty.",
289
+ },
290
+ }),
291
+ "PageInfo cursor, as defined in https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo",
292
+ )
293
+ const edgeNodes = new Map<string, string>()
294
+ const edgeType = (node: string, name = `${node}Edge`) => {
295
+ const owner = edgeNodes.get(name)
296
+ if (owner !== undefined && owner !== node)
297
+ invalid(
298
+ `Incompatible edge ${name}: nodes ${owner} and ${node}; use a different edgeName`,
299
+ )
300
+ edgeNodes.set(name, node)
301
+ return derivedObject(name, `edge:${node}`, () => ({
302
+ cursor: {
303
+ type: new GraphQLNonNull(GraphQLString),
304
+ description:
305
+ "https://facebook.github.io/relay/graphql/connections.htm#sec-Cursor",
306
+ },
307
+ node: {
308
+ type: new GraphQLNonNull(nodeType(node, `${name}.node`)),
309
+ description:
310
+ "https://facebook.github.io/relay/graphql/connections.htm#sec-Node",
311
+ },
312
+ }))
313
+ }
314
+ const connectionEdges = new Map<string, string>()
315
+ const connectionNodes = new Map<string, string>()
316
+ const countedConnections = new Map<string, boolean>()
317
+ const connectionType = (
318
+ node: string,
319
+ name: string,
320
+ counted = false,
321
+ edgeName = `${node}Edge`,
322
+ ): GraphQLObjectType => {
323
+ const existing = named.get(name)
324
+ if (existing) {
325
+ if (
326
+ connectionNodes.get(name) !== node ||
327
+ countedConnections.get(name) !== counted ||
328
+ connectionEdges.get(name) !== edgeName
329
+ )
330
+ invalid(
331
+ `Duplicate GraphQL name: ${name}; already used for ${connectionNodes.get(name) ?? "another type"}`,
332
+ )
333
+ if (!(existing instanceof GraphQLObjectType))
334
+ return invalid(`Expected connection object: ${name}`)
335
+ return existing
336
+ }
337
+ connectionEdges.set(name, edgeName)
338
+ connectionNodes.set(name, node)
339
+ countedConnections.set(name, counted)
340
+ return add(
341
+ new GraphQLObjectType({
342
+ name,
343
+ fields: () => ({
344
+ edges: {
345
+ description:
346
+ "https://facebook.github.io/relay/graphql/connections.htm#sec-Edge-Types",
347
+ type: new GraphQLNonNull(
348
+ new GraphQLList(new GraphQLNonNull(edgeType(node, edgeName))),
349
+ ),
350
+ },
351
+ pageInfo: {
352
+ type: new GraphQLNonNull(pageInfo()),
353
+ description:
354
+ "https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo",
355
+ },
356
+ ...(counted
357
+ ? { totalCount: { type: new GraphQLNonNull(GraphQLInt) } }
358
+ : {}),
359
+ }),
360
+ }),
361
+ )
362
+ }
363
+ function derived(name: string): GraphQLNamedType | undefined {
364
+ if (name === "PageInfo") return pageInfo()
365
+ for (const suffix of ["Edge", "Connection"] as const) {
366
+ const node = name.slice(0, -suffix.length)
367
+ if (!name.endsWith(suffix) || !isNode(node)) continue
368
+ return suffix === "Edge" ? edgeType(node) : connectionType(node, name)
369
+ }
370
+ return undefined
371
+ }
372
+
373
+ return { derived, connectionType }
374
+ }