@ultimabase/rest 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/entries.d.ts +38 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +47 -0
- package/dist/fields.d.ts +77 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +288 -0
- package/dist/functions.d.ts +28 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +156 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/json-schema.d.ts +8 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +44 -0
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +17 -0
- package/dist/live.d.ts +15 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +168 -0
- package/dist/models.d.ts +8 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +136 -0
- package/dist/openapi-functions.d.ts +11 -0
- package/dist/openapi-functions.d.ts.map +1 -0
- package/dist/openapi-functions.js +85 -0
- package/dist/openapi-inputs.d.ts +30 -0
- package/dist/openapi-inputs.d.ts.map +1 -0
- package/dist/openapi-inputs.js +316 -0
- package/dist/openapi-models.d.ts +14 -0
- package/dist/openapi-models.d.ts.map +1 -0
- package/dist/openapi-models.js +280 -0
- package/dist/openapi.d.ts +47 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +150 -0
- package/dist/params.d.ts +11 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/params.js +27 -0
- package/dist/sse.d.ts +17 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +63 -0
- package/dist/streams.d.ts +73 -0
- package/dist/streams.d.ts.map +1 -0
- package/dist/streams.js +218 -0
- package/dist/values.d.ts +7 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +123 -0
- package/package.json +47 -3
- package/src/entries.ts +82 -0
- package/src/fields.ts +391 -0
- package/src/functions.ts +205 -0
- package/src/index.ts +105 -0
- package/src/json-schema.ts +51 -0
- package/src/limits.ts +28 -0
- package/src/live.ts +223 -0
- package/src/models.ts +180 -0
- package/src/openapi-functions.ts +127 -0
- package/src/openapi-inputs.ts +399 -0
- package/src/openapi-models.ts +367 -0
- package/src/openapi.ts +204 -0
- package/src/params.ts +39 -0
- package/src/sse.ts +79 -0
- package/src/streams.ts +301 -0
- package/src/values.ts +163 -0
package/src/streams.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// An open live read over Server-Sent Events (D-154), ended with its client,
|
|
2
|
+
// its caller or the application. A live list whose client left is kept 10 s,
|
|
3
|
+
// what it sends held, so that the same user's request with its token takes
|
|
4
|
+
// it back, grown, even before the server saw it leave. A live list's first
|
|
5
|
+
// event is its token.
|
|
6
|
+
import { randomUUID } from 'node:crypto'
|
|
7
|
+
import { run } from '@ultimabase/http/internal'
|
|
8
|
+
import type {
|
|
9
|
+
LiveHandle,
|
|
10
|
+
LiveMessage,
|
|
11
|
+
RealtimeApi,
|
|
12
|
+
Viewer,
|
|
13
|
+
} from '@ultimabase/realtime'
|
|
14
|
+
import type { Logger } from 'ultimabase'
|
|
15
|
+
import type { Shape } from './fields.ts'
|
|
16
|
+
import { shaped } from './fields.ts'
|
|
17
|
+
import type { Events } from './sse.ts'
|
|
18
|
+
import { eventsTo } from './sse.ts'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The realtime engine, what ends each open stream, the kept lists, and what
|
|
22
|
+
* keeps each open list as if its client left, by token.
|
|
23
|
+
*/
|
|
24
|
+
export type Live = {
|
|
25
|
+
readonly realtime: RealtimeApi
|
|
26
|
+
readonly streams: Set<() => void>
|
|
27
|
+
readonly kept: Map<string, Kept>
|
|
28
|
+
readonly open: Map<string, () => void>
|
|
29
|
+
readonly logger: Logger
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Where a live read writes: an open stream of events, its response aside. */
|
|
33
|
+
export type Sink = Omit<Events, 'response'>
|
|
34
|
+
|
|
35
|
+
type Changed = Exclude<
|
|
36
|
+
LiveMessage,
|
|
37
|
+
{ readonly kind: 'closed' | 'row' | 'value' }
|
|
38
|
+
>
|
|
39
|
+
/** What a live list's messages write. */
|
|
40
|
+
export type Write = (message: Changed) => void
|
|
41
|
+
|
|
42
|
+
/** A live read to open, given the viewer and what takes its messages. */
|
|
43
|
+
export type Open = (
|
|
44
|
+
viewer: Viewer,
|
|
45
|
+
send: (message: LiveMessage) => void,
|
|
46
|
+
) => Promise<LiveHandle>
|
|
47
|
+
|
|
48
|
+
/** A live list: what writes it, what read it is, its rows, and its token. */
|
|
49
|
+
export type Listed = {
|
|
50
|
+
readonly write: (sink: Sink) => Write
|
|
51
|
+
readonly signature: string
|
|
52
|
+
readonly first: number
|
|
53
|
+
readonly resume: string | undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A sink that writes to its stream, or holds what it is sent while none. */
|
|
57
|
+
type Relay = Sink & {
|
|
58
|
+
readonly to: (sink: Sink) => void
|
|
59
|
+
/** Holds what it is sent from now on; `ended` runs if it ends. */
|
|
60
|
+
readonly park: (ended: () => void) => void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type Stream = {
|
|
64
|
+
readonly viewer: Viewer
|
|
65
|
+
readonly relay: Relay
|
|
66
|
+
readonly keep: Keep | undefined
|
|
67
|
+
readonly signature: string
|
|
68
|
+
handle?: LiveHandle
|
|
69
|
+
/** Whether its client left before its first read. */
|
|
70
|
+
ended: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type Keep = { readonly token: string; first: number }
|
|
74
|
+
|
|
75
|
+
/** A list whose client left, until a resume takes it or its time is up. */
|
|
76
|
+
export type Kept = {
|
|
77
|
+
readonly stream: Stream
|
|
78
|
+
readonly handle: LiveHandle
|
|
79
|
+
readonly keep: Keep
|
|
80
|
+
/** Takes it out, still open. */
|
|
81
|
+
readonly take: () => void
|
|
82
|
+
readonly discard: () => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** How long a list whose client left waits for a resume. */
|
|
86
|
+
const keptFor = 10_000
|
|
87
|
+
const nothing = (): void => undefined
|
|
88
|
+
|
|
89
|
+
function relayOf(): Relay {
|
|
90
|
+
let current: Sink | undefined
|
|
91
|
+
let ended = nothing
|
|
92
|
+
const held: (readonly [string, unknown, number | undefined])[] = []
|
|
93
|
+
return {
|
|
94
|
+
send: (event, data, id) => {
|
|
95
|
+
if (current === undefined) held.push([event, data, id])
|
|
96
|
+
else current.send(event, data, id)
|
|
97
|
+
},
|
|
98
|
+
full: () => current?.full() ?? false,
|
|
99
|
+
drained: (then) => {
|
|
100
|
+
if (current === undefined) then()
|
|
101
|
+
else current.drained(then)
|
|
102
|
+
},
|
|
103
|
+
end: () => {
|
|
104
|
+
if (current === undefined) ended()
|
|
105
|
+
else current.end()
|
|
106
|
+
},
|
|
107
|
+
to: (sink) => {
|
|
108
|
+
current = sink
|
|
109
|
+
for (const [event, data, id] of held.splice(0)) sink.send(event, data, id)
|
|
110
|
+
},
|
|
111
|
+
park: (then) => {
|
|
112
|
+
current = undefined
|
|
113
|
+
ended = then
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Follows the read `open` starts into `sink`; a list's changes go through
|
|
120
|
+
* `list`. Changes are dropped while the client's buffer is full, until the
|
|
121
|
+
* engine sends the window again. A closing error is sent as it comes: its
|
|
122
|
+
* read made it public.
|
|
123
|
+
*/
|
|
124
|
+
export async function follow(
|
|
125
|
+
sink: Sink,
|
|
126
|
+
viewer: Viewer,
|
|
127
|
+
shape: Shape,
|
|
128
|
+
open: Open,
|
|
129
|
+
list?: (sink: Sink) => Write,
|
|
130
|
+
): Promise<LiveHandle> {
|
|
131
|
+
const writes = list?.(sink)
|
|
132
|
+
let handle: LiveHandle | undefined
|
|
133
|
+
let dropping = false
|
|
134
|
+
let owed = false
|
|
135
|
+
const send = (message: LiveMessage) => {
|
|
136
|
+
if (message.kind === 'closed') {
|
|
137
|
+
if (message.error !== undefined) sink.send('error', message.error)
|
|
138
|
+
// A viewer closes when its caller changed: the client reconnects.
|
|
139
|
+
else if (!viewer.closed) {
|
|
140
|
+
sink.send('error', {
|
|
141
|
+
code: 'INTERNAL',
|
|
142
|
+
message: 'The live read closed.',
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
return sink.end()
|
|
146
|
+
}
|
|
147
|
+
if (dropping && message.kind === 'changes') return
|
|
148
|
+
if (!dropping && sink.full()) {
|
|
149
|
+
dropping = true
|
|
150
|
+
sink.drained(() => {
|
|
151
|
+
if (handle === undefined) owed = true
|
|
152
|
+
else void handle.resend()
|
|
153
|
+
})
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
dropping = false
|
|
157
|
+
if (message.kind === 'value') {
|
|
158
|
+
return sink.send('data', message.value, message.revision)
|
|
159
|
+
}
|
|
160
|
+
if (message.kind !== 'row') return writes?.(message)
|
|
161
|
+
const { row, revision } = message
|
|
162
|
+
sink.send('data', row && shaped(row, shape), revision)
|
|
163
|
+
}
|
|
164
|
+
handle = await open(viewer, send)
|
|
165
|
+
if (owed) void handle.resend()
|
|
166
|
+
return handle
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Keeps `stream`'s list once its client left, until a resume takes it. */
|
|
170
|
+
function park(live: Live, stream: Stream, handle: LiveHandle, keep: Keep) {
|
|
171
|
+
const take = () => {
|
|
172
|
+
clearTimeout(timer)
|
|
173
|
+
live.kept.delete(keep.token)
|
|
174
|
+
live.streams.delete(discard)
|
|
175
|
+
}
|
|
176
|
+
const discard = () => {
|
|
177
|
+
take()
|
|
178
|
+
handle.close()
|
|
179
|
+
stream.viewer.close()
|
|
180
|
+
}
|
|
181
|
+
const timer = setTimeout(discard, keptFor)
|
|
182
|
+
timer.unref()
|
|
183
|
+
stream.relay.park(discard)
|
|
184
|
+
live.streams.add(discard)
|
|
185
|
+
live.kept.set(keep.token, { stream, handle, keep, take, discard })
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* `request`'s stream of events, which `stream` writes to from now on; the
|
|
190
|
+
* list is kept once the client leaves, unless the stream was ended.
|
|
191
|
+
*/
|
|
192
|
+
function eventsFor(
|
|
193
|
+
live: Live,
|
|
194
|
+
request: Request,
|
|
195
|
+
stream: Stream,
|
|
196
|
+
): { readonly response: Response; readonly finish: () => void } {
|
|
197
|
+
let over = false
|
|
198
|
+
let left = false
|
|
199
|
+
const leave = () => {
|
|
200
|
+
if (left) return
|
|
201
|
+
left = true
|
|
202
|
+
live.streams.delete(finish)
|
|
203
|
+
const { viewer, handle, keep } = stream
|
|
204
|
+
if (keep !== undefined) live.open.delete(keep.token)
|
|
205
|
+
if (over || handle === undefined || keep === undefined || viewer.closed) {
|
|
206
|
+
stream.ended = true
|
|
207
|
+
handle?.close()
|
|
208
|
+
return viewer.close()
|
|
209
|
+
}
|
|
210
|
+
park(live, stream, handle, keep)
|
|
211
|
+
}
|
|
212
|
+
const events = eventsTo(request, leave)
|
|
213
|
+
const finish = () => {
|
|
214
|
+
over = true
|
|
215
|
+
events.end()
|
|
216
|
+
}
|
|
217
|
+
live.streams.add(finish)
|
|
218
|
+
if (stream.keep !== undefined) {
|
|
219
|
+
// A resume may come before the server sees this client leave.
|
|
220
|
+
live.open.set(stream.keep.token, () => {
|
|
221
|
+
leave()
|
|
222
|
+
events.end()
|
|
223
|
+
})
|
|
224
|
+
// First, before what a kept list held.
|
|
225
|
+
events.send('resume', stream.keep.token)
|
|
226
|
+
}
|
|
227
|
+
stream.relay.to({ ...events, end: finish })
|
|
228
|
+
return { response: events.response, finish }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The list `list.resume` keeps, as the same read of the same user, grown. */
|
|
232
|
+
async function resumed(
|
|
233
|
+
live: Live,
|
|
234
|
+
request: Request,
|
|
235
|
+
list: Listed,
|
|
236
|
+
): Promise<Response | undefined> {
|
|
237
|
+
const token = list.resume ?? ''
|
|
238
|
+
live.open.get(token)?.()
|
|
239
|
+
const kept = live.kept.get(token)
|
|
240
|
+
if (kept === undefined) return undefined
|
|
241
|
+
const { stream, handle, keep } = kept
|
|
242
|
+
if (stream.signature !== list.signature || list.first < keep.first) {
|
|
243
|
+
return undefined
|
|
244
|
+
}
|
|
245
|
+
// Out first, so that neither its time nor another request takes it.
|
|
246
|
+
kept.take()
|
|
247
|
+
// The kept read follows this request's caller; another user ends it.
|
|
248
|
+
if (!(await stream.viewer.adopt(request))) {
|
|
249
|
+
kept.discard()
|
|
250
|
+
return undefined
|
|
251
|
+
}
|
|
252
|
+
const { response, finish } = eventsFor(live, request, stream)
|
|
253
|
+
try {
|
|
254
|
+
if (list.first > keep.first) {
|
|
255
|
+
await run(async () => handle.grow(list.first - keep.first))
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
finish()
|
|
259
|
+
stream.viewer.close()
|
|
260
|
+
throw error
|
|
261
|
+
}
|
|
262
|
+
keep.first = list.first
|
|
263
|
+
return response
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* `request`'s stream of the read `open` starts, closed with the client; a
|
|
268
|
+
* list resumes the one its token keeps when it may.
|
|
269
|
+
*/
|
|
270
|
+
export async function streamed(
|
|
271
|
+
live: Live,
|
|
272
|
+
request: Request,
|
|
273
|
+
shape: Shape,
|
|
274
|
+
open: Open,
|
|
275
|
+
list?: Listed,
|
|
276
|
+
): Promise<Response> {
|
|
277
|
+
if (list?.resume !== undefined) {
|
|
278
|
+
const taken = await resumed(live, request, list)
|
|
279
|
+
if (taken !== undefined) return taken
|
|
280
|
+
}
|
|
281
|
+
const viewer = await live.realtime.connect(request)
|
|
282
|
+
const stream: Stream = {
|
|
283
|
+
viewer,
|
|
284
|
+
relay: relayOf(),
|
|
285
|
+
keep: list && { token: randomUUID(), first: list.first },
|
|
286
|
+
signature: list?.signature ?? '',
|
|
287
|
+
ended: false,
|
|
288
|
+
}
|
|
289
|
+
const { response, finish } = eventsFor(live, request, stream)
|
|
290
|
+
try {
|
|
291
|
+
stream.handle = await run(async () =>
|
|
292
|
+
follow(stream.relay, viewer, shape, open, list?.write),
|
|
293
|
+
)
|
|
294
|
+
} catch (error) {
|
|
295
|
+
finish()
|
|
296
|
+
viewer.close()
|
|
297
|
+
throw error
|
|
298
|
+
}
|
|
299
|
+
if (stream.ended) stream.handle.close()
|
|
300
|
+
return response
|
|
301
|
+
}
|
package/src/values.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Values as GraphQL's JSON (D-129): a date is ISO 8601 text, read back to a
|
|
2
|
+
// `Date` from its field's type, in filters and in data, as `DateTime` is.
|
|
3
|
+
import type { SchemaField } from 'ultimabase'
|
|
4
|
+
import { InvalidQuery } from 'ultimabase'
|
|
5
|
+
import type { Api, Args } from './entries.ts'
|
|
6
|
+
import { entryOf } from './entries.ts'
|
|
7
|
+
|
|
8
|
+
const iso =
|
|
9
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/
|
|
10
|
+
|
|
11
|
+
export const isArgs = (value: unknown): value is Args =>
|
|
12
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
13
|
+
|
|
14
|
+
/** A string as a `Date`; any other value left for the kernel to check. */
|
|
15
|
+
function date(value: unknown, path: string): unknown {
|
|
16
|
+
if (typeof value !== 'string') return value
|
|
17
|
+
const parsed = iso.test(value) ? new Date(value) : undefined
|
|
18
|
+
if (parsed !== undefined && !Number.isNaN(parsed.getTime())) return parsed
|
|
19
|
+
throw new InvalidQuery(
|
|
20
|
+
path,
|
|
21
|
+
`${path} must be an ISO 8601 DateTime, such as 2026-01-31T12:00:00.000Z.`,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isDate = (field: SchemaField | undefined, key: string): boolean =>
|
|
26
|
+
key === 'createdAt' ||
|
|
27
|
+
key === 'updatedAt' ||
|
|
28
|
+
(field?.kind === 'scalar' && field.type === 'dateTime')
|
|
29
|
+
|
|
30
|
+
/** A date field's filter: a value, or operators whose values are dates. */
|
|
31
|
+
function dateFilter(value: unknown, path: string): unknown {
|
|
32
|
+
if (!isArgs(value)) return date(value, path)
|
|
33
|
+
const filter: { [operator: string]: unknown } = {}
|
|
34
|
+
for (const [operator, operand] of Object.entries(value)) {
|
|
35
|
+
const at = `${path}.${operator}`
|
|
36
|
+
filter[operator] = Array.isArray(operand)
|
|
37
|
+
? operand.map((item: unknown, index) => date(item, `${at}.${index}`))
|
|
38
|
+
: date(operand, at)
|
|
39
|
+
}
|
|
40
|
+
return filter
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const each = (
|
|
44
|
+
value: unknown,
|
|
45
|
+
path: string,
|
|
46
|
+
map: (item: Args, at: string) => Args,
|
|
47
|
+
): unknown =>
|
|
48
|
+
Array.isArray(value)
|
|
49
|
+
? value.map((item: unknown, index) =>
|
|
50
|
+
isArgs(item) ? map(item, `${path}.${index}`) : item,
|
|
51
|
+
)
|
|
52
|
+
: isArgs(value)
|
|
53
|
+
? map(value, path)
|
|
54
|
+
: value
|
|
55
|
+
|
|
56
|
+
/** A filter with its dates read, through `AND`, `OR`, `NOT` and relations. */
|
|
57
|
+
export function whereOf(
|
|
58
|
+
api: Api,
|
|
59
|
+
model: string,
|
|
60
|
+
where: Args,
|
|
61
|
+
path: string,
|
|
62
|
+
): Args {
|
|
63
|
+
const fields = entryOf(api, model).model.fields
|
|
64
|
+
const read: { [key: string]: unknown } = {}
|
|
65
|
+
for (const [key, value] of Object.entries(where)) {
|
|
66
|
+
const at = `${path}.${key}`
|
|
67
|
+
const field = fields.find((candidate) => candidate.name === key)
|
|
68
|
+
if (key === 'AND' || key === 'OR' || key === 'NOT') {
|
|
69
|
+
read[key] = each(value, at, (inner, to) => whereOf(api, model, inner, to))
|
|
70
|
+
} else if (isDate(field, key)) read[key] = dateFilter(value, at)
|
|
71
|
+
else if (field !== undefined && field.kind !== 'scalar' && isArgs(value)) {
|
|
72
|
+
// `is`, `some` and the others each take the target's filter.
|
|
73
|
+
const { target } = field
|
|
74
|
+
read[key] = Object.fromEntries(
|
|
75
|
+
Object.entries(value).map(([operator, inner]) => [
|
|
76
|
+
operator,
|
|
77
|
+
isArgs(inner)
|
|
78
|
+
? whereOf(api, target, inner, `${at}.${operator}`)
|
|
79
|
+
: inner,
|
|
80
|
+
]),
|
|
81
|
+
)
|
|
82
|
+
} else read[key] = value
|
|
83
|
+
}
|
|
84
|
+
return read
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type Read = (item: Args, path: string) => Args
|
|
88
|
+
|
|
89
|
+
/** `item` with its `key` read, when it is an object. */
|
|
90
|
+
function within(item: Args, key: string, path: string, read: Read): Args {
|
|
91
|
+
const value = item[key]
|
|
92
|
+
return isArgs(value)
|
|
93
|
+
? { ...item, [key]: read(value, `${path}.${key}`) }
|
|
94
|
+
: item
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The operations that create, update or delete a target's row (D-138).
|
|
98
|
+
const reaching = new Set(['connectOrCreate', 'create', 'delete', 'update'])
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A relation write with the dates of its rows and unique targets read
|
|
102
|
+
* (D-128, D-138). A hidden model is connected, disconnected or set, never
|
|
103
|
+
* created, updated or deleted, as over GraphQL.
|
|
104
|
+
*/
|
|
105
|
+
function relationOf(
|
|
106
|
+
api: Api,
|
|
107
|
+
model: string,
|
|
108
|
+
field: Exclude<SchemaField, { readonly kind: 'scalar' }>,
|
|
109
|
+
value: Args,
|
|
110
|
+
path: string,
|
|
111
|
+
): Args {
|
|
112
|
+
const { target } = field
|
|
113
|
+
const hidden = !entryOf(api, target).visible
|
|
114
|
+
const row: Read = (item, at) => dataOf(api, target, item, at)
|
|
115
|
+
const unique: Read = (item, at) => whereOf(api, target, item, at)
|
|
116
|
+
const read: { [key: string]: unknown } = { ...value }
|
|
117
|
+
for (const [key, operand] of Object.entries(value)) {
|
|
118
|
+
const at = `${path}.${key}`
|
|
119
|
+
if (hidden && reaching.has(key)) {
|
|
120
|
+
throw new InvalidQuery(at, `Unknown key '${at}' on ${model}.`)
|
|
121
|
+
}
|
|
122
|
+
if (key === 'create') read[key] = each(operand, at, row)
|
|
123
|
+
else if (key === 'connectOrCreate') {
|
|
124
|
+
read[key] = each(operand, at, (item, to) =>
|
|
125
|
+
within(within(item, 'where', to, unique), 'create', to, row),
|
|
126
|
+
)
|
|
127
|
+
} else if (key === 'update' && field.kind === 'many') {
|
|
128
|
+
read[key] = each(operand, at, (item, to) =>
|
|
129
|
+
within(within(item, 'where', to, unique), 'data', to, row),
|
|
130
|
+
)
|
|
131
|
+
} else if (key === 'update') read[key] = each(operand, at, row)
|
|
132
|
+
else if (key === 'delete' && field.kind === 'many') {
|
|
133
|
+
read[key] = each(operand, at, unique)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return read
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Data with its dates read, in the rows its relation writes reach too. */
|
|
140
|
+
export function dataOf(
|
|
141
|
+
api: Api,
|
|
142
|
+
model: string,
|
|
143
|
+
data: Args,
|
|
144
|
+
path: string,
|
|
145
|
+
): Args {
|
|
146
|
+
const read: { [field: string]: unknown } = { ...data }
|
|
147
|
+
for (const field of entryOf(api, model).model.fields) {
|
|
148
|
+
const value = data[field.name]
|
|
149
|
+
const at = `${path}.${field.name}`
|
|
150
|
+
if (field.kind === 'scalar') {
|
|
151
|
+
if (field.type === 'dateTime' && value !== undefined) {
|
|
152
|
+
read[field.name] = date(value, at)
|
|
153
|
+
}
|
|
154
|
+
// Number updates are the server API's only (D-132).
|
|
155
|
+
if (field.type === 'number' && isArgs(value)) {
|
|
156
|
+
throw new InvalidQuery(at, `${at} must be a number.`)
|
|
157
|
+
}
|
|
158
|
+
} else if (isArgs(value)) {
|
|
159
|
+
read[field.name] = relationOf(api, model, field, value, at)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return read
|
|
163
|
+
}
|