@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.
- package/CHANGELOG.md +24 -0
- package/README.md +4 -2
- package/dist/data-types.d.ts +15 -0
- package/dist/data-types.d.ts.map +1 -0
- package/dist/data-types.js +53 -0
- package/dist/data.d.ts +34 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +207 -0
- package/dist/depth.d.ts +6 -0
- package/dist/depth.d.ts.map +1 -0
- package/dist/depth.js +67 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/functions.d.ts +28 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +195 -0
- package/dist/graphiql.d.ts +3 -0
- package/dist/graphiql.d.ts.map +1 -0
- package/dist/graphiql.js +46 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/inputs.d.ts +20 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +236 -0
- package/dist/json-fields.d.ts +12 -0
- package/dist/json-fields.d.ts.map +1 -0
- package/dist/json-fields.js +66 -0
- package/dist/json-schema.d.ts +7 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +16 -0
- package/dist/live-stream.d.ts +59 -0
- package/dist/live-stream.d.ts.map +1 -0
- package/dist/live-stream.js +136 -0
- package/dist/mutations.d.ts +9 -0
- package/dist/mutations.d.ts.map +1 -0
- package/dist/mutations.js +228 -0
- package/dist/names.d.ts +8 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +14 -0
- package/dist/outputs.d.ts +16 -0
- package/dist/outputs.d.ts.map +1 -0
- package/dist/outputs.js +177 -0
- package/dist/overlap.d.ts +5 -0
- package/dist/overlap.d.ts.map +1 -0
- package/dist/overlap.js +82 -0
- package/dist/pages.d.ts +11 -0
- package/dist/pages.d.ts.map +1 -0
- package/dist/pages.js +22 -0
- package/dist/registry.d.ts +80 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +97 -0
- package/dist/reread.d.ts +9 -0
- package/dist/reread.d.ts.map +1 -0
- package/dist/reread.js +116 -0
- package/dist/rows.d.ts +23 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +106 -0
- package/dist/scalars.d.ts +13 -0
- package/dist/scalars.d.ts.map +1 -0
- package/dist/scalars.js +88 -0
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +130 -0
- package/dist/select.d.ts +24 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +152 -0
- package/dist/sockets.d.ts +25 -0
- package/dist/sockets.d.ts.map +1 -0
- package/dist/sockets.js +234 -0
- package/dist/subscriptions.d.ts +14 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +242 -0
- package/package.json +53 -3
- package/src/data-types.ts +100 -0
- package/src/data.ts +366 -0
- package/src/depth.ts +98 -0
- package/src/errors.ts +48 -0
- package/src/functions.ts +291 -0
- package/src/graphiql.ts +54 -0
- package/src/index.ts +231 -0
- package/src/inputs.ts +348 -0
- package/src/json-fields.ts +95 -0
- package/src/json-schema.ts +21 -0
- package/src/live-stream.ts +192 -0
- package/src/mutations.ts +317 -0
- package/src/names.ts +25 -0
- package/src/outputs.ts +267 -0
- package/src/overlap.ts +97 -0
- package/src/pages.ts +39 -0
- package/src/registry.ts +189 -0
- package/src/reread.ts +159 -0
- package/src/rows.ts +151 -0
- package/src/scalars.ts +110 -0
- package/src/schema.ts +189 -0
- package/src/select.ts +230 -0
- package/src/sockets.ts +291 -0
- 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
|
+
}
|