@ultimabase/graphql 0.0.0-stage → 0.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 (99) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +4 -2
  3. package/dist/data-types.d.ts +15 -0
  4. package/dist/data-types.d.ts.map +1 -0
  5. package/dist/data-types.js +53 -0
  6. package/dist/data.d.ts +34 -0
  7. package/dist/data.d.ts.map +1 -0
  8. package/dist/data.js +207 -0
  9. package/dist/depth.d.ts +6 -0
  10. package/dist/depth.d.ts.map +1 -0
  11. package/dist/depth.js +67 -0
  12. package/dist/errors.d.ts +7 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +42 -0
  15. package/dist/functions.d.ts +28 -0
  16. package/dist/functions.d.ts.map +1 -0
  17. package/dist/functions.js +195 -0
  18. package/dist/graphiql.d.ts +3 -0
  19. package/dist/graphiql.d.ts.map +1 -0
  20. package/dist/graphiql.js +46 -0
  21. package/dist/index.d.ts +47 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +165 -0
  24. package/dist/inputs.d.ts +20 -0
  25. package/dist/inputs.d.ts.map +1 -0
  26. package/dist/inputs.js +236 -0
  27. package/dist/json-fields.d.ts +12 -0
  28. package/dist/json-fields.d.ts.map +1 -0
  29. package/dist/json-fields.js +66 -0
  30. package/dist/json-schema.d.ts +7 -0
  31. package/dist/json-schema.d.ts.map +1 -0
  32. package/dist/json-schema.js +16 -0
  33. package/dist/live-stream.d.ts +59 -0
  34. package/dist/live-stream.d.ts.map +1 -0
  35. package/dist/live-stream.js +136 -0
  36. package/dist/mutations.d.ts +9 -0
  37. package/dist/mutations.d.ts.map +1 -0
  38. package/dist/mutations.js +228 -0
  39. package/dist/names.d.ts +8 -0
  40. package/dist/names.d.ts.map +1 -0
  41. package/dist/names.js +14 -0
  42. package/dist/outputs.d.ts +16 -0
  43. package/dist/outputs.d.ts.map +1 -0
  44. package/dist/outputs.js +177 -0
  45. package/dist/overlap.d.ts +5 -0
  46. package/dist/overlap.d.ts.map +1 -0
  47. package/dist/overlap.js +82 -0
  48. package/dist/pages.d.ts +11 -0
  49. package/dist/pages.d.ts.map +1 -0
  50. package/dist/pages.js +22 -0
  51. package/dist/registry.d.ts +80 -0
  52. package/dist/registry.d.ts.map +1 -0
  53. package/dist/registry.js +97 -0
  54. package/dist/reread.d.ts +9 -0
  55. package/dist/reread.d.ts.map +1 -0
  56. package/dist/reread.js +116 -0
  57. package/dist/rows.d.ts +23 -0
  58. package/dist/rows.d.ts.map +1 -0
  59. package/dist/rows.js +106 -0
  60. package/dist/scalars.d.ts +13 -0
  61. package/dist/scalars.d.ts.map +1 -0
  62. package/dist/scalars.js +88 -0
  63. package/dist/schema.d.ts +17 -0
  64. package/dist/schema.d.ts.map +1 -0
  65. package/dist/schema.js +130 -0
  66. package/dist/select.d.ts +24 -0
  67. package/dist/select.d.ts.map +1 -0
  68. package/dist/select.js +152 -0
  69. package/dist/sockets.d.ts +25 -0
  70. package/dist/sockets.d.ts.map +1 -0
  71. package/dist/sockets.js +234 -0
  72. package/dist/subscriptions.d.ts +14 -0
  73. package/dist/subscriptions.d.ts.map +1 -0
  74. package/dist/subscriptions.js +242 -0
  75. package/package.json +58 -3
  76. package/src/data-types.ts +100 -0
  77. package/src/data.ts +366 -0
  78. package/src/depth.ts +98 -0
  79. package/src/errors.ts +48 -0
  80. package/src/functions.ts +291 -0
  81. package/src/graphiql.ts +54 -0
  82. package/src/index.ts +231 -0
  83. package/src/inputs.ts +348 -0
  84. package/src/json-fields.ts +95 -0
  85. package/src/json-schema.ts +21 -0
  86. package/src/live-stream.ts +192 -0
  87. package/src/mutations.ts +317 -0
  88. package/src/names.ts +25 -0
  89. package/src/outputs.ts +267 -0
  90. package/src/overlap.ts +97 -0
  91. package/src/pages.ts +39 -0
  92. package/src/registry.ts +189 -0
  93. package/src/reread.ts +159 -0
  94. package/src/rows.ts +151 -0
  95. package/src/scalars.ts +110 -0
  96. package/src/schema.ts +189 -0
  97. package/src/select.ts +230 -0
  98. package/src/sockets.ts +291 -0
  99. package/src/subscriptions.ts +334 -0
@@ -0,0 +1,291 @@
1
+ // The application's functions (D-109): a query under Query, a mutation
2
+ // under Mutation, each run as the request's caller with the arguments the
3
+ // client sent; a live query also under Subscription (D-154).
4
+ import type {
5
+ GraphQLFieldConfig,
6
+ GraphQLNamedType,
7
+ GraphQLOutputType,
8
+ GraphQLResolveInfo,
9
+ } from 'graphql'
10
+ import {
11
+ GraphQLBoolean,
12
+ GraphQLList,
13
+ GraphQLNonNull,
14
+ GraphQLObjectType,
15
+ } from 'graphql'
16
+ import { readableVirtuals } from '@ultimabase/http/internal'
17
+ import type {
18
+ ExposedFunction,
19
+ ExposedOutput,
20
+ ExposedType,
21
+ ExposedVirtual,
22
+ } from 'ultimabase'
23
+ import { Refused } from 'ultimabase'
24
+ import type { Path } from './data.ts'
25
+ import type { DataTypes } from './data-types.ts'
26
+ import { checkName, inputArgs, outputData } from './data.ts'
27
+ import { upper } from './names.ts'
28
+ import { isMaster, objectType } from './outputs.ts'
29
+ import { selectOf } from './select.ts'
30
+ import { followed } from './subscriptions.ts'
31
+ import type { Args, Context, Registry } from './registry.ts'
32
+ import { as } from './registry.ts'
33
+ import { reread } from './reread.ts'
34
+ import { plain } from './scalars.ts'
35
+ import type { Read } from './rows.ts'
36
+
37
+ /** A function's key of `.with` (D-109): its field's name, or hidden. */
38
+ export type GraphqlFunctionWith = { readonly name?: string } | false
39
+
40
+ export type Field = GraphQLFieldConfig<unknown, Context, Args>
41
+
42
+ /** A function's root field, and the function that gives it. */
43
+ export type RootField = {
44
+ readonly query: boolean
45
+ readonly name: string
46
+ readonly owner: string
47
+ readonly field: Field
48
+ }
49
+
50
+ const at = (owner: string, name: string, place: string): Path => ({
51
+ owner,
52
+ name,
53
+ suffix: '',
54
+ at: place,
55
+ })
56
+
57
+ /** Whether a model's rows, as `info` selects them, read a relation. */
58
+ function relates(
59
+ registry: Registry,
60
+ model: string,
61
+ info: GraphQLResolveInfo,
62
+ master: boolean,
63
+ ): boolean {
64
+ const type = objectType(registry, model)
65
+ const select = selectOf(registry, info, type, info.fieldNodes, master)
66
+ return Object.values(select).some((read) => read !== true)
67
+ }
68
+
69
+ class Builder {
70
+ readonly #types = new Map<string, GraphQLObjectType>()
71
+ readonly #registry: Registry
72
+ readonly #data: DataTypes
73
+ readonly #take: number
74
+ /** What each field reads, by type then field, for the row budget. */
75
+ readonly reads = new Map<string, Map<string, Read>>()
76
+ /** Each named type, and the first function to give it. */
77
+ readonly owners: Map<string, string>
78
+
79
+ constructor(registry: Registry, take: number) {
80
+ this.#registry = registry
81
+ this.#data = registry.data
82
+ this.owners = this.#data.owners
83
+ this.#take = take
84
+ }
85
+
86
+ /** A model or a type is non-null unless `.nullable()`; a list too. */
87
+ output(
88
+ output: ExposedOutput,
89
+ path: Path,
90
+ parent: string,
91
+ field: string,
92
+ ): GraphQLOutputType {
93
+ if (output.kind === 'schema') {
94
+ const { type, nullable } = outputData(output.schema, path, this.#data)
95
+ return nullable ? type : new GraphQLNonNull(type)
96
+ }
97
+ const reads = this.reads.get(parent) ?? new Map<string, Read>()
98
+ this.reads.set(parent, reads)
99
+ const list = output.form === 'list'
100
+ reads.set(
101
+ field,
102
+ list ? { kind: 'list', size: this.#take } : { kind: 'one' },
103
+ )
104
+ const named =
105
+ output.kind === 'model'
106
+ ? objectType(this.#registry, output.name)
107
+ : this.#type(output.type, path.owner)
108
+ if (output.form === 'nullable') return named
109
+ const item = new GraphQLNonNull(named)
110
+ return list ? new GraphQLNonNull(new GraphQLList(item)) : item
111
+ }
112
+
113
+ /** The type named `name`, if a function, a virtual or a field gave it. */
114
+ named(name: string): GraphQLNamedType | undefined {
115
+ return this.#types.get(name) ?? this.#data.named(name)
116
+ }
117
+
118
+ /** A `defineType`, built once; its fields cannot refer back to it. */
119
+ #type(type: ExposedType, owner: string): GraphQLObjectType {
120
+ const known = this.#types.get(type.name)
121
+ if (known !== undefined) return known
122
+ const { name } = type
123
+ checkName(name, `defineType ${name}`)
124
+ const other = this.owners.get(name)
125
+ if (other !== undefined) {
126
+ throw new Refused(
127
+ `${other} and ${owner} give the type ${name} two shapes; name each once.`,
128
+ )
129
+ }
130
+ this.owners.set(name, owner)
131
+ const fields = [...type.fields].map(([field, output]): [string, Field] => {
132
+ checkName(field, `${name}.${field}`)
133
+ const path = at(name, `${name}${upper(field)}`, field)
134
+ return [field, { type: this.output(output, path, name, field) }]
135
+ })
136
+ const built = new GraphQLObjectType<unknown, Context>({
137
+ name,
138
+ description: type.description,
139
+ fields: Object.fromEntries(fields),
140
+ })
141
+ this.#types.set(name, built)
142
+ return built
143
+ }
144
+
145
+ /** A virtual's field: an argument per input property, its output (D-151). */
146
+ virtual(model: string, virtual: ExposedVirtual, ruled: boolean): Field {
147
+ const { name, input, output } = virtual
148
+ const owner = `${model}.${name}`
149
+ const base = `${model}${upper(name)}`
150
+ const given =
151
+ input === undefined
152
+ ? undefined
153
+ : inputArgs(input, at(owner, `${base}Input`, 'input'), this.#data)
154
+ if (given?.flat === false) {
155
+ throw new Refused(
156
+ `${owner}: its input is an object schema, whose properties are its arguments.`,
157
+ )
158
+ }
159
+ const type = this.output(
160
+ output,
161
+ at(owner, `${base}Output`, name),
162
+ model,
163
+ name,
164
+ )
165
+ const take = this.#take
166
+ const registry = this.#registry
167
+ return {
168
+ // Absent when a read rule hides it (D-44).
169
+ type: ruled && type instanceof GraphQLNonNull ? type.ofType : type,
170
+ description: virtual.description,
171
+ args: given?.args ?? {},
172
+ resolve: (source, _, context, info) => {
173
+ const value: unknown = Reflect.get(Object(source), name)
174
+ // The budget counts `take` rows in a list (D-109).
175
+ if (
176
+ output.kind === 'model' &&
177
+ Array.isArray(value) &&
178
+ value.length > take &&
179
+ relates(registry, output.name, info, isMaster(context))
180
+ ) {
181
+ throw new RangeError(
182
+ `${owner} returned ${value.length} rows; a list counts take (${take}) rows, so it holds no more under a selected relation.`,
183
+ )
184
+ }
185
+ return value
186
+ },
187
+ }
188
+ }
189
+
190
+ /**
191
+ * A function's field, and its subscription when it is `live` (D-154),
192
+ * whose rows the budget counts as the query's.
193
+ */
194
+ field(
195
+ fn: ExposedFunction,
196
+ name: string,
197
+ owner: string,
198
+ live: boolean,
199
+ ): { readonly field: Field; readonly live?: Field } {
200
+ const { input, output } = fn
201
+ const base = upper(name)
202
+ const given =
203
+ input === undefined
204
+ ? undefined
205
+ : inputArgs(input, at(owner, `${base}Input`, 'input'), this.#data)
206
+ const root = fn.query ? 'Query' : 'Mutation'
207
+ const type =
208
+ output === undefined
209
+ ? new GraphQLNonNull(GraphQLBoolean)
210
+ : this.output(output, at(owner, `${base}Output`, 'output'), root, name)
211
+ const registry = this.#registry
212
+ const take = this.#take
213
+ const value = async (
214
+ args: Args,
215
+ context: Context,
216
+ info: GraphQLResolveInfo,
217
+ ): Promise<unknown> => {
218
+ const sent = given?.flat === true ? args : args['input']
219
+ const result = await fn.run(plain(sent), as(context), context.client)
220
+ if (output === undefined) return true
221
+ if (result === undefined && type instanceof GraphQLNonNull) {
222
+ throw new TypeError(
223
+ `${fn.name} returned undefined; make its output .nullable() and return null.`,
224
+ )
225
+ }
226
+ const call = { name: fn.name, take }
227
+ return reread(registry, output, result, context, info, call)
228
+ }
229
+ const config = {
230
+ type,
231
+ description: fn.description,
232
+ args: given?.args ?? {},
233
+ }
234
+ const field: Field = {
235
+ ...config,
236
+ resolve: async (_, args, context, info) => value(args, context, info),
237
+ }
238
+ if (!live) return { field }
239
+ const read = this.reads.get(root)?.get(name)
240
+ if (read !== undefined) {
241
+ const reads = this.reads.get('Subscription') ?? new Map<string, Read>()
242
+ this.reads.set('Subscription', reads.set(name, read))
243
+ }
244
+ return { field, live: followed(owner, config, value) }
245
+ }
246
+ }
247
+
248
+ /** The functions' root fields, and what each reads (D-109). */
249
+ export function functionFields(
250
+ registry: Registry,
251
+ functions: readonly ExposedFunction<GraphqlFunctionWith>[],
252
+ take: number,
253
+ live: (owner: string) => boolean,
254
+ ): {
255
+ readonly fields: readonly RootField[]
256
+ /** The live functions' subscriptions, named as their fields (D-154). */
257
+ readonly subscriptions: { readonly [name: string]: Field }
258
+ readonly reads: ReadonlyMap<string, ReadonlyMap<string, Read>>
259
+ readonly owners: ReadonlyMap<string, string>
260
+ readonly named: (name: string) => GraphQLNamedType | undefined
261
+ } {
262
+ const builder = new Builder(registry, take)
263
+ registry.virtualFields = (model) => {
264
+ const entry = registry.entry(model)
265
+ const { readRules } = entry.exposed
266
+ return Object.fromEntries(
267
+ readableVirtuals(entry).map((virtual) => [
268
+ virtual.name,
269
+ builder.virtual(model, virtual, readRules[virtual.name] === 'rule'),
270
+ ]),
271
+ )
272
+ }
273
+ const fields: RootField[] = []
274
+ const subscriptions: { [name: string]: Field } = {}
275
+ for (const fn of functions) {
276
+ if (fn.with === false) continue
277
+ const name = checkName(fn.with?.name ?? fn.name, fn.name)
278
+ // A plugin's function is named with its plugin (D-113).
279
+ const owner = fn.plugin === undefined ? fn.name : `${fn.plugin}.${fn.name}`
280
+ const built = builder.field(fn, name, owner, live(owner))
281
+ fields.push({ query: fn.query, name, owner, field: built.field })
282
+ if (built.live !== undefined) subscriptions[name] = built.live
283
+ }
284
+ return {
285
+ fields,
286
+ subscriptions,
287
+ reads: builder.reads,
288
+ owners: builder.owners,
289
+ named: (name) => builder.named(name),
290
+ }
291
+ }
@@ -0,0 +1,54 @@
1
+ // GraphiQL 5 (D-107): the page a browser gets at `/graphql` outside
2
+ // production, its modules loaded from esm.sh at pinned versions.
3
+
4
+ const imports = {
5
+ react: 'https://esm.sh/react@19.3.1',
6
+ 'react/': 'https://esm.sh/react@19.3.1/',
7
+ 'react-dom': 'https://esm.sh/react-dom@19.3.1',
8
+ 'react-dom/': 'https://esm.sh/react-dom@19.3.1/',
9
+ graphiql:
10
+ 'https://esm.sh/graphiql@5.4.0?standalone&external=react,react-dom,@graphiql/react,graphql',
11
+ 'graphiql/': 'https://esm.sh/graphiql@5.4.0/',
12
+ '@graphiql/react':
13
+ 'https://esm.sh/@graphiql/react@0.39.1?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid',
14
+ '@graphiql/toolkit':
15
+ 'https://esm.sh/@graphiql/toolkit@0.12.1?standalone&external=graphql',
16
+ graphql: 'https://esm.sh/graphql@17.0.2',
17
+ '@emotion/is-prop-valid': 'data:text/javascript,',
18
+ }
19
+
20
+ /** A value written into a script, unable to close it. */
21
+ const inline = (value: unknown): string =>
22
+ JSON.stringify(value).replaceAll('<', '\\u003c')
23
+
24
+ /** The page, served at `path`, that sends its documents to `path`. */
25
+ export function graphiqlPage(path: string): () => Response {
26
+ const html = `<!doctype html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="utf-8">
30
+ <meta name="viewport" content="width=device-width,initial-scale=1">
31
+ <title>UltimaBase GraphiQL</title>
32
+ <style>html,body,#graphiql{height:100%;margin:0}</style>
33
+ <link rel="stylesheet" href="https://esm.sh/graphiql@5.4.0/dist/style.css">
34
+ <script type="importmap">${inline({ imports })}</script>
35
+ <script type="module">
36
+ import React from 'react'
37
+ import ReactDOM from 'react-dom/client'
38
+ import { GraphiQL } from 'graphiql'
39
+ import { createGraphiQLFetcher } from '@graphiql/toolkit'
40
+ import 'graphiql/setup-workers/esm.sh'
41
+ const fetcher = createGraphiQLFetcher({ url: ${inline(path)} })
42
+ ReactDOM.createRoot(document.getElementById('graphiql')).render(
43
+ React.createElement(GraphiQL, { fetcher }),
44
+ )
45
+ </script>
46
+ </head>
47
+ <body><div id="graphiql"></div></body>
48
+ </html>
49
+ `
50
+ return () =>
51
+ new Response(html, {
52
+ headers: { 'content-type': 'text/html; charset=utf-8' },
53
+ })
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,231 @@
1
+ // `graphqlPlugin()` (D-107, D-109): a Relay schema generated from the models
2
+ // and the functions, mounted on the HTTP plugin and run as each request's
3
+ // caller; with the realtime plugin, its live models' subscriptions (D-154).
4
+ import type { GraphQLSchema } from 'graphql'
5
+ import {
6
+ executeRootSelectionSet,
7
+ MaxIntrospectionDepthRule,
8
+ NoSchemaIntrospectionCustomRule,
9
+ parse,
10
+ specifiedRules,
11
+ validate,
12
+ validateExecutionArgs,
13
+ } from 'graphql'
14
+ import { createHandler } from 'graphql-http/lib/use/fetch'
15
+ import type { Http } from '@ultimabase/http'
16
+ import { clientOf, httpPlugin, routes } from '@ultimabase/http'
17
+ import type { Realtime } from '@ultimabase/realtime'
18
+ import { realtimePlugin } from '@ultimabase/realtime'
19
+ import type { Caller, Client, PluginOf, PluginTypes, UseOf } from 'ultimabase'
20
+ import { definePlugin } from 'ultimabase'
21
+ import manifest from '../package.json' with { type: 'json' }
22
+ import { depthRule, introspectionDepthRule } from './depth.ts'
23
+ import { formatter } from './errors.ts'
24
+ import type { GraphqlFunctionWith } from './functions.ts'
25
+ import { functionFields } from './functions.ts'
26
+ import { graphiqlPage } from './graphiql.ts'
27
+ import { overlapRefusal } from './overlap.ts'
28
+ import { rootNames } from './names.ts'
29
+ import type { Context, Entry, GraphqlWith } from './registry.ts'
30
+ import { Registry } from './registry.ts'
31
+ import { rowBudget } from './rows.ts'
32
+ import { schemaOf } from './schema.ts'
33
+ import { selectFor } from './select.ts'
34
+ import type { Operations } from './sockets.ts'
35
+ import { serveSockets } from './sockets.ts'
36
+ import { subscriptionFields } from './subscriptions.ts'
37
+
38
+ export type { GraphqlFunctionWith } from './functions.ts'
39
+ export type { Context as GraphqlContext, GraphqlWith } from './registry.ts'
40
+
41
+ /** What `graphqlPlugin` takes (D-107). */
42
+ export interface GraphqlOptions {
43
+ /** `/graphql` by default. */
44
+ readonly path?: string
45
+ /** Relay connections, or `take` and `skip` lists. */
46
+ readonly pagination?: 'connection' | 'list'
47
+ /** The rows a list returns when none is asked for; 100 by default (D-55). */
48
+ readonly take?: number
49
+ /** The rows a caller other than Master may ask for; 100 by default. */
50
+ readonly maxTake?: number
51
+ /** The deepest a document of a caller other than Master nests; 10 by default. */
52
+ readonly maxDepth?: number
53
+ /**
54
+ * The most rows a document of a caller other than Master may return, each
55
+ * list times the lists above it; 20 000 by default.
56
+ */
57
+ readonly maxRows?: number
58
+ /**
59
+ * Introspection and "Did you mean" suggestions: closed in production,
60
+ * open elsewhere, unless set.
61
+ */
62
+ readonly introspection?: boolean
63
+ /** Replaces the generated schema with one built from it (D-45). */
64
+ readonly extend?: (schema: GraphQLSchema) => GraphQLSchema
65
+ }
66
+
67
+ /** The GraphQL plugin's types (D-57). */
68
+ export interface Graphql extends PluginTypes {
69
+ readonly name: 'graphql'
70
+ readonly appOptions: GraphqlOptions
71
+ readonly requires: Http
72
+ readonly optional: Realtime
73
+ readonly with: GraphqlWith
74
+ readonly functionWith: GraphqlFunctionWith
75
+ }
76
+
77
+ /** About ten times the standard introspection query. */
78
+ const maxTokens = 2000
79
+
80
+ // What closes each running application's WebSockets, by its plugin's context.
81
+ const closers = new WeakMap<object, () => Promise<void>>()
82
+
83
+ const plugin = definePlugin<Graphql>({
84
+ name: 'graphql',
85
+ manifest,
86
+ requires: { http: httpPlugin },
87
+ optional: { realtime: realtimePlugin },
88
+ api: (pluginContext) => {
89
+ const { plugins, models, functions, options, logger } = pluginContext
90
+ const { realtime } = plugins
91
+ const take = options.take ?? 100
92
+ const entries = new Map<string, Entry>()
93
+ for (const exposed of models) {
94
+ const name = exposed.schema.name
95
+ const own = exposed.with === false ? {} : (exposed.with ?? {})
96
+ entries.set(name, {
97
+ exposed,
98
+ model: exposed.schema,
99
+ visible: exposed.with !== false,
100
+ ...rootNames(name, own.plural),
101
+ take,
102
+ maxTake: own.maxTake ?? options.maxTake ?? 100,
103
+ })
104
+ }
105
+ const registry = new Registry(entries, {
106
+ pagination: options.pagination ?? 'connection',
107
+ })
108
+ const liveFunction = realtime?.liveFunction ?? (() => false)
109
+ const called = functionFields(registry, functions, take, liveFunction)
110
+ const live = realtime?.live ?? (() => false)
111
+ const generated = schemaOf(registry, {
112
+ ...called,
113
+ subscriptions: {
114
+ ...subscriptionFields(registry, live),
115
+ ...called.subscriptions,
116
+ },
117
+ })
118
+ const schema = options.extend?.(generated) ?? generated
119
+ const production = process.env['NODE_ENV'] === 'production'
120
+ const hideSuggestions = !(options.introspection ?? !production)
121
+ const closed = hideSuggestions ? [NoSchemaIntrospectionCustomRule] : []
122
+ const maxDepth = options.maxDepth ?? 10
123
+ const depth = depthRule(maxDepth)
124
+ const maxRows = options.maxRows ?? 20_000
125
+ const budget = rowBudget(registry, { maxRows, maxDepth }, called.reads)
126
+ // The contexts of Master's requests, which no budget limits.
127
+ const exempt = new WeakSet<object>()
128
+ // The caller the HTTP plugin read and the client, by its request.
129
+ const requests = new WeakMap<
130
+ Request,
131
+ { readonly caller: Caller; readonly client: Client }
132
+ >()
133
+ const heldOf = (request: Request) => {
134
+ const held = requests.get(request)
135
+ if (held === undefined) throw new Error('No caller was read.')
136
+ return held
137
+ }
138
+ const operations: Operations = {
139
+ schema,
140
+ // Validation is quadratic in a field repeated under one key, so a
141
+ // document is bounded before it is validated.
142
+ parse: (source) => parse(source, { maxTokens }),
143
+ rules: (master) => [
144
+ ...specifiedRules.filter((rule) => rule !== MaxIntrospectionDepthRule),
145
+ introspectionDepthRule,
146
+ ...closed,
147
+ ...(master ? [] : [depth]),
148
+ ],
149
+ validate: (document, rules) => {
150
+ const refused = overlapRefusal(document)
151
+ if (refused !== undefined) return [refused]
152
+ return validate(schema, document, rules, { hideSuggestions })
153
+ },
154
+ execute: async (args) => {
155
+ const validated = validateExecutionArgs({ ...args, hideSuggestions })
156
+ if (!('schema' in validated)) return { errors: validated }
157
+ const { contextValue } = validated
158
+ const master =
159
+ typeof contextValue === 'object' &&
160
+ contextValue !== null &&
161
+ exempt.has(contextValue)
162
+ const refused = master ? undefined : budget(validated)
163
+ if (refused !== undefined) return { errors: [refused] }
164
+ return executeRootSelectionSet(validated)
165
+ },
166
+ context: (caller, client) => {
167
+ const master = caller.kind === 'master'
168
+ const select = selectFor(registry, master)
169
+ const context = { caller, client, select }
170
+ if (master) exempt.add(context)
171
+ return context
172
+ },
173
+ hideSuggestions,
174
+ format: formatter(logger),
175
+ logger,
176
+ }
177
+ const handler = createHandler<Context>({
178
+ schema,
179
+ parse: operations.parse,
180
+ context: (request) => {
181
+ const { caller, client } = heldOf(request.raw)
182
+ return operations.context(caller, client)
183
+ },
184
+ validationRules: (request) =>
185
+ operations.rules(heldOf(request.raw).caller.kind === 'master'),
186
+ validate: (_, document, rules) => operations.validate(document, rules),
187
+ execute: operations.execute,
188
+ formatError: operations.format,
189
+ })
190
+ const path = options.path ?? '/graphql'
191
+ const page = production ? undefined : graphiqlPage(path)
192
+ plugins.http.mount(
193
+ routes().on(['GET', 'POST'], path, async (c) => {
194
+ // A browser opening the URL, not a client sending a document.
195
+ const browsing =
196
+ c.req.method === 'GET' &&
197
+ c.req.header('accept')?.includes('text/html') === true
198
+ if (page !== undefined && browsing) return page()
199
+ // A middleware may have read the body: Hono kept it, not the request.
200
+ const request =
201
+ c.req.method === 'POST' && c.req.raw.bodyUsed
202
+ ? new Request(c.req.raw, {
203
+ method: 'POST',
204
+ body: await c.req.text(),
205
+ })
206
+ : c.req.raw
207
+ const { client, sent } = clientOf(c)
208
+ requests.set(request, { caller: c.var.caller, client })
209
+ return sent(await handler(request))
210
+ }),
211
+ )
212
+ if (realtime !== undefined) {
213
+ const close = serveSockets(plugins.http, path, realtime, operations)
214
+ closers.set(pluginContext, close)
215
+ }
216
+ },
217
+ start: ({ plugins, options, logger }) => {
218
+ if (process.env['NODE_ENV'] === 'production') return
219
+ logger.info(
220
+ `GraphiQL on ${plugins.http.url()}${options.path ?? '/graphql'}.`,
221
+ )
222
+ },
223
+ // Before the HTTP plugin's stop, which waits for the sockets to close.
224
+ stop: async (context) => closers.get(context)?.(),
225
+ })
226
+
227
+ /** The application's GraphQL API, served by `httpPlugin` (D-107). */
228
+ export const graphqlPlugin: PluginOf<Graphql> = plugin.plugin
229
+
230
+ /** A model's `.with({ graphql })`, from its `use` (D-107). */
231
+ export const graphql: UseOf<Graphql> = plugin.use