@ultimabase/graphql 0.0.0-stage → 0.1.1

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 +24 -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 +53 -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,334 @@
1
+ // The subscriptions (D-154): each live model's row and list, with its query's
2
+ // name, arguments and type, followed by the realtime engine. The first result
3
+ // is `{ data }`; a list then sends only what changed, under `extensions.live`,
4
+ // or its whole window in page mode. A list's first result carries the token
5
+ // that resumes it once completed, grown.
6
+ import { randomUUID } from 'node:crypto'
7
+ import {
8
+ applied,
9
+ badInput,
10
+ connection,
11
+ pageOf,
12
+ run,
13
+ } from '@ultimabase/http/internal'
14
+ import type { LiveMessage } from '@ultimabase/realtime'
15
+ import type {
16
+ ExecutionResult,
17
+ GraphQLFieldConfig,
18
+ GraphQLResolveInfo,
19
+ } from 'graphql'
20
+ import { GraphQLError, GraphQLList, GraphQLNonNull } from 'graphql'
21
+ import type { Caller, Row } from 'ultimabase'
22
+ import { objectOf, uniqueInput, uniqueWhere } from './inputs.ts'
23
+ import type { Keep, Live } from './live-stream.ts'
24
+ import { LiveEvent, liveStream, lives, resumed } from './live-stream.ts'
25
+ import { connectionType, isMaster, listArgs, objectType } from './outputs.ts'
26
+ import { whereOf } from './pages.ts'
27
+ import type { Args, Context, Entry, Registry } from './registry.ts'
28
+ import type { Key } from './select.ts'
29
+ import { grouped, rowNodes, selectOf } from './select.ts'
30
+
31
+ type Fields = { [field: string]: GraphQLFieldConfig<unknown, Context, Args> }
32
+ type Message = Exclude<LiveMessage, { readonly kind: 'closed' }>
33
+
34
+ const responseKey: Key = (node) => node.alias?.value ?? node.name.value
35
+
36
+ /** The response key of the first field named `name` the info's field selects. */
37
+ function keyOf(info: GraphQLResolveInfo, name: string): string | undefined {
38
+ for (const [key, [first]] of grouped(info, info.fieldNodes, responseKey)) {
39
+ if (first.name.value === name) return key
40
+ }
41
+ return undefined
42
+ }
43
+
44
+ const valueAt = (value: unknown, key: string | undefined): unknown =>
45
+ key === undefined || typeof value !== 'object' || value === null
46
+ ? undefined
47
+ : Reflect.get(value, key)
48
+
49
+ const fromTop = { size: undefined, backward: false }
50
+
51
+ /** A window from the top of its order, as a connection. */
52
+ function connectionOf(
53
+ rows: readonly Row[],
54
+ hasMore: boolean,
55
+ totalCount: number | undefined,
56
+ ) {
57
+ const { edges, pageInfo } = connection(rows, fromTop, (row) => row)
58
+ return { edges, pageInfo: { ...pageInfo, hasNextPage: hasMore }, totalCount }
59
+ }
60
+
61
+ const revised = (
62
+ result: ExecutionResult,
63
+ revision: number,
64
+ resume?: string,
65
+ ) => ({
66
+ ...result,
67
+ extensions: {
68
+ ...result.extensions,
69
+ live: { revision, ...(resume !== undefined && { resume }) },
70
+ },
71
+ })
72
+
73
+ /** Where a list's root, its edges, page info and count are in the result. */
74
+ const keysOf = (info: GraphQLResolveInfo, connected: boolean) =>
75
+ [
76
+ String(info.path.key),
77
+ ...(connected
78
+ ? [
79
+ keyOf(info, 'edges'),
80
+ keyOf(info, 'pageInfo'),
81
+ keyOf(info, 'totalCount'),
82
+ ]
83
+ : [undefined, undefined, undefined]),
84
+ ] as const
85
+
86
+ /**
87
+ * What a live read's messages run, with what it sent so far; a list's
88
+ * first result carries `resume`.
89
+ */
90
+ function follower(
91
+ info: GraphQLResolveInfo,
92
+ connected: boolean,
93
+ page: boolean,
94
+ resume?: string,
95
+ ): (message: Message) => LiveEvent {
96
+ const [key, edgesKey, pageInfoKey, totalCountKey] = keysOf(info, connected)
97
+ let token = resume
98
+ let window: readonly Row[] = []
99
+ let hasMore = false
100
+ let count: number | undefined
101
+ let pageInfo = ''
102
+ const rootOf = (rows: readonly Row[]) =>
103
+ connected ? connectionOf(rows, hasMore, count) : rows
104
+ const sent = (result: ExecutionResult): ExecutionResult => {
105
+ const at = valueAt(result.data, key)
106
+ pageInfo = JSON.stringify(valueAt(at, pageInfoKey)) ?? ''
107
+ return result
108
+ }
109
+ return (message) => {
110
+ if (message.kind === 'row' || message.kind === 'value') {
111
+ const { revision } = message
112
+ const root = message.kind === 'row' ? message.row : message.value
113
+ return new LiveEvent(root, (result) => revised(result, revision))
114
+ }
115
+ const { revision } = message
116
+ hasMore = message.hasMore ?? hasMore
117
+ count = message.totalCount ?? count
118
+ if (message.kind === 'reset' || page) {
119
+ window =
120
+ message.kind === 'reset'
121
+ ? message.rows
122
+ : applied(window, message.changes, (row) => row)
123
+ const shown = token
124
+ token = undefined
125
+ return new LiveEvent(rootOf(window), (result) =>
126
+ revised(sent(result), revision, shown),
127
+ )
128
+ }
129
+ const { changes } = message
130
+ window = applied(window, changes, ({ id }) => ({ id }))
131
+ const upserts = changes.flatMap((change) =>
132
+ 'upsert' in change ? [change.upsert] : [],
133
+ )
134
+ const root = connected
135
+ ? {
136
+ ...connectionOf(window, hasMore, count),
137
+ edges: connectionOf(upserts, false, undefined).edges,
138
+ }
139
+ : upserts
140
+ return new LiveEvent(root, (result) => {
141
+ const at = valueAt(result.data, key)
142
+ const upserted = connected ? valueAt(at, edgesKey) : at
143
+ const shown: readonly unknown[] = Array.isArray(upserted) ? upserted : []
144
+ let next = 0
145
+ const live: { [key: string]: unknown } = { revision }
146
+ if (!connected || edgesKey !== undefined) {
147
+ live['changes'] = changes.map((change) => {
148
+ if ('remove' in change) return change
149
+ next += 1
150
+ return { upsert: shown[next - 1], after: change.after }
151
+ })
152
+ }
153
+ if (message.totalCount !== undefined && totalCountKey !== undefined) {
154
+ live['totalCount'] = valueAt(at, totalCountKey)
155
+ }
156
+ const before = pageInfo
157
+ sent(result)
158
+ if (pageInfoKey !== undefined && pageInfo !== before) {
159
+ live['pageInfo'] = valueAt(at, pageInfoKey)
160
+ }
161
+ return {
162
+ ...(result.errors && { errors: result.errors }),
163
+ extensions: { ...result.extensions, live },
164
+ }
165
+ })
166
+ }
167
+ }
168
+
169
+ /**
170
+ * The last result of a live read the engine closed, or of a live function
171
+ * that failed with `error`, which the error formatter shows or masks.
172
+ */
173
+ const closedEvent = (error?: unknown) =>
174
+ new LiveEvent(null, () => ({
175
+ errors: [
176
+ error === undefined
177
+ ? new GraphQLError('The live read closed.', {
178
+ extensions: { code: 'INTERNAL' },
179
+ })
180
+ : new GraphQLError('A live function failed.', {
181
+ originalError:
182
+ error instanceof Error
183
+ ? error
184
+ : new Error('A live function threw.', { cause: error }),
185
+ }),
186
+ ],
187
+ }))
188
+
189
+ /** What a subscription's event holds; its stream only yields `LiveEvent`s. */
190
+ const rootOf = (event: unknown): unknown => Reflect.get(Object(event), 'root')
191
+
192
+ /** The WebSocket operation `context` runs in. */
193
+ function liveOf(context: Context): Live {
194
+ const live = lives.get(context)
195
+ if (live === undefined) throw badInput('A subscription runs over WebSocket.')
196
+ return live
197
+ }
198
+
199
+ /** `post(where)` and `posts(where, orderBy, first)` of a live model. */
200
+ function liveFields(registry: Registry, entry: Entry): Fields {
201
+ const name = entry.model.name
202
+ const type = objectType(registry, name)
203
+ const { pagination } = registry.settings
204
+ const connected = pagination === 'connection'
205
+ const list = connected
206
+ ? new GraphQLNonNull(connectionType(registry, `${name}Connection`, name))
207
+ : new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(type)))
208
+ // A window starts at the top of its order (D-154).
209
+ const windowArgs = Object.fromEntries(
210
+ Object.entries(listArgs(registry, name)).filter(
211
+ ([arg]) => !['after', 'last', 'before', 'skip'].includes(arg),
212
+ ),
213
+ )
214
+ return {
215
+ [entry.single]: {
216
+ type,
217
+ args: {
218
+ where: { type: new GraphQLNonNull(uniqueInput(registry, name)) },
219
+ },
220
+ subscribe: async (_, args, context, info) => {
221
+ const { channel, page } = liveOf(context)
222
+ const where = uniqueWhere(registry, name, objectOf(args['where']))
223
+ const master = isMaster(context)
224
+ const select = selectOf(registry, info, type, info.fieldNodes, master)
225
+ return run(async () =>
226
+ liveStream(
227
+ channel,
228
+ async (send) => channel.viewer.row(name, { where, select }, send),
229
+ follower(info, false, page),
230
+ closedEvent,
231
+ ),
232
+ )
233
+ },
234
+ resolve: rootOf,
235
+ },
236
+ [entry.list]: {
237
+ type: list,
238
+ args: windowArgs,
239
+ subscribe: async (_, args, context, info) => {
240
+ const { channel, page } = liveOf(context)
241
+ const master = isMaster(context)
242
+ const { read: paged, relay } = pageOf(pagination, entry, args, master)
243
+ const asked = relay === undefined ? paged['take'] : relay.size
244
+ const rows = typeof asked === 'number' ? asked : entry.take
245
+ const arg = connected ? 'first' : 'take'
246
+ if (page && rows > entry.maxTake) {
247
+ throw badInput(
248
+ `${arg} is at most ${entry.maxTake} on ${name} in page mode.`,
249
+ )
250
+ }
251
+ const nodes = rowNodes(registry, info)
252
+ const select = selectOf(registry, info, type, nodes, master)
253
+ const { orderBy: order } = args
254
+ const read = {
255
+ ...whereOf(registry, name, args),
256
+ ...(order === undefined || order === null ? {} : { orderBy: order }),
257
+ select,
258
+ totalCount:
259
+ connected && grouped(info, info.fieldNodes).has('totalCount'),
260
+ }
261
+ const signature = JSON.stringify([
262
+ name,
263
+ page,
264
+ keysOf(info, connected),
265
+ read,
266
+ ])
267
+ const { resume } = liveOf(context)
268
+ return run(async () => {
269
+ const taken =
270
+ resume === undefined
271
+ ? undefined
272
+ : await resumed(channel, resume, signature, rows)
273
+ if (taken !== undefined) return taken
274
+ const keep: Keep = { token: randomUUID(), signature, first: rows }
275
+ return liveStream(
276
+ channel,
277
+ async (send) =>
278
+ channel.viewer.list(name, { ...read, first: rows }, send),
279
+ follower(info, connected, page, keep.token),
280
+ closedEvent,
281
+ keep,
282
+ )
283
+ })
284
+ },
285
+ resolve: rootOf,
286
+ },
287
+ }
288
+ }
289
+
290
+ /**
291
+ * A live function's subscription (D-154), as its query field: `value` runs
292
+ * it as the subscriber each time it may have changed.
293
+ */
294
+ export function followed(
295
+ owner: string,
296
+ field: Omit<GraphQLFieldConfig<unknown, Context, Args>, 'resolve'>,
297
+ value: (
298
+ args: Args,
299
+ context: Context,
300
+ info: GraphQLResolveInfo,
301
+ ) => Promise<unknown>,
302
+ ): GraphQLFieldConfig<unknown, Context, Args> {
303
+ return {
304
+ ...field,
305
+ subscribe: async (_, args, context, info) => {
306
+ const { channel } = liveOf(context)
307
+ const compute = async (caller: Caller) =>
308
+ value(args, { ...context, caller }, info)
309
+ return run(async () =>
310
+ liveStream(
311
+ channel,
312
+ async (send) => channel.viewer.value(owner, compute, send),
313
+ follower(info, false, false),
314
+ closedEvent,
315
+ ),
316
+ )
317
+ },
318
+ resolve: rootOf,
319
+ }
320
+ }
321
+
322
+ /** The live models' subscriptions, by root name. */
323
+ export function subscriptionFields(
324
+ registry: Registry,
325
+ live: (model: string) => boolean,
326
+ ): Fields {
327
+ const fields: Fields = {}
328
+ for (const entry of registry.entries.values()) {
329
+ if (entry.visible && live(entry.model.name)) {
330
+ Object.assign(fields, liveFields(registry, entry))
331
+ }
332
+ }
333
+ return fields
334
+ }