lakebed 0.0.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/README.md +94 -0
- package/package.json +62 -0
- package/src/anonymous-server.js +1078 -0
- package/src/anonymous.js +996 -0
- package/src/cli.js +1066 -0
- package/src/client.d.ts +8 -0
- package/src/client.js +154 -0
- package/src/runtime.js +252 -0
- package/src/server.d.ts +53 -0
- package/src/server.js +39 -0
- package/src/source-store.js +110 -0
package/src/anonymous.js
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
export const ANONYMOUS_ARTIFACT_FORMAT = "lakebed.capsule.artifact.v1";
|
|
5
|
+
export const ANONYMOUS_ARTIFACT_MEDIA_TYPE = "application/vnd.lakebed.artifact+json";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_ANONYMOUS_LIMITS = {
|
|
8
|
+
artifactBytes: 1024 * 1024,
|
|
9
|
+
stateBytes: 1024 * 1024,
|
|
10
|
+
requestsPerDay: 10000,
|
|
11
|
+
mutationsPerDay: 1000,
|
|
12
|
+
rowsReturned: 1000,
|
|
13
|
+
instructionBudget: 50000,
|
|
14
|
+
maxValueBytes: 65536,
|
|
15
|
+
logEntries: 1000
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const expressionOps = new Set(["arg", "auth", "call", "row"]);
|
|
19
|
+
|
|
20
|
+
export class AnonymousCompilerError extends Error {
|
|
21
|
+
constructor(diagnostics) {
|
|
22
|
+
super(diagnostics.map((diagnostic) => diagnostic.message).join("\n"));
|
|
23
|
+
this.name = "AnonymousCompilerError";
|
|
24
|
+
this.diagnostics = diagnostics;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sha256(value) {
|
|
29
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function stableStringify(value) {
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (value === null || typeof value !== "object") {
|
|
38
|
+
return JSON.stringify(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return `[${value.map((item) => stableStringify(item) ?? "null").join(",")}]`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const entries = Object.entries(value)
|
|
46
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
47
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
48
|
+
|
|
49
|
+
return `{${entries
|
|
50
|
+
.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue) ?? "null"}`)
|
|
51
|
+
.join(",")}}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function byteLength(value) {
|
|
55
|
+
return Buffer.byteLength(typeof value === "string" ? value : stableStringify(value), "utf8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cloneJson(value) {
|
|
59
|
+
return JSON.parse(JSON.stringify(value));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isPlainObject(value) {
|
|
63
|
+
return Boolean(value) && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isExpression(value) {
|
|
67
|
+
return Array.isArray(value) && expressionOps.has(value[0]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function serializeValue(value) {
|
|
71
|
+
if (value instanceof SymbolicValue) {
|
|
72
|
+
return value.expr;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
return value.map((item) => serializeValue(item));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isPlainObject(value)) {
|
|
80
|
+
const object = {};
|
|
81
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
82
|
+
object[key] = serializeValue(entryValue);
|
|
83
|
+
}
|
|
84
|
+
return object;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class SymbolicValue {
|
|
91
|
+
constructor(expr, sample) {
|
|
92
|
+
this.expr = expr;
|
|
93
|
+
this.sample = sample;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
trim() {
|
|
97
|
+
return new SymbolicValue(["call", "trim", this.expr], String(this.sample).trim());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
slice(start, end) {
|
|
101
|
+
return new SymbolicValue(["call", "slice", this.expr, start, end], String(this.sample).slice(start, end));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
toLowerCase() {
|
|
105
|
+
return new SymbolicValue(["call", "toLowerCase", this.expr], String(this.sample).toLowerCase());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
toUpperCase() {
|
|
109
|
+
return new SymbolicValue(["call", "toUpperCase", this.expr], String(this.sample).toUpperCase());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
includes(value) {
|
|
113
|
+
return String(this.sample).includes(String(value instanceof SymbolicValue ? value.sample : value));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get length() {
|
|
117
|
+
return String(this.sample).length;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
valueOf() {
|
|
121
|
+
return this.sample;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
toString() {
|
|
125
|
+
return String(this.sample);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createSymbolicArg(index) {
|
|
130
|
+
return new SymbolicValue(["arg", index], index === 0 ? "sample-value" : `sample-value-${index}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function createSymbolicAuth() {
|
|
134
|
+
const userId = new SymbolicValue(["auth", "userId"], "guest:trace");
|
|
135
|
+
const displayName = new SymbolicValue(["auth", "displayName"], "Trace Guest");
|
|
136
|
+
return { displayName, userId };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createSymbolicRow({ auth, idExpr, scanId, schema, tableName }) {
|
|
140
|
+
const fields = schema?.[tableName]?.fields ?? {};
|
|
141
|
+
const row = {
|
|
142
|
+
id: idExpr ?? new SymbolicValue(["row", scanId, "id"], "row-trace"),
|
|
143
|
+
createdAt: new SymbolicValue(["row", scanId, "createdAt"], "2026-01-01T00:00:00.000Z"),
|
|
144
|
+
updatedAt: new SymbolicValue(["row", scanId, "updatedAt"], "2026-01-01T00:00:00.000Z")
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
148
|
+
if (fieldName === "ownerId" || fieldName === "authorId") {
|
|
149
|
+
row[fieldName] = auth.userId;
|
|
150
|
+
} else if (fieldName === "authorName") {
|
|
151
|
+
row[fieldName] = auth.displayName;
|
|
152
|
+
} else if (field.kind === "boolean") {
|
|
153
|
+
row[fieldName] = true;
|
|
154
|
+
} else {
|
|
155
|
+
row[fieldName] = new SymbolicValue(["row", scanId, fieldName], `${fieldName}-trace`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return row;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
class QueryTrace {
|
|
163
|
+
constructor({ filters = [], limit = null, orderBy = null, recorder, tableName }) {
|
|
164
|
+
this.filters = filters;
|
|
165
|
+
this.limitValue = limit;
|
|
166
|
+
this.orderByValue = orderBy;
|
|
167
|
+
this.recorder = recorder;
|
|
168
|
+
this.tableName = tableName;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
where(field, value) {
|
|
172
|
+
return new QueryTrace({
|
|
173
|
+
filters: [...this.filters, { field, value: serializeValue(value) }],
|
|
174
|
+
limit: this.limitValue,
|
|
175
|
+
orderBy: this.orderByValue,
|
|
176
|
+
recorder: this.recorder,
|
|
177
|
+
tableName: this.tableName
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
orderBy(field, direction = "asc") {
|
|
182
|
+
return new QueryTrace({
|
|
183
|
+
filters: this.filters,
|
|
184
|
+
limit: this.limitValue,
|
|
185
|
+
orderBy: { field, direction: direction === "desc" ? "desc" : "asc" },
|
|
186
|
+
recorder: this.recorder,
|
|
187
|
+
tableName: this.tableName
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
limit(count) {
|
|
192
|
+
return new QueryTrace({
|
|
193
|
+
filters: this.filters,
|
|
194
|
+
limit: Number(count),
|
|
195
|
+
orderBy: this.orderByValue,
|
|
196
|
+
recorder: this.recorder,
|
|
197
|
+
tableName: this.tableName
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
toSpec() {
|
|
202
|
+
return {
|
|
203
|
+
op: "table.all",
|
|
204
|
+
table: this.tableName,
|
|
205
|
+
filters: this.filters,
|
|
206
|
+
orderBy: this.orderByValue,
|
|
207
|
+
limit: this.limitValue
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
all() {
|
|
212
|
+
const spec = this.toSpec();
|
|
213
|
+
if (this.recorder.mode === "query") {
|
|
214
|
+
this.recorder.query = spec;
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const scanId = `scan_${this.recorder.nextScanId}`;
|
|
219
|
+
this.recorder.nextScanId += 1;
|
|
220
|
+
this.recorder.scans.set(scanId, spec);
|
|
221
|
+
this.recorder.operations.push({ op: "scan", scanId, query: spec });
|
|
222
|
+
return [createSymbolicRow({ auth: this.recorder.auth, scanId, schema: this.recorder.schema, tableName: this.tableName })];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
class TableTrace extends QueryTrace {
|
|
227
|
+
constructor({ recorder, tableName }) {
|
|
228
|
+
super({ recorder, tableName });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
get(id) {
|
|
232
|
+
const idExpr = serializeValue(id);
|
|
233
|
+
this.recorder.operations.push({ id: idExpr, op: "get", table: this.tableName });
|
|
234
|
+
return createSymbolicRow({
|
|
235
|
+
auth: this.recorder.auth,
|
|
236
|
+
idExpr: id instanceof SymbolicValue ? id : new SymbolicValue(idExpr, "row-trace"),
|
|
237
|
+
scanId: `get_${this.recorder.operations.length}`,
|
|
238
|
+
schema: this.recorder.schema,
|
|
239
|
+
tableName: this.tableName
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
insert(value) {
|
|
244
|
+
const values = serializeValue(value);
|
|
245
|
+
this.recorder.operations.push({ op: "insert", table: this.tableName, values });
|
|
246
|
+
return createSymbolicRow({
|
|
247
|
+
auth: this.recorder.auth,
|
|
248
|
+
scanId: `insert_${this.recorder.operations.length}`,
|
|
249
|
+
schema: this.recorder.schema,
|
|
250
|
+
tableName: this.tableName
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
update(id, patch) {
|
|
255
|
+
this.recorder.operations.push({
|
|
256
|
+
id: serializeValue(id),
|
|
257
|
+
op: "update",
|
|
258
|
+
patch: serializeValue(patch),
|
|
259
|
+
table: this.tableName
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
delete(id) {
|
|
264
|
+
this.recorder.operations.push({
|
|
265
|
+
id: serializeValue(id),
|
|
266
|
+
op: "delete",
|
|
267
|
+
table: this.tableName
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function createTraceContext({ mode, schema }) {
|
|
273
|
+
const recorder = {
|
|
274
|
+
auth: createSymbolicAuth(),
|
|
275
|
+
mode,
|
|
276
|
+
nextScanId: 1,
|
|
277
|
+
operations: [],
|
|
278
|
+
query: null,
|
|
279
|
+
scans: new Map(),
|
|
280
|
+
schema
|
|
281
|
+
};
|
|
282
|
+
const db = {};
|
|
283
|
+
|
|
284
|
+
for (const tableName of Object.keys(schema ?? {})) {
|
|
285
|
+
db[tableName] = new TableTrace({ recorder, tableName });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
ctx: {
|
|
290
|
+
auth: recorder.auth,
|
|
291
|
+
db,
|
|
292
|
+
env: {},
|
|
293
|
+
log: {
|
|
294
|
+
error() {},
|
|
295
|
+
info() {},
|
|
296
|
+
warn() {}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
recorder
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function diagnostic(file, message) {
|
|
304
|
+
return { file, message };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function readSourceFiles(sourceStore) {
|
|
308
|
+
const paths = (await sourceStore.listFiles()).filter((path) => !path.startsWith("__lakebed/"));
|
|
309
|
+
const files = [];
|
|
310
|
+
|
|
311
|
+
for (const path of paths) {
|
|
312
|
+
const contents = await sourceStore.readFile(path);
|
|
313
|
+
files.push({
|
|
314
|
+
bytes: byteLength(contents),
|
|
315
|
+
contents,
|
|
316
|
+
hash: sha256(contents),
|
|
317
|
+
path
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function forbiddenSourceDiagnostics(files) {
|
|
325
|
+
const checks = [
|
|
326
|
+
[/\beval\s*\(/, "eval is not available in anonymous server code."],
|
|
327
|
+
[/\bFunction\s*\(/, "Function constructors are not available in anonymous server code."],
|
|
328
|
+
[/\bimport\s*\(/, "Dynamic import is not available in anonymous server code."],
|
|
329
|
+
[/\bfetch\s*\(/, "Outbound fetch is disabled for anonymous deploys."],
|
|
330
|
+
[/\basync\b/, "Async server handlers are not part of the anonymous IR yet. Use synchronous Lakebed database operations."],
|
|
331
|
+
[/\bwhile\s*\(/, "while loops are not available in anonymous server code."],
|
|
332
|
+
[/\bfor\s*\(\s*;/, "Unbounded for loops are not available in anonymous server code."],
|
|
333
|
+
[/\bprocess\b/, "process is not available in anonymous server code."],
|
|
334
|
+
[/\bglobalThis\b/, "globalThis is not available in anonymous server code."],
|
|
335
|
+
[/\bsetTimeout\s*\(/, "Timers are not available in anonymous server code."],
|
|
336
|
+
[/\bsetInterval\s*\(/, "Timers are not available in anonymous server code."],
|
|
337
|
+
[/from\s+["']node:/, "Node built-ins are not available in anonymous server code."]
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
const diagnostics = [];
|
|
341
|
+
for (const file of files.filter((candidate) => candidate.path.startsWith("server/") || candidate.path.startsWith("shared/"))) {
|
|
342
|
+
for (const [pattern, message] of checks) {
|
|
343
|
+
if (pattern.test(file.contents)) {
|
|
344
|
+
diagnostics.push(diagnostic(file.path, message));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return diagnostics;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function serializeSchema(schema) {
|
|
353
|
+
const cleanSchema = {};
|
|
354
|
+
const diagnostics = [];
|
|
355
|
+
|
|
356
|
+
for (const [tableName, table] of Object.entries(schema ?? {})) {
|
|
357
|
+
if (table?.kind !== "table" || !isPlainObject(table.fields ?? {})) {
|
|
358
|
+
diagnostics.push(diagnostic("server/index.ts", `Anonymous deploys only support Lakebed table() schema entries. Check schema.${tableName}.`));
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const fields = {};
|
|
363
|
+
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
364
|
+
if (!field || (field.kind !== "string" && field.kind !== "boolean")) {
|
|
365
|
+
diagnostics.push(
|
|
366
|
+
diagnostic("server/index.ts", `Anonymous deploys only support string() and boolean() fields. Check ${tableName}.${fieldName}.`)
|
|
367
|
+
);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (typeof field.defaultValue === "function") {
|
|
372
|
+
diagnostics.push(
|
|
373
|
+
diagnostic("server/index.ts", `Anonymous deploys do not support function defaults yet. Check ${tableName}.${fieldName}.`)
|
|
374
|
+
);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
fields[fieldName] = {
|
|
379
|
+
defaultValue: field.defaultValue,
|
|
380
|
+
kind: field.kind
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
cleanSchema[tableName] = { kind: "table", fields };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { diagnostics, schema: cleanSchema };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function inferMutationGuards(handler) {
|
|
391
|
+
const source = Function.prototype.toString.call(handler);
|
|
392
|
+
const guards = [];
|
|
393
|
+
if (source.includes("ownerId") && source.includes("auth.userId")) {
|
|
394
|
+
guards.push({ field: "ownerId", equalsAuth: "userId", op: "rowFieldEqualsAuth" });
|
|
395
|
+
}
|
|
396
|
+
if (source.includes("authorId") && source.includes("auth.userId")) {
|
|
397
|
+
guards.push({ field: "authorId", equalsAuth: "userId", op: "rowFieldEqualsAuth" });
|
|
398
|
+
}
|
|
399
|
+
return guards;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function compileQueryHandler({ handler, name, schema }) {
|
|
403
|
+
const { ctx, recorder } = createTraceContext({ mode: "query", schema });
|
|
404
|
+
try {
|
|
405
|
+
handler(ctx);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
throw new Error(`Unable to compile query "${name}" to anonymous IR: ${error instanceof Error ? error.message : String(error)}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!recorder.query) {
|
|
411
|
+
throw new Error(`Unable to compile query "${name}": query handlers must end with a ctx.db.<table>...all() call.`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return recorder.query;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function compileMutationHandler({ handler, name, schema }) {
|
|
418
|
+
const { ctx, recorder } = createTraceContext({ mode: "mutation", schema });
|
|
419
|
+
const args = Array.from({ length: Math.max(0, handler.length - 1) }, (_, index) => createSymbolicArg(index));
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
handler(ctx, ...args);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
throw new Error(`Unable to compile mutation "${name}" to anonymous IR: ${error instanceof Error ? error.message : String(error)}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const guards = inferMutationGuards(handler);
|
|
428
|
+
const operations = [];
|
|
429
|
+
|
|
430
|
+
for (const operation of recorder.operations) {
|
|
431
|
+
if (operation.op === "scan" || operation.op === "get") {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (operation.op === "delete" && Array.isArray(operation.id) && operation.id[0] === "row") {
|
|
436
|
+
const query = recorder.scans.get(operation.id[1]);
|
|
437
|
+
if (!query) {
|
|
438
|
+
throw new Error(`Unable to compile mutation "${name}": delete uses an unknown scanned row.`);
|
|
439
|
+
}
|
|
440
|
+
operations.push({ op: "deleteWhere", query, table: operation.table });
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (operation.op === "update") {
|
|
445
|
+
operations.push({ ...operation, guards });
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
operations.push(operation);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (operations.length === 0) {
|
|
453
|
+
throw new Error(`Unable to compile mutation "${name}": no supported database operation was recorded.`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
op: "mutation",
|
|
458
|
+
params: args.map((_, index) => ({ name: `arg${index}`, type: "unknown" })),
|
|
459
|
+
body: operations
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function compileServerToIr(app, schema) {
|
|
464
|
+
const queries = {};
|
|
465
|
+
const mutations = {};
|
|
466
|
+
const diagnostics = [];
|
|
467
|
+
|
|
468
|
+
for (const [name, handler] of Object.entries(app.queries ?? {})) {
|
|
469
|
+
try {
|
|
470
|
+
queries[name] = compileQueryHandler({ handler, name, schema });
|
|
471
|
+
} catch (error) {
|
|
472
|
+
diagnostics.push(diagnostic("server/index.ts", error instanceof Error ? error.message : String(error)));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
for (const [name, handler] of Object.entries(app.mutations ?? {})) {
|
|
477
|
+
try {
|
|
478
|
+
mutations[name] = compileMutationHandler({ handler, name, schema });
|
|
479
|
+
} catch (error) {
|
|
480
|
+
diagnostics.push(diagnostic("server/index.ts", error instanceof Error ? error.message : String(error)));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { diagnostics, mutations, queries };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export async function createAnonymousArtifact({ app, clientOut, sourceStore, version = "0.0.1" }) {
|
|
488
|
+
const sourceFiles = await readSourceFiles(sourceStore);
|
|
489
|
+
const diagnostics = forbiddenSourceDiagnostics(sourceFiles);
|
|
490
|
+
const { diagnostics: schemaDiagnostics, schema } = serializeSchema(app.schema);
|
|
491
|
+
diagnostics.push(...schemaDiagnostics);
|
|
492
|
+
|
|
493
|
+
if (diagnostics.length > 0) {
|
|
494
|
+
throw new AnonymousCompilerError(diagnostics);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const compiled = compileServerToIr(app, schema);
|
|
498
|
+
diagnostics.push(...compiled.diagnostics);
|
|
499
|
+
|
|
500
|
+
if (diagnostics.length > 0) {
|
|
501
|
+
throw new AnonymousCompilerError(diagnostics);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const clientBundle = await readFile(clientOut);
|
|
505
|
+
const clientBundleBase64 = clientBundle.toString("base64");
|
|
506
|
+
const clientBundleHash = sha256(clientBundle);
|
|
507
|
+
const sourceManifest = sourceFiles.map(({ bytes, hash, path }) => ({ bytes, hash, path }));
|
|
508
|
+
const sourceSnapshotHash = sha256(stableStringify(sourceManifest));
|
|
509
|
+
const artifact = {
|
|
510
|
+
name: app.name ?? "Lakebed Capsule",
|
|
511
|
+
client: {
|
|
512
|
+
bundleHash: clientBundleHash,
|
|
513
|
+
bytes: clientBundle.byteLength,
|
|
514
|
+
entry: "/client.js"
|
|
515
|
+
},
|
|
516
|
+
createdWith: {
|
|
517
|
+
compiler: "0.1.0",
|
|
518
|
+
lakebed: version
|
|
519
|
+
},
|
|
520
|
+
deployTarget: "anonymous-interpreter",
|
|
521
|
+
format: ANONYMOUS_ARTIFACT_FORMAT,
|
|
522
|
+
limits: {
|
|
523
|
+
instructionBudget: DEFAULT_ANONYMOUS_LIMITS.instructionBudget,
|
|
524
|
+
maxRowsReturned: DEFAULT_ANONYMOUS_LIMITS.rowsReturned,
|
|
525
|
+
maxValueBytes: DEFAULT_ANONYMOUS_LIMITS.maxValueBytes
|
|
526
|
+
},
|
|
527
|
+
server: {
|
|
528
|
+
helpers: {},
|
|
529
|
+
imports: ["lakebed/server"],
|
|
530
|
+
mutations: compiled.mutations,
|
|
531
|
+
queries: compiled.queries,
|
|
532
|
+
schema
|
|
533
|
+
},
|
|
534
|
+
source: {
|
|
535
|
+
files: sourceManifest,
|
|
536
|
+
snapshotHash: sourceSnapshotHash
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
const artifactHash = sha256(stableStringify(artifact));
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
artifact,
|
|
543
|
+
artifactHash,
|
|
544
|
+
clientBundle: clientBundleBase64,
|
|
545
|
+
clientBundleHash
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function validateExpression(expr, path, diagnostics) {
|
|
550
|
+
if (!isExpression(expr)) {
|
|
551
|
+
diagnostics.push(diagnostic(path, "Expected an anonymous IR expression."));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const [op] = expr;
|
|
556
|
+
if (op === "arg") {
|
|
557
|
+
if (!Number.isInteger(expr[1]) || expr.length !== 2) {
|
|
558
|
+
diagnostics.push(diagnostic(path, "Invalid arg expression."));
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (op === "auth") {
|
|
564
|
+
if ((expr[1] !== "userId" && expr[1] !== "displayName") || expr.length !== 2) {
|
|
565
|
+
diagnostics.push(diagnostic(path, "Invalid auth expression."));
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (op === "row") {
|
|
571
|
+
if (typeof expr[1] !== "string" || typeof expr[2] !== "string" || expr.length !== 3) {
|
|
572
|
+
diagnostics.push(diagnostic(path, "Invalid row expression."));
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (op === "call") {
|
|
578
|
+
const method = expr[1];
|
|
579
|
+
if (!["trim", "slice", "toLowerCase", "toUpperCase"].includes(method)) {
|
|
580
|
+
diagnostics.push(diagnostic(path, `Unsupported call expression: ${method}`));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
validateExpression(expr[2], path, diagnostics);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function validateValue(value, path, diagnostics) {
|
|
588
|
+
if (isExpression(value)) {
|
|
589
|
+
validateExpression(value, path, diagnostics);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (Array.isArray(value)) {
|
|
594
|
+
for (const item of value) {
|
|
595
|
+
validateValue(item, path, diagnostics);
|
|
596
|
+
}
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (isPlainObject(value)) {
|
|
601
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
602
|
+
validateValue(entryValue, `${path}.${key}`, diagnostics);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function validateQuery(query, path, schema, diagnostics) {
|
|
608
|
+
if (!isPlainObject(query) || query.op !== "table.all" || !schema[query.table]) {
|
|
609
|
+
diagnostics.push(diagnostic(path, "Invalid anonymous query IR."));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
for (const filter of query.filters ?? []) {
|
|
614
|
+
if (!filter || typeof filter.field !== "string") {
|
|
615
|
+
diagnostics.push(diagnostic(path, "Invalid query filter."));
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
validateValue(filter.value, path, diagnostics);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (query.orderBy && (typeof query.orderBy.field !== "string" || !["asc", "desc"].includes(query.orderBy.direction))) {
|
|
622
|
+
diagnostics.push(diagnostic(path, "Invalid query orderBy."));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (query.limit !== null && query.limit !== undefined && (!Number.isInteger(query.limit) || query.limit <= 0)) {
|
|
626
|
+
diagnostics.push(diagnostic(path, "Invalid query limit."));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export function validateAnonymousArtifact(artifact) {
|
|
631
|
+
const diagnostics = [];
|
|
632
|
+
|
|
633
|
+
if (!isPlainObject(artifact)) {
|
|
634
|
+
return [diagnostic("artifact", "Artifact must be a JSON object.")];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (artifact.format !== ANONYMOUS_ARTIFACT_FORMAT) {
|
|
638
|
+
diagnostics.push(diagnostic("artifact.format", `Expected ${ANONYMOUS_ARTIFACT_FORMAT}.`));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (artifact.deployTarget !== "anonymous-interpreter") {
|
|
642
|
+
diagnostics.push(diagnostic("artifact.deployTarget", "Anonymous deploys require deployTarget anonymous-interpreter."));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const schema = artifact.server?.schema;
|
|
646
|
+
if (!isPlainObject(schema)) {
|
|
647
|
+
diagnostics.push(diagnostic("artifact.server.schema", "Artifact must include a server schema."));
|
|
648
|
+
} else {
|
|
649
|
+
for (const [tableName, table] of Object.entries(schema)) {
|
|
650
|
+
if (table?.kind !== "table" || !isPlainObject(table.fields)) {
|
|
651
|
+
diagnostics.push(diagnostic(`artifact.server.schema.${tableName}`, "Invalid table schema."));
|
|
652
|
+
}
|
|
653
|
+
for (const [fieldName, field] of Object.entries(table.fields ?? {})) {
|
|
654
|
+
if (field?.kind !== "string" && field?.kind !== "boolean") {
|
|
655
|
+
diagnostics.push(diagnostic(`artifact.server.schema.${tableName}.${fieldName}`, "Unsupported field kind."));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const [name, query] of Object.entries(artifact.server?.queries ?? {})) {
|
|
662
|
+
validateQuery(query, `artifact.server.queries.${name}`, schema ?? {}, diagnostics);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
for (const [name, mutation] of Object.entries(artifact.server?.mutations ?? {})) {
|
|
666
|
+
if (mutation?.op !== "mutation" || !Array.isArray(mutation.body)) {
|
|
667
|
+
diagnostics.push(diagnostic(`artifact.server.mutations.${name}`, "Invalid mutation IR."));
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
for (const [index, operation] of mutation.body.entries()) {
|
|
672
|
+
const path = `artifact.server.mutations.${name}.body.${index}`;
|
|
673
|
+
if (!["insert", "update", "delete", "deleteWhere"].includes(operation.op) || !schema?.[operation.table]) {
|
|
674
|
+
diagnostics.push(diagnostic(path, `Unsupported mutation operation: ${operation.op}`));
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (operation.op === "insert") {
|
|
679
|
+
validateValue(operation.values, path, diagnostics);
|
|
680
|
+
} else if (operation.op === "update") {
|
|
681
|
+
validateValue(operation.id, path, diagnostics);
|
|
682
|
+
validateValue(operation.patch, path, diagnostics);
|
|
683
|
+
} else if (operation.op === "delete") {
|
|
684
|
+
validateValue(operation.id, path, diagnostics);
|
|
685
|
+
} else if (operation.op === "deleteWhere") {
|
|
686
|
+
validateQuery(operation.query, path, schema, diagnostics);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (byteLength(artifact) > DEFAULT_ANONYMOUS_LIMITS.artifactBytes) {
|
|
692
|
+
diagnostics.push(diagnostic("artifact", `Artifact exceeds ${DEFAULT_ANONYMOUS_LIMITS.artifactBytes} bytes.`));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return diagnostics;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function validateAnonymousDeployPayload(payload) {
|
|
699
|
+
if (!payload || typeof payload !== "object") {
|
|
700
|
+
throw new Error("Deploy payload must be a JSON object.");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const diagnostics = validateAnonymousArtifact(payload.artifact);
|
|
704
|
+
if (diagnostics.length > 0) {
|
|
705
|
+
throw new AnonymousCompilerError(diagnostics);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const clientBundle = Buffer.from(String(payload.clientBundle ?? ""), "base64");
|
|
709
|
+
const clientBundleHash = sha256(clientBundle);
|
|
710
|
+
if (clientBundleHash !== payload.artifact.client?.bundleHash) {
|
|
711
|
+
throw new Error("Client bundle hash does not match artifact.client.bundleHash.");
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (clientBundle.byteLength !== payload.artifact.client?.bytes) {
|
|
715
|
+
throw new Error("Client bundle byte count does not match artifact.client.bytes.");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const artifactHash = sha256(stableStringify(payload.artifact));
|
|
719
|
+
return {
|
|
720
|
+
artifact: cloneJson(payload.artifact),
|
|
721
|
+
artifactHash,
|
|
722
|
+
clientBundle,
|
|
723
|
+
clientBundleBase64: clientBundle.toString("base64"),
|
|
724
|
+
clientBundleHash
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export function parseTtlSeconds(value, fallback = 7 * 24 * 60 * 60) {
|
|
729
|
+
if (value === undefined || value === null || value === "") {
|
|
730
|
+
return fallback;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (typeof value === "number") {
|
|
734
|
+
return Math.max(60, Math.min(fallback, Math.floor(value)));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const match = String(value).trim().match(/^(\d+)([smhd])?$/);
|
|
738
|
+
if (!match) {
|
|
739
|
+
throw new Error(`Invalid TTL: ${value}. Use a value like 1h, 3d, or 604800.`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const amount = Number(match[1]);
|
|
743
|
+
const unit = match[2] ?? "s";
|
|
744
|
+
const multipliers = { d: 86400, h: 3600, m: 60, s: 1 };
|
|
745
|
+
return Math.max(60, Math.min(fallback, amount * multipliers[unit]));
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export function createDeployId() {
|
|
749
|
+
return `dep_${randomBytes(5).toString("base64url")}`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export function createClaimToken() {
|
|
753
|
+
return `tok_${randomBytes(24).toString("base64url")}`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export function createSlug() {
|
|
757
|
+
const adjectives = ["frosted", "quiet", "bright", "lucky", "silver", "rapid", "clear", "open"];
|
|
758
|
+
const nouns = ["river", "field", "ridge", "harbor", "garden", "signal", "meadow", "orbit"];
|
|
759
|
+
const bytes = randomBytes(4);
|
|
760
|
+
return `${adjectives[bytes[0] % adjectives.length]}-${nouns[bytes[1] % nouns.length]}-${bytes.toString("base64url").slice(0, 6).toLowerCase()}`;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export function hashClaimToken(token) {
|
|
764
|
+
return sha256(token);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function evaluateExpression(expr, { args, auth, row }) {
|
|
768
|
+
if (!isExpression(expr)) {
|
|
769
|
+
return expr;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const [op] = expr;
|
|
773
|
+
if (op === "arg") {
|
|
774
|
+
return args[expr[1]];
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (op === "auth") {
|
|
778
|
+
return auth[expr[1]];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (op === "row") {
|
|
782
|
+
return row?.[expr[2]];
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (op === "call") {
|
|
786
|
+
const value = evaluateExpression(expr[2], { args, auth, row });
|
|
787
|
+
if (expr[1] === "trim") {
|
|
788
|
+
return String(value ?? "").trim();
|
|
789
|
+
}
|
|
790
|
+
if (expr[1] === "slice") {
|
|
791
|
+
return String(value ?? "").slice(expr[3], expr[4]);
|
|
792
|
+
}
|
|
793
|
+
if (expr[1] === "toLowerCase") {
|
|
794
|
+
return String(value ?? "").toLowerCase();
|
|
795
|
+
}
|
|
796
|
+
if (expr[1] === "toUpperCase") {
|
|
797
|
+
return String(value ?? "").toUpperCase();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
throw new Error(`Unsupported anonymous expression: ${JSON.stringify(expr)}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function evaluateValue(value, context) {
|
|
805
|
+
if (isExpression(value)) {
|
|
806
|
+
return evaluateExpression(value, context);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (Array.isArray(value)) {
|
|
810
|
+
return value.map((item) => evaluateValue(item, context));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (isPlainObject(value)) {
|
|
814
|
+
const object = {};
|
|
815
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
816
|
+
object[key] = evaluateValue(entryValue, context);
|
|
817
|
+
}
|
|
818
|
+
return object;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return value;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function metadataFields() {
|
|
825
|
+
return new Set(["id", "createdAt", "updatedAt"]);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function assertFieldValue(tableName, fieldName, field, value) {
|
|
829
|
+
if (value === undefined) {
|
|
830
|
+
throw new Error(`Missing value for ${tableName}.${fieldName}`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (field.kind === "string" && typeof value !== "string") {
|
|
834
|
+
throw new Error(`Expected ${tableName}.${fieldName} to be a string.`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (field.kind === "boolean" && typeof value !== "boolean") {
|
|
838
|
+
throw new Error(`Expected ${tableName}.${fieldName} to be a boolean.`);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (byteLength(value) > DEFAULT_ANONYMOUS_LIMITS.maxValueBytes) {
|
|
842
|
+
throw new Error(`Value for ${tableName}.${fieldName} exceeds ${DEFAULT_ANONYMOUS_LIMITS.maxValueBytes} bytes.`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function prepareInsert(schema, tableName, value) {
|
|
847
|
+
const table = schema[tableName];
|
|
848
|
+
if (!table) {
|
|
849
|
+
throw new Error(`Unknown table: ${tableName}`);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const fields = table.fields ?? {};
|
|
853
|
+
const metadata = metadataFields();
|
|
854
|
+
for (const key of Object.keys(value)) {
|
|
855
|
+
if (!fields[key] && !metadata.has(key)) {
|
|
856
|
+
throw new Error(`Unknown field for ${tableName}: ${key}`);
|
|
857
|
+
}
|
|
858
|
+
if (metadata.has(key)) {
|
|
859
|
+
throw new Error(`Lakebed manages ${tableName}.${key}; app code cannot set it directly.`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const timestamp = new Date().toISOString();
|
|
864
|
+
const row = {
|
|
865
|
+
id: randomUUID(),
|
|
866
|
+
createdAt: timestamp,
|
|
867
|
+
updatedAt: timestamp
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
871
|
+
const valueOrDefault = value[fieldName] ?? field.defaultValue;
|
|
872
|
+
assertFieldValue(tableName, fieldName, field, valueOrDefault);
|
|
873
|
+
row[fieldName] = valueOrDefault;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return row;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function preparePatch(schema, tableName, patch) {
|
|
880
|
+
const table = schema[tableName];
|
|
881
|
+
if (!table) {
|
|
882
|
+
throw new Error(`Unknown table: ${tableName}`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const cleanPatch = {};
|
|
886
|
+
const fields = table.fields ?? {};
|
|
887
|
+
const metadata = metadataFields();
|
|
888
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
889
|
+
if (!fields[key] && !metadata.has(key)) {
|
|
890
|
+
throw new Error(`Unknown field for ${tableName}: ${key}`);
|
|
891
|
+
}
|
|
892
|
+
if (metadata.has(key)) {
|
|
893
|
+
throw new Error(`Lakebed manages ${tableName}.${key}; app code cannot update it directly.`);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
assertFieldValue(tableName, key, fields[key], value);
|
|
897
|
+
cleanPatch[key] = value;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
cleanPatch.updatedAt = new Date().toISOString();
|
|
901
|
+
return cleanPatch;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function compareValues(left, right) {
|
|
905
|
+
if (left === right) {
|
|
906
|
+
return 0;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return left > right ? 1 : -1;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function executeQuerySpec({ args = [], artifact, auth, query, state, deployId }) {
|
|
913
|
+
const rows = await state.listRows(deployId, query.table);
|
|
914
|
+
let results = rows.map((row) => ({ ...row }));
|
|
915
|
+
|
|
916
|
+
for (const filter of query.filters ?? []) {
|
|
917
|
+
const expected = evaluateValue(filter.value, { args, auth });
|
|
918
|
+
results = results.filter((row) => row[filter.field] === expected);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (query.orderBy) {
|
|
922
|
+
const direction = query.orderBy.direction === "desc" ? -1 : 1;
|
|
923
|
+
results = [...results].sort((left, right) => compareValues(left[query.orderBy.field], right[query.orderBy.field]) * direction);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const limit = Math.min(
|
|
927
|
+
query.limit ?? artifact.limits?.maxRowsReturned ?? DEFAULT_ANONYMOUS_LIMITS.rowsReturned,
|
|
928
|
+
artifact.limits?.maxRowsReturned ?? DEFAULT_ANONYMOUS_LIMITS.rowsReturned
|
|
929
|
+
);
|
|
930
|
+
return results.slice(0, limit);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export async function executeAnonymousQuery({ args = [], artifact, auth, deployId, name, state }) {
|
|
934
|
+
const query = artifact.server.queries?.[name];
|
|
935
|
+
if (!query) {
|
|
936
|
+
throw new Error(`Unknown query: ${name}`);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return executeQuerySpec({ args, artifact, auth, deployId, query, state });
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function checkUpdateGuards({ auth, guards = [], row }) {
|
|
943
|
+
for (const guard of guards) {
|
|
944
|
+
if (guard.op === "rowFieldEqualsAuth" && row?.[guard.field] !== auth[guard.equalsAuth]) {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export async function executeAnonymousMutation({ args = [], artifact, auth, deployId, name, state }) {
|
|
952
|
+
const mutation = artifact.server.mutations?.[name];
|
|
953
|
+
if (!mutation) {
|
|
954
|
+
throw new Error(`Unknown mutation: ${name}`);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return state.transaction(deployId, async (tx) => {
|
|
958
|
+
for (const operation of mutation.body) {
|
|
959
|
+
if (operation.op === "insert") {
|
|
960
|
+
const values = evaluateValue(operation.values, { args, auth });
|
|
961
|
+
const row = prepareInsert(artifact.server.schema, operation.table, values);
|
|
962
|
+
await tx.insertRow(deployId, operation.table, row);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (operation.op === "update") {
|
|
967
|
+
const id = evaluateValue(operation.id, { args, auth });
|
|
968
|
+
const row = await tx.getRow(deployId, operation.table, id);
|
|
969
|
+
if (!row || !(await checkUpdateGuards({ auth, guards: operation.guards, row }))) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const patch = preparePatch(artifact.server.schema, operation.table, evaluateValue(operation.patch, { args, auth, row }));
|
|
973
|
+
await tx.updateRow(deployId, operation.table, id, patch);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (operation.op === "delete") {
|
|
978
|
+
const id = evaluateValue(operation.id, { args, auth });
|
|
979
|
+
await tx.deleteRow(deployId, operation.table, id);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (operation.op === "deleteWhere") {
|
|
984
|
+
const rows = await executeQuerySpec({ args, artifact, auth, deployId, query: operation.query, state: tx });
|
|
985
|
+
for (const row of rows) {
|
|
986
|
+
await tx.deleteRow(deployId, operation.table, row.id);
|
|
987
|
+
}
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
throw new Error(`Unsupported anonymous mutation op: ${operation.op}`);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return null;
|
|
995
|
+
});
|
|
996
|
+
}
|