@lsync/server 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/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/client.d.mts +2 -0
- package/dist/client.mjs +2 -0
- package/dist/index.d.mts +205 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1346 -0
- package/dist/index.mjs.map +1 -0
- package/dist/router-C4eYfMV7.mjs +39 -0
- package/dist/router-C4eYfMV7.mjs.map +1 -0
- package/dist/router-D_wddPdy.d.mts +211 -0
- package/dist/router-D_wddPdy.d.mts.map +1 -0
- package/package.json +49 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
import { n as webSocketAttachmentSchema, t as router } from "./router-C4eYfMV7.mjs";
|
|
2
|
+
import { and, compileReadExpression, compileReadExpression as compileReadExpression$1, eq, field, gt, gte, inArray, lt, lte, matchesReadPredicate, matchesReadPredicate as matchesReadPredicate$1, ne, not, or, parseClientRpcRequest, readExpressionReferences, readExpressionReferences as readExpressionReferences$1, readExpressionRow, readExpressionRow as readExpressionRow$1, readRpcId, sendServerMessage, val } from "@lsync/transport";
|
|
3
|
+
import sqlTag, { join, raw } from "sql-template-tag";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/access-expression.ts
|
|
6
|
+
function accessRows(referenceNames) {
|
|
7
|
+
return {
|
|
8
|
+
references: readExpressionReferences$1(referenceNames),
|
|
9
|
+
row: readExpressionRow$1()
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function reduceAccessExpression(expression, references) {
|
|
13
|
+
const dependencies = [];
|
|
14
|
+
const reduced = reduce(expression, references, dependencies);
|
|
15
|
+
if (isVal(reduced)) return {
|
|
16
|
+
allow: reduced.value === true,
|
|
17
|
+
dependencies
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
allow: true,
|
|
21
|
+
dependencies,
|
|
22
|
+
predicate: compileReadExpression$1(reduced)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function reduce(expression, references, dependencies) {
|
|
26
|
+
const unwrapped = unwrap(expression);
|
|
27
|
+
if (isVal(unwrapped)) return unwrapped;
|
|
28
|
+
if (isRef(unwrapped)) return reduceRef(unwrapped, references, dependencies);
|
|
29
|
+
if (unwrapped.name === "and") return reduceAnd(unwrapped, references, dependencies);
|
|
30
|
+
if (unwrapped.name === "or") return reduceOr(unwrapped, references, dependencies);
|
|
31
|
+
if (unwrapped.name === "not") {
|
|
32
|
+
const child = reduceExpressionArg(unwrapped.args[0], references, dependencies);
|
|
33
|
+
return isVal(child) ? val$1(child.value !== true) : func("not", child);
|
|
34
|
+
}
|
|
35
|
+
const args = unwrapped.args.map((arg) => reduceExpressionArg(arg, references, dependencies));
|
|
36
|
+
if (args.every(isVal)) return val$1(evaluate(unwrapped.name, args.map((arg) => arg.value)));
|
|
37
|
+
return func(unwrapped.name, ...args);
|
|
38
|
+
}
|
|
39
|
+
function reduceRef(expression, references, dependencies) {
|
|
40
|
+
if (expression.source !== "reference") return expression;
|
|
41
|
+
const name = expression.name;
|
|
42
|
+
const reference = name ? references.get(name) : void 0;
|
|
43
|
+
if (!name || !reference) return val$1(void 0);
|
|
44
|
+
const field = expression.path.join(".");
|
|
45
|
+
dependencies.push({
|
|
46
|
+
collection: reference.collection,
|
|
47
|
+
field,
|
|
48
|
+
key: reference.key,
|
|
49
|
+
name
|
|
50
|
+
});
|
|
51
|
+
return val$1(valueAtPath$1(reference.value, expression.path));
|
|
52
|
+
}
|
|
53
|
+
function reduceAnd(expression, references, dependencies) {
|
|
54
|
+
const reduced = expression.args.map((arg) => reduceExpressionArg(arg, references, dependencies));
|
|
55
|
+
if (reduced.some((arg) => isVal(arg) && arg.value !== true)) return val$1(false);
|
|
56
|
+
const remaining = reduced.filter((arg) => !isVal(arg));
|
|
57
|
+
if (remaining.length === 0) return val$1(true);
|
|
58
|
+
return remaining.length === 1 ? remaining[0] : func("and", ...remaining);
|
|
59
|
+
}
|
|
60
|
+
function reduceOr(expression, references, dependencies) {
|
|
61
|
+
const reduced = expression.args.map((arg) => reduceExpressionArg(arg, references, dependencies));
|
|
62
|
+
if (reduced.some((arg) => isVal(arg) && arg.value === true)) return val$1(true);
|
|
63
|
+
const remaining = reduced.filter((arg) => !isVal(arg));
|
|
64
|
+
if (remaining.length === 0) return val$1(false);
|
|
65
|
+
return remaining.length === 1 ? remaining[0] : func("or", ...remaining);
|
|
66
|
+
}
|
|
67
|
+
function reduceExpressionArg(value, references, dependencies) {
|
|
68
|
+
return isExpression(value) ? reduce(value, references, dependencies) : val$1(value);
|
|
69
|
+
}
|
|
70
|
+
function evaluate(name, args) {
|
|
71
|
+
const [left, right] = args;
|
|
72
|
+
switch (name) {
|
|
73
|
+
case "eq": return left === right;
|
|
74
|
+
case "not_eq": return left !== right;
|
|
75
|
+
case "gt": return compare(left, right, (a, b) => a > b);
|
|
76
|
+
case "gte": return compare(left, right, (a, b) => a >= b);
|
|
77
|
+
case "lt": return compare(left, right, (a, b) => a < b);
|
|
78
|
+
case "lte": return compare(left, right, (a, b) => a <= b);
|
|
79
|
+
case "in": return Array.isArray(right) && right.includes(left);
|
|
80
|
+
default: throw new Error(`Unsupported read access operator: ${name}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function compare(left, right, compareValues) {
|
|
84
|
+
if (typeof left === "number" && typeof right === "number") return compareValues(left, right);
|
|
85
|
+
if (typeof left === "string" && typeof right === "string") return compareValues(left, right);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
function func(name, ...args) {
|
|
89
|
+
return {
|
|
90
|
+
type: "func",
|
|
91
|
+
name,
|
|
92
|
+
args
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function val$1(value) {
|
|
96
|
+
return {
|
|
97
|
+
type: "val",
|
|
98
|
+
value
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function valueAtPath$1(value, path) {
|
|
102
|
+
return path.reduce((current, segment) => {
|
|
103
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
104
|
+
return current[String(segment)];
|
|
105
|
+
}, value);
|
|
106
|
+
}
|
|
107
|
+
function unwrap(expression) {
|
|
108
|
+
return "expression" in expression ? unwrap(expression.expression) : expression;
|
|
109
|
+
}
|
|
110
|
+
function isExpression(value) {
|
|
111
|
+
return typeof value === "object" && value !== null && ("type" in value || "expression" in value);
|
|
112
|
+
}
|
|
113
|
+
function isVal(value) {
|
|
114
|
+
return unwrap(value).type === "val";
|
|
115
|
+
}
|
|
116
|
+
function isRef(value) {
|
|
117
|
+
return unwrap(value).type === "ref";
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/collections.ts
|
|
121
|
+
function resolveCollection(collectionName, collections = {}) {
|
|
122
|
+
const exact = collections[collectionName];
|
|
123
|
+
if (exact) return {
|
|
124
|
+
name: collectionName,
|
|
125
|
+
collection: exact,
|
|
126
|
+
params: {},
|
|
127
|
+
scope: collectionScope(collectionName)
|
|
128
|
+
};
|
|
129
|
+
const best = Object.entries(collections).flatMap(([name, collection], index) => {
|
|
130
|
+
const match = matchCollectionPattern(name, collectionName);
|
|
131
|
+
return match ? [{
|
|
132
|
+
name,
|
|
133
|
+
collection,
|
|
134
|
+
params: match.params,
|
|
135
|
+
staticSegments: match.staticSegments,
|
|
136
|
+
segments: match.segments,
|
|
137
|
+
index
|
|
138
|
+
}] : [];
|
|
139
|
+
}).sort(comparePatternMatches)[0];
|
|
140
|
+
if (!best) return;
|
|
141
|
+
return {
|
|
142
|
+
name: best.name,
|
|
143
|
+
collection: best.collection,
|
|
144
|
+
params: best.params,
|
|
145
|
+
scope: collectionScope(collectionName)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function collectionScope(collectionName) {
|
|
149
|
+
return `/${splitCollectionPath(collectionName).map(normalizeSegment).join("/")}/`;
|
|
150
|
+
}
|
|
151
|
+
function isCollectionPattern(collectionName) {
|
|
152
|
+
return collectionName.split("/").some((segment) => /^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(segment));
|
|
153
|
+
}
|
|
154
|
+
function collectionApiHandlers(collections = {}) {
|
|
155
|
+
const handlers = {};
|
|
156
|
+
for (const [collectionPath, collection] of Object.entries(collections)) for (const [name, definition] of Object.entries(collection.api ?? {})) {
|
|
157
|
+
const path = defaultCollectionApiPath(collectionPath, name);
|
|
158
|
+
if (handlers[path]) throw new Error(`Duplicate API path: ${path}`);
|
|
159
|
+
handlers[path] = (args) => definition.handler({
|
|
160
|
+
...args,
|
|
161
|
+
input: definition.input.parse(args.input)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return handlers;
|
|
165
|
+
}
|
|
166
|
+
function defaultCollectionApiPath(collectionPath, name) {
|
|
167
|
+
const prefix = splitCollectionPath(collectionPath).filter((segment) => !collectionPathParamName(segment)).join(".");
|
|
168
|
+
return prefix ? `${prefix}.${name}` : name;
|
|
169
|
+
}
|
|
170
|
+
function matchCollectionPattern(pattern, collectionName) {
|
|
171
|
+
if (!isCollectionPattern(pattern)) return;
|
|
172
|
+
const patternSegments = splitCollectionPath(pattern);
|
|
173
|
+
const collectionSegments = splitCollectionPath(collectionName);
|
|
174
|
+
if (patternSegments.length !== collectionSegments.length) return;
|
|
175
|
+
const params = {};
|
|
176
|
+
let staticSegments = 0;
|
|
177
|
+
for (const [index, segment] of patternSegments.entries()) {
|
|
178
|
+
const param = segment.match(/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/);
|
|
179
|
+
if (param?.[1]) {
|
|
180
|
+
params[param[1]] = decodeURIComponent(collectionSegments[index] ?? "");
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (segment !== collectionSegments[index]) return;
|
|
184
|
+
staticSegments += 1;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
params,
|
|
188
|
+
staticSegments,
|
|
189
|
+
segments: patternSegments.length
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function comparePatternMatches(left, right) {
|
|
193
|
+
return right.staticSegments - left.staticSegments || right.segments - left.segments || left.index - right.index;
|
|
194
|
+
}
|
|
195
|
+
function splitCollectionPath(path) {
|
|
196
|
+
const trimmed = path.trim().replace(/^\/+|\/+$/g, "");
|
|
197
|
+
if (!trimmed) return [];
|
|
198
|
+
return trimmed.split("/");
|
|
199
|
+
}
|
|
200
|
+
function collectionPathParamName(segment) {
|
|
201
|
+
return segment.match(/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/)?.[1] ?? segment.match(/^:([A-Za-z_][A-Za-z0-9_]*)$/)?.[1];
|
|
202
|
+
}
|
|
203
|
+
function normalizeSegment(segment) {
|
|
204
|
+
return encodeURIComponent(decodeURIComponent(segment));
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/access.ts
|
|
208
|
+
function authorizeReadQuery(query, collections, auth, store) {
|
|
209
|
+
const decision = readAccessDecision(query.collection, collections, auth, store);
|
|
210
|
+
if (!decision.allow) return void 0;
|
|
211
|
+
if (!decision.predicate) return query;
|
|
212
|
+
const predicate = combinePredicates$1(decision.predicate, query.predicate);
|
|
213
|
+
return predicate ? {
|
|
214
|
+
...query,
|
|
215
|
+
predicate
|
|
216
|
+
} : query;
|
|
217
|
+
}
|
|
218
|
+
function visibleUpdateForAuth(update, collections, auth, store) {
|
|
219
|
+
const decision = readAccessDecision(update.collection, collections, auth, store);
|
|
220
|
+
if (!decision.allow) return void 0;
|
|
221
|
+
if (!decision.predicate) return update;
|
|
222
|
+
const before = update.previousValue ? matchesReadPredicate$1(update.previousValue, decision.predicate) : false;
|
|
223
|
+
const after = update.value ? matchesReadPredicate$1(update.value, decision.predicate) : false;
|
|
224
|
+
if (!before && !after) return void 0;
|
|
225
|
+
if (before && !after) {
|
|
226
|
+
const { previousValue, value, ...base } = update;
|
|
227
|
+
return {
|
|
228
|
+
...base,
|
|
229
|
+
type: "delete"
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return !before && after && update.type === "update" ? {
|
|
233
|
+
...update,
|
|
234
|
+
type: "insert"
|
|
235
|
+
} : update;
|
|
236
|
+
}
|
|
237
|
+
function authorizeWriteBatch(batch, collections, auth, store) {
|
|
238
|
+
for (const update of batch.updates) {
|
|
239
|
+
if (writeAccessDecision(update, collections, auth, store).allow) continue;
|
|
240
|
+
throw new Error(`Access denied for ${update.type} on ${update.collection}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function authorizeApiCall(handler, name, input, auth) {
|
|
244
|
+
if (!handler) return;
|
|
245
|
+
const row = asRecord(input) ?? {};
|
|
246
|
+
const decision = reduceAccessExpression(handler({
|
|
247
|
+
auth,
|
|
248
|
+
input: readExpressionRow$1()
|
|
249
|
+
}), /* @__PURE__ */ new Map());
|
|
250
|
+
if (!(decision.allow && (!decision.predicate || matchesReadPredicate$1(row, decision.predicate)))) throw new Error(`Access denied for API: ${name}`);
|
|
251
|
+
}
|
|
252
|
+
function readAccessDecision(collection, collections, auth, store) {
|
|
253
|
+
const resolved = resolveCollection(collection, collections);
|
|
254
|
+
if (!resolved) return {
|
|
255
|
+
allow: true,
|
|
256
|
+
dependencies: []
|
|
257
|
+
};
|
|
258
|
+
const access = resolved?.collection.access;
|
|
259
|
+
if (!access?.read) return {
|
|
260
|
+
allow: true,
|
|
261
|
+
dependencies: []
|
|
262
|
+
};
|
|
263
|
+
const references = accessReferences(collection, collections, auth);
|
|
264
|
+
const rows = accessRows([...references.keys()]);
|
|
265
|
+
return reduceAccessExpression(access.read({
|
|
266
|
+
auth,
|
|
267
|
+
collection: resolved.scope,
|
|
268
|
+
references: rows.references,
|
|
269
|
+
params: resolved.params,
|
|
270
|
+
pattern: resolved.name,
|
|
271
|
+
row: rows.row
|
|
272
|
+
}), new Map([...references].map(([name, reference]) => [name, {
|
|
273
|
+
...reference,
|
|
274
|
+
value: store.read(reference.collection, reference.key)
|
|
275
|
+
}])));
|
|
276
|
+
}
|
|
277
|
+
function writeAccessDecision(update, collections, auth, store) {
|
|
278
|
+
const resolved = resolveCollection(update.collection, collections);
|
|
279
|
+
if (!resolved) return {
|
|
280
|
+
allow: true,
|
|
281
|
+
dependencies: []
|
|
282
|
+
};
|
|
283
|
+
const access = resolved.collection.access;
|
|
284
|
+
const handler = writeAccessHandler(access, update.type);
|
|
285
|
+
if (!handler) return {
|
|
286
|
+
allow: true,
|
|
287
|
+
dependencies: []
|
|
288
|
+
};
|
|
289
|
+
const row = writeAccessRow(update, store);
|
|
290
|
+
if (!row) return {
|
|
291
|
+
allow: false,
|
|
292
|
+
dependencies: []
|
|
293
|
+
};
|
|
294
|
+
const references = accessReferences(update.collection, collections, auth);
|
|
295
|
+
const rows = accessRows([...references.keys()]);
|
|
296
|
+
const decision = reduceAccessExpression(handler({
|
|
297
|
+
auth,
|
|
298
|
+
collection: resolved.scope,
|
|
299
|
+
references: rows.references,
|
|
300
|
+
params: resolved.params,
|
|
301
|
+
pattern: resolved.name,
|
|
302
|
+
row: rows.row
|
|
303
|
+
}), new Map([...references].map(([name, reference]) => [name, {
|
|
304
|
+
...reference,
|
|
305
|
+
value: store.read(reference.collection, reference.key)
|
|
306
|
+
}])));
|
|
307
|
+
if (!decision.allow || !decision.predicate) return decision;
|
|
308
|
+
return matchesReadPredicate$1(row, decision.predicate) ? decision : {
|
|
309
|
+
...decision,
|
|
310
|
+
allow: false
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function accessReferences(collection, collections, auth) {
|
|
314
|
+
const resolved = resolveCollection(collection, collections);
|
|
315
|
+
const references = resolved?.collection.access?.references;
|
|
316
|
+
if (!resolved || !references) return /* @__PURE__ */ new Map();
|
|
317
|
+
return new Map(Object.entries(references).map(([name, resolver]) => [name, normalizeAccessReference(resolver({
|
|
318
|
+
auth,
|
|
319
|
+
collection: resolved.scope,
|
|
320
|
+
params: resolved.params,
|
|
321
|
+
pattern: resolved.name
|
|
322
|
+
}))]));
|
|
323
|
+
}
|
|
324
|
+
function normalizeAccessReference(reference) {
|
|
325
|
+
if (typeof reference !== "string") return reference;
|
|
326
|
+
const normalized = reference.trim().replace(/^\/+|\/+$/g, "");
|
|
327
|
+
const segments = normalized ? normalized.split("/") : [];
|
|
328
|
+
const key = segments.at(-1);
|
|
329
|
+
if (!key || segments.length < 2) throw new Error(`Invalid access reference path: ${reference}`);
|
|
330
|
+
return {
|
|
331
|
+
collection: `/${segments.slice(0, -1).map(encodeURIComponent).join("/")}/`,
|
|
332
|
+
key: decodeURIComponent(key)
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function combinePredicates$1(left, right) {
|
|
336
|
+
if (!left) return right;
|
|
337
|
+
if (!right) return left;
|
|
338
|
+
return {
|
|
339
|
+
type: "and",
|
|
340
|
+
predicates: [left, right]
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function writeAccessHandler(access, type) {
|
|
344
|
+
if (!access) return void 0;
|
|
345
|
+
return access[type] ?? access.write ?? access.read;
|
|
346
|
+
}
|
|
347
|
+
function writeAccessRow(update, store) {
|
|
348
|
+
if (update.type === "insert") return asRecord(update.value);
|
|
349
|
+
const existing = asRecord(update.previousValue) ?? store.read(update.collection, update.key);
|
|
350
|
+
if (update.type === "delete") return existing;
|
|
351
|
+
const next = asRecord(update.value);
|
|
352
|
+
return existing && next ? {
|
|
353
|
+
...existing,
|
|
354
|
+
...next
|
|
355
|
+
} : next ?? existing;
|
|
356
|
+
}
|
|
357
|
+
function asRecord(value) {
|
|
358
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
359
|
+
}
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/collection-normalize.ts
|
|
362
|
+
function normalizeCollection(collection, collections) {
|
|
363
|
+
const configured = resolveCollection(collection, collections);
|
|
364
|
+
if (configured) return configured.scope;
|
|
365
|
+
if (collections && Object.keys(collections).length > 0) throw new Error(`Unknown collection: ${collection}`);
|
|
366
|
+
return collectionScope(collection);
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/sqlite-sql.ts
|
|
370
|
+
function execSql(storage, statement) {
|
|
371
|
+
return storage.exec(statement.sql, ...statement.values);
|
|
372
|
+
}
|
|
373
|
+
function identifierSql(identifier) {
|
|
374
|
+
return raw(`"${identifier.replaceAll(`"`, `""`)}"`);
|
|
375
|
+
}
|
|
376
|
+
function rawSql(value) {
|
|
377
|
+
return raw(value);
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/sqlite-json-read.ts
|
|
381
|
+
function readSQLiteJsonRows(sql, query, collections = {}) {
|
|
382
|
+
ensureSQLiteJsonTables(sql, collections);
|
|
383
|
+
const resolved = resolveCollection(query.collection, collections);
|
|
384
|
+
if (!resolved) {
|
|
385
|
+
if (Object.keys(collections).length > 0) throw new Error(`Unknown collection: ${query.collection}`);
|
|
386
|
+
throw new Error(`Cannot read unconfigured collection: ${query.collection}`);
|
|
387
|
+
}
|
|
388
|
+
const storage = sqliteJsonStorage(resolved.collection.storage);
|
|
389
|
+
if (!storage) throw new Error(`Collection is not readable with SQLite JSON storage: ${query.collection}`);
|
|
390
|
+
const table = tableName$1(resolved.name, storage);
|
|
391
|
+
const filters = query.filters ?? [];
|
|
392
|
+
const predicate = query.predicate;
|
|
393
|
+
const orderBy = query.orderBy ?? [];
|
|
394
|
+
const limit = query.limit ?? 1e3;
|
|
395
|
+
const scope = { scope: resolved.scope };
|
|
396
|
+
if (query.cursor) {
|
|
397
|
+
const currentPredicate = combinePredicates(predicate, query.cursor.whereCurrent);
|
|
398
|
+
const fromPredicate = combinePredicates(predicate, query.cursor.whereFrom);
|
|
399
|
+
const currentRows = selectRows(sql, table, {
|
|
400
|
+
filters,
|
|
401
|
+
orderBy,
|
|
402
|
+
...scope,
|
|
403
|
+
...currentPredicate ? { predicate: currentPredicate } : {}
|
|
404
|
+
});
|
|
405
|
+
const fromRows = selectRows(sql, table, {
|
|
406
|
+
filters,
|
|
407
|
+
limit,
|
|
408
|
+
orderBy,
|
|
409
|
+
...scope,
|
|
410
|
+
...fromPredicate ? { predicate: fromPredicate } : {}
|
|
411
|
+
});
|
|
412
|
+
return { rows: uniqueRows([...currentRows, ...fromRows]).map((row) => JSON.parse(row.value)) };
|
|
413
|
+
}
|
|
414
|
+
return { rows: selectRows(sql, table, {
|
|
415
|
+
filters,
|
|
416
|
+
limit,
|
|
417
|
+
offset: query.offset ?? 0,
|
|
418
|
+
orderBy,
|
|
419
|
+
...scope,
|
|
420
|
+
...predicate ? { predicate } : {}
|
|
421
|
+
}).map((row) => JSON.parse(row.value)) };
|
|
422
|
+
}
|
|
423
|
+
function readSQLiteJsonRow(sql, collection, key, collections = {}) {
|
|
424
|
+
ensureSQLiteJsonTables(sql, collections);
|
|
425
|
+
const resolved = resolveCollection(collection, collections);
|
|
426
|
+
if (!resolved) {
|
|
427
|
+
if (Object.keys(collections).length > 0) throw new Error(`Unknown collection: ${collection}`);
|
|
428
|
+
throw new Error(`Cannot read unconfigured collection: ${collection}`);
|
|
429
|
+
}
|
|
430
|
+
if (resolved.collection.storage?.kind !== "sqlite-json") throw new Error(`Collection is not readable with SQLite JSON storage: ${collection}`);
|
|
431
|
+
const row = execSql(sql, sqlTag`
|
|
432
|
+
SELECT value FROM ${identifierSql(tableName$1(resolved.name, resolved.collection.storage))}
|
|
433
|
+
WHERE path = ${resolved.scope} AND key = ${String(key)}
|
|
434
|
+
`).toArray()[0];
|
|
435
|
+
return row ? JSON.parse(row.value) : void 0;
|
|
436
|
+
}
|
|
437
|
+
function selectRows(sql, table, options) {
|
|
438
|
+
const where = [sqlTag`path = ${options.scope}`];
|
|
439
|
+
where.push(...options.filters.map((filter) => filterSql(filter)));
|
|
440
|
+
if (options.predicate) where.push(predicateSql(options.predicate));
|
|
441
|
+
const order = options.orderBy.length > 0 ? join(options.orderBy.map((item) => orderBySql(item))) : sqlTag`key`;
|
|
442
|
+
return execSql(sql, sqlTag`
|
|
443
|
+
SELECT key, value FROM ${identifierSql(table)}
|
|
444
|
+
WHERE ${join(where, " AND ")}
|
|
445
|
+
ORDER BY ${order}
|
|
446
|
+
LIMIT ${options.limit ?? 1e3} OFFSET ${options.offset ?? 0}
|
|
447
|
+
`).toArray();
|
|
448
|
+
}
|
|
449
|
+
function uniqueRows(rows) {
|
|
450
|
+
const seen = /* @__PURE__ */ new Set();
|
|
451
|
+
const unique = [];
|
|
452
|
+
for (const row of rows) if (!seen.has(row.key)) {
|
|
453
|
+
seen.add(row.key);
|
|
454
|
+
unique.push(row);
|
|
455
|
+
}
|
|
456
|
+
return unique;
|
|
457
|
+
}
|
|
458
|
+
function tableName$1(collectionName, storage) {
|
|
459
|
+
return storage.tableName ?? collectionName;
|
|
460
|
+
}
|
|
461
|
+
function jsonPath$1(field) {
|
|
462
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(field)) throw new Error(`Invalid JSON index field: ${field}`);
|
|
463
|
+
return `$.${field}`;
|
|
464
|
+
}
|
|
465
|
+
function orderBySql(orderBy) {
|
|
466
|
+
const direction = orderBy.direction === "desc" ? "DESC" : "ASC";
|
|
467
|
+
return sqlTag`json_extract(value, ${jsonPath$1(orderBy.field)}) ${directionSql(direction)}`;
|
|
468
|
+
}
|
|
469
|
+
function predicateSql(predicate) {
|
|
470
|
+
if (predicate.type === "comparison") return filterSql(predicate);
|
|
471
|
+
const joiner = predicate.type === "and" ? " AND " : " OR ";
|
|
472
|
+
return sqlTag`(${join(predicate.predicates.map((child) => predicateSql(child)), joiner)})`;
|
|
473
|
+
}
|
|
474
|
+
function combinePredicates(left, right) {
|
|
475
|
+
if (!left) return right;
|
|
476
|
+
if (!right) return left;
|
|
477
|
+
return {
|
|
478
|
+
type: "and",
|
|
479
|
+
predicates: [left, right]
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function filterSql(filter) {
|
|
483
|
+
const field = sqlTag`json_extract(value, ${jsonPath$1(filter.field)})`;
|
|
484
|
+
if (filter.op === "in") {
|
|
485
|
+
if (!Array.isArray(filter.value) || filter.value.length === 0) throw new Error("The in filter requires a non-empty array value");
|
|
486
|
+
return sqlTag`${field} IN (${join(filter.value.map(toSqlJsonValue))})`;
|
|
487
|
+
}
|
|
488
|
+
const value = toSqlJsonValue(filter.value);
|
|
489
|
+
if (value === null) {
|
|
490
|
+
if (filter.op === "eq") return sqlTag`${field} IS ${value}`;
|
|
491
|
+
if (filter.op === "ne") return sqlTag`${field} IS NOT ${value}`;
|
|
492
|
+
}
|
|
493
|
+
switch (filter.op) {
|
|
494
|
+
case "eq": return sqlTag`${field} = ${value}`;
|
|
495
|
+
case "ne": return sqlTag`${field} != ${value}`;
|
|
496
|
+
case "gt": return sqlTag`${field} > ${value}`;
|
|
497
|
+
case "gte": return sqlTag`${field} >= ${value}`;
|
|
498
|
+
case "lt": return sqlTag`${field} < ${value}`;
|
|
499
|
+
case "lte": return sqlTag`${field} <= ${value}`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function directionSql(direction) {
|
|
503
|
+
return rawSql(direction);
|
|
504
|
+
}
|
|
505
|
+
function toSqlJsonValue(value) {
|
|
506
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
507
|
+
if (value === null || typeof value === "string" || typeof value === "number" || value instanceof ArrayBuffer) return value;
|
|
508
|
+
throw new Error(`Unsupported SQLite JSON filter value: ${typeof value}`);
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/storage.ts
|
|
512
|
+
function sqliteJsonTable(options = {}) {
|
|
513
|
+
return {
|
|
514
|
+
kind: "sqlite-json",
|
|
515
|
+
...options.tableName ? { tableName: options.tableName } : {},
|
|
516
|
+
indexes: (options.indexes ?? []).map((index) => Array.isArray(index) ? { fields: index } : index)
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function ensureSQLiteJsonTables(sql, collections = {}) {
|
|
520
|
+
for (const [collectionName, collection] of Object.entries(collections)) {
|
|
521
|
+
const storage = sqliteJsonStorage(collection.storage);
|
|
522
|
+
if (!storage) continue;
|
|
523
|
+
const table = tableName(collectionName, storage);
|
|
524
|
+
const result = execSql(sql, sqlTag`
|
|
525
|
+
CREATE TABLE IF NOT EXISTS ${identifierSql(table)} (
|
|
526
|
+
key TEXT NOT NULL,
|
|
527
|
+
path TEXT NOT NULL,
|
|
528
|
+
value TEXT NOT NULL,
|
|
529
|
+
version INTEGER NOT NULL,
|
|
530
|
+
created_at TEXT NOT NULL,
|
|
531
|
+
updated_at TEXT NOT NULL,
|
|
532
|
+
PRIMARY KEY (path, key)
|
|
533
|
+
)
|
|
534
|
+
`);
|
|
535
|
+
for (const index of storage.indexes) execSql(sql, createIndexSql(table, index));
|
|
536
|
+
if (result.rowsWritten > 0) {
|
|
537
|
+
if (collection.initialData) for (const data of collection.initialData) {
|
|
538
|
+
const key = String(data.id);
|
|
539
|
+
const path = collectionScope(collectionName);
|
|
540
|
+
const value = JSON.stringify(data);
|
|
541
|
+
const version = 1;
|
|
542
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
543
|
+
const updatedAt = createdAt;
|
|
544
|
+
execSql(sql, sqlTag`
|
|
545
|
+
INSERT INTO ${identifierSql(table)} (key, path, value, version, created_at, updated_at)
|
|
546
|
+
VALUES (${key}, ${path}, ${value}, ${version}, ${createdAt}, ${updatedAt})
|
|
547
|
+
`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function applySQLiteJsonBatch(sql, batch, collections = {}) {
|
|
553
|
+
ensureSQLiteJsonTables(sql, collections);
|
|
554
|
+
for (const update of batch.updates) {
|
|
555
|
+
const resolved = resolveCollection(update.collection, collections);
|
|
556
|
+
const storage = resolved ? sqliteJsonStorage(resolved.collection.storage) : void 0;
|
|
557
|
+
if (!resolved || !storage) continue;
|
|
558
|
+
const table = tableName(resolved.name, storage);
|
|
559
|
+
const key = String(update.key);
|
|
560
|
+
const scope = resolved.scope;
|
|
561
|
+
const updatedAt = new Date(update.createdAt).toISOString();
|
|
562
|
+
if (update.type === "delete") {
|
|
563
|
+
execSql(sql, sqlTag`DELETE FROM ${identifierSql(table)} WHERE ${whereKeySql(scope, key)}`);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (update.type === "insert") {
|
|
567
|
+
execSql(sql, sqlTag`
|
|
568
|
+
INSERT INTO ${identifierSql(table)} (key, path, value, version, created_at, updated_at)
|
|
569
|
+
VALUES (${key}, ${scope}, ${stringifyRow(update.value)}, 1, ${updatedAt}, ${updatedAt})
|
|
570
|
+
ON CONFLICT(path, key) DO UPDATE SET
|
|
571
|
+
path = excluded.path,
|
|
572
|
+
value = excluded.value,
|
|
573
|
+
version = ${identifierSql(table)}.version + 1,
|
|
574
|
+
updated_at = excluded.updated_at
|
|
575
|
+
`);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const existing = execSql(sql, sqlTag`SELECT value FROM ${identifierSql(table)} WHERE ${whereKeySql(scope, key)}`).toArray()[0];
|
|
579
|
+
if (!existing) throw new Error(`Cannot update missing ${update.collection} row: ${key}`);
|
|
580
|
+
const merged = mergeRows(JSON.parse(existing.value), update.value);
|
|
581
|
+
execSql(sql, sqlTag`
|
|
582
|
+
UPDATE ${identifierSql(table)}
|
|
583
|
+
SET value = ${JSON.stringify(merged)}, version = version + 1, updated_at = ${updatedAt}
|
|
584
|
+
WHERE ${whereKeySql(scope, key)}
|
|
585
|
+
`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function sqliteJsonStorage(storage) {
|
|
589
|
+
if (!storage) return sqliteJsonTable();
|
|
590
|
+
return storage.kind === "sqlite-json" ? storage : void 0;
|
|
591
|
+
}
|
|
592
|
+
function whereKeySql(scope, key) {
|
|
593
|
+
return sqlTag`path = ${scope} AND key = ${key}`;
|
|
594
|
+
}
|
|
595
|
+
function tableName(collectionName, storage) {
|
|
596
|
+
return storage.tableName ?? collectionName;
|
|
597
|
+
}
|
|
598
|
+
function createIndexSql(table, index) {
|
|
599
|
+
const name = index.name ?? `${table}_${index.fields.join("_")}_idx`;
|
|
600
|
+
const fields = index.fields.map((field) => rawSql(`json_extract(value, '$.${jsonPath(field)}')`));
|
|
601
|
+
return sqlTag`
|
|
602
|
+
CREATE INDEX IF NOT EXISTS ${identifierSql(name)}
|
|
603
|
+
ON ${identifierSql(table)} (${join(fields)})
|
|
604
|
+
`;
|
|
605
|
+
}
|
|
606
|
+
function jsonPath(field) {
|
|
607
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(field)) throw new Error(`Invalid JSON index field: ${field}`);
|
|
608
|
+
return field;
|
|
609
|
+
}
|
|
610
|
+
function stringifyRow(value) {
|
|
611
|
+
if (!isRecord(value)) throw new Error("SQLite JSON storage requires object row values");
|
|
612
|
+
return JSON.stringify(value);
|
|
613
|
+
}
|
|
614
|
+
function mergeRows(existing, changes) {
|
|
615
|
+
if (!isRecord(existing) || !isRecord(changes)) throw new Error("SQLite JSON storage can only merge object update values");
|
|
616
|
+
return {
|
|
617
|
+
...existing,
|
|
618
|
+
...changes
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function isRecord(value) {
|
|
622
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
623
|
+
}
|
|
624
|
+
//#endregion
|
|
625
|
+
//#region src/collection-builder.ts
|
|
626
|
+
function collectionConfigsBuilder() {
|
|
627
|
+
return new CollectionConfigsBuilder({});
|
|
628
|
+
}
|
|
629
|
+
function collectionBuilder() {
|
|
630
|
+
return new CollectionBuilder({});
|
|
631
|
+
}
|
|
632
|
+
const Collections = {
|
|
633
|
+
builder: collectionConfigsBuilder,
|
|
634
|
+
collection: collectionBuilder
|
|
635
|
+
};
|
|
636
|
+
var CollectionConfigsBuilder = class CollectionConfigsBuilder {
|
|
637
|
+
collections;
|
|
638
|
+
constructor(collections) {
|
|
639
|
+
this.collections = Object.freeze({ ...collections });
|
|
640
|
+
}
|
|
641
|
+
collection(name, configure) {
|
|
642
|
+
const builder = configure(new CollectionBuilder({ path: rootPathForName(name) }));
|
|
643
|
+
return new CollectionConfigsBuilder({
|
|
644
|
+
...this.collections,
|
|
645
|
+
...builder.entries()
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
path(path, configure) {
|
|
649
|
+
const builder = configure(new CollectionBuilder({ path }));
|
|
650
|
+
return new CollectionConfigsBuilder({
|
|
651
|
+
...this.collections,
|
|
652
|
+
...builder.entries()
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
build() {
|
|
656
|
+
return { ...this.collections };
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
var CollectionBuilder = class CollectionBuilder {
|
|
660
|
+
state;
|
|
661
|
+
constructor(state) {
|
|
662
|
+
this.state = freezeState(state);
|
|
663
|
+
}
|
|
664
|
+
name(name) {
|
|
665
|
+
return this.next({ path: rootPathForName(name) });
|
|
666
|
+
}
|
|
667
|
+
path(path) {
|
|
668
|
+
return this.next({ path });
|
|
669
|
+
}
|
|
670
|
+
schema(schema) {
|
|
671
|
+
return this.next({ schema });
|
|
672
|
+
}
|
|
673
|
+
access(access) {
|
|
674
|
+
return this.next({ access });
|
|
675
|
+
}
|
|
676
|
+
storage(storage) {
|
|
677
|
+
return this.next({ storage });
|
|
678
|
+
}
|
|
679
|
+
index(fieldOrIndex, ...fields) {
|
|
680
|
+
return this.next({ indexes: [...this.state.indexes ?? [], normalizeIndex(fieldOrIndex, fields)] });
|
|
681
|
+
}
|
|
682
|
+
initialData(...initialData) {
|
|
683
|
+
return this.next({ initialData });
|
|
684
|
+
}
|
|
685
|
+
api(name, input, handler) {
|
|
686
|
+
return this.next({ api: {
|
|
687
|
+
...this.state.api,
|
|
688
|
+
[name]: freezeApiDefinition({
|
|
689
|
+
input,
|
|
690
|
+
handler
|
|
691
|
+
})
|
|
692
|
+
} });
|
|
693
|
+
}
|
|
694
|
+
child(name, configure) {
|
|
695
|
+
const path = this.requirePath("CollectionBuilder.child");
|
|
696
|
+
const builder = configure(new CollectionBuilder({ path: childPathForName(path, parentIdParamForName(path), name) }));
|
|
697
|
+
return this.next({ children: {
|
|
698
|
+
...this.state.children,
|
|
699
|
+
[name]: builder
|
|
700
|
+
} });
|
|
701
|
+
}
|
|
702
|
+
buildConfig() {
|
|
703
|
+
if (!this.state.schema) throw new Error("CollectionBuilder requires a schema");
|
|
704
|
+
const config = { schema: this.state.schema };
|
|
705
|
+
if (this.state.access) config.access = this.state.access;
|
|
706
|
+
config.storage = storageWithIndexes(this.state.storage, this.state.indexes ?? []);
|
|
707
|
+
if (this.state.initialData) config.initialData = [...this.state.initialData];
|
|
708
|
+
if (this.state.api) config.api = { ...this.state.api };
|
|
709
|
+
return config;
|
|
710
|
+
}
|
|
711
|
+
build() {
|
|
712
|
+
return this.entries();
|
|
713
|
+
}
|
|
714
|
+
entries() {
|
|
715
|
+
const entries = { [this.requirePath("CollectionBuilder.build")]: this.buildConfig() };
|
|
716
|
+
for (const child of Object.values(this.state.children ?? {})) Object.assign(entries, child.entries());
|
|
717
|
+
return entries;
|
|
718
|
+
}
|
|
719
|
+
next(updates) {
|
|
720
|
+
return new CollectionBuilder({
|
|
721
|
+
...this.state,
|
|
722
|
+
...updates
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
requirePath(caller) {
|
|
726
|
+
if (!this.state.path) throw new Error(`${caller} requires a name or path`);
|
|
727
|
+
return this.state.path;
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
function freezeState(state) {
|
|
731
|
+
const next = {};
|
|
732
|
+
if (state.path) next.path = state.path;
|
|
733
|
+
if (state.schema) next.schema = state.schema;
|
|
734
|
+
if (state.access) next.access = state.access;
|
|
735
|
+
if (state.storage) next.storage = state.storage;
|
|
736
|
+
if (state.indexes) next.indexes = state.indexes.map((index) => ({ ...index }));
|
|
737
|
+
if (state.initialData) next.initialData = [...state.initialData];
|
|
738
|
+
if (state.api) next.api = Object.freeze({ ...state.api });
|
|
739
|
+
if (state.children) next.children = Object.freeze({ ...state.children });
|
|
740
|
+
return Object.freeze(next);
|
|
741
|
+
}
|
|
742
|
+
function freezeApiDefinition(definition) {
|
|
743
|
+
return Object.freeze({ ...definition });
|
|
744
|
+
}
|
|
745
|
+
function normalizeIndex(fieldOrIndex, fields) {
|
|
746
|
+
if (typeof fieldOrIndex === "string") return { fields: [fieldOrIndex, ...fields] };
|
|
747
|
+
return Array.isArray(fieldOrIndex) ? { fields: fieldOrIndex } : fieldOrIndex;
|
|
748
|
+
}
|
|
749
|
+
function storageWithIndexes(storage, indexes) {
|
|
750
|
+
const current = storage ?? sqliteJsonTable();
|
|
751
|
+
if (current.kind !== "sqlite-json") return current;
|
|
752
|
+
return {
|
|
753
|
+
...current,
|
|
754
|
+
indexes: [...current.indexes, ...indexes]
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function rootPathForName(name) {
|
|
758
|
+
return `/${encodeURIComponent(name)}/`;
|
|
759
|
+
}
|
|
760
|
+
function childPathForName(parentPath, parentParam, name) {
|
|
761
|
+
return `${parentPath.replace(/\/+$/g, "")}/{${parentParam}}/${encodeURIComponent(name)}/`;
|
|
762
|
+
}
|
|
763
|
+
function parentIdParamForName(parentPath) {
|
|
764
|
+
return `${singularize(parentPath.trim().replace(/^\/+|\/+$/g, "").split("/").filter((part) => !part.startsWith("{") && !part.startsWith(":")).at(-1) ?? "parent")}Id`;
|
|
765
|
+
}
|
|
766
|
+
function singularize(value) {
|
|
767
|
+
return value.endsWith("s") ? value.slice(0, -1) : value;
|
|
768
|
+
}
|
|
769
|
+
//#endregion
|
|
770
|
+
//#region src/definition-builder.ts
|
|
771
|
+
function collectionShardOptionsFrom(definitions) {
|
|
772
|
+
return new CollectionShardDefinitionBuilder(definitions.collections, {});
|
|
773
|
+
}
|
|
774
|
+
var CollectionShardDefinitionBuilder = class CollectionShardDefinitionBuilder {
|
|
775
|
+
definitions;
|
|
776
|
+
overrides;
|
|
777
|
+
constructor(definitions, overrides) {
|
|
778
|
+
this.definitions = definitions;
|
|
779
|
+
this.overrides = overrides;
|
|
780
|
+
}
|
|
781
|
+
collection(path, configure) {
|
|
782
|
+
const override = configure(new ServerCollectionBuilder({})).toOverride();
|
|
783
|
+
return new CollectionShardDefinitionBuilder(this.definitions, {
|
|
784
|
+
...this.overrides,
|
|
785
|
+
[path]: override
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
build() {
|
|
789
|
+
const collections = {};
|
|
790
|
+
for (const [name, definition] of Object.entries(this.definitions)) addDefinition(collections, definition, name, this.overrides);
|
|
791
|
+
return { collections };
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
var ServerCollectionBuilder = class ServerCollectionBuilder {
|
|
795
|
+
override;
|
|
796
|
+
constructor(override) {
|
|
797
|
+
this.override = override;
|
|
798
|
+
}
|
|
799
|
+
index(...fields) {
|
|
800
|
+
return new ServerCollectionBuilder({
|
|
801
|
+
...this.override,
|
|
802
|
+
indexes: [...this.override.indexes ?? [], fields]
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
initialData(data) {
|
|
806
|
+
return new ServerCollectionBuilder({
|
|
807
|
+
...this.override,
|
|
808
|
+
initialData: data
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
reference(name, resolver) {
|
|
812
|
+
return new ServerCollectionBuilder({
|
|
813
|
+
...this.override,
|
|
814
|
+
references: {
|
|
815
|
+
...this.override.references,
|
|
816
|
+
[name]: resolver
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
access(kind, handler) {
|
|
821
|
+
return new ServerCollectionBuilder({
|
|
822
|
+
...this.override,
|
|
823
|
+
access: {
|
|
824
|
+
...this.override.access,
|
|
825
|
+
[kind]: handler
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
api(name, handler) {
|
|
830
|
+
return new ServerCollectionBuilder({
|
|
831
|
+
...this.override,
|
|
832
|
+
api: {
|
|
833
|
+
...this.override.api,
|
|
834
|
+
[name]: handler
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
toOverride() {
|
|
839
|
+
return this.override;
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
function addDefinition(collections, definition, dotPath, overrides) {
|
|
843
|
+
const override = overrides[dotPath] ?? {};
|
|
844
|
+
let builder = collectionBuilder().path(definition.path).schema(definition.schema);
|
|
845
|
+
for (const fields of override.indexes ?? []) builder = builder.index(fields);
|
|
846
|
+
if (override.initialData) builder = builder.initialData(...override.initialData);
|
|
847
|
+
const access = accessConfig(override);
|
|
848
|
+
if (access) builder = builder.access(access);
|
|
849
|
+
for (const [name, handler] of Object.entries(override.api ?? {})) {
|
|
850
|
+
const apiDefinition = definition.api[name];
|
|
851
|
+
const apiAccess = override.access?.[`api.${name}`];
|
|
852
|
+
const apiPath = apiDefinition?.path ?? defaultCollectionApiPath(definition.path, name);
|
|
853
|
+
const input = apiDefinition?.input ?? z.undefined();
|
|
854
|
+
builder = builder.api(name, input, async (args) => {
|
|
855
|
+
authorizeApiCall(apiAccess, apiPath, args.input, args.auth);
|
|
856
|
+
return handler(args);
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
Object.assign(collections, builder.build());
|
|
860
|
+
for (const [name, child] of Object.entries(definition.children)) addDefinition(collections, child, `${dotPath}.${name}`, overrides);
|
|
861
|
+
}
|
|
862
|
+
function accessConfig(override) {
|
|
863
|
+
const access = override.access ?? {};
|
|
864
|
+
const apiAccess = Object.fromEntries(Object.entries(access).filter(([kind]) => kind.startsWith("api.")).map(([kind, handler]) => [kind.slice(4), handler]));
|
|
865
|
+
const config = {};
|
|
866
|
+
if (override.references) config.references = override.references;
|
|
867
|
+
if (access.read) config.read = access.read;
|
|
868
|
+
if (access.write) config.write = access.write;
|
|
869
|
+
if (access.insert) config.insert = access.insert;
|
|
870
|
+
if (access.update) config.update = access.update;
|
|
871
|
+
if (access.delete) config.delete = access.delete;
|
|
872
|
+
if (Object.keys(apiAccess).length > 0) config.api = apiAccess;
|
|
873
|
+
return Object.keys(config).length > 0 ? config : void 0;
|
|
874
|
+
}
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/durable-object-rpc.ts
|
|
877
|
+
function callRouter(caller, request) {
|
|
878
|
+
if (request.method === "query" && request.params.path === "read") return caller.read(request.params.input.json);
|
|
879
|
+
if (request.method === "query" && request.params.path === "changes") return caller.changes(request.params.input.json);
|
|
880
|
+
if (request.method === "subscribe") return caller.subscribe(request.params.input.json);
|
|
881
|
+
if (request.method === "unsubscribe") return caller.unsubscribe(request.params.input.json);
|
|
882
|
+
if (request.method !== "mutation") throw new Error("Unsupported operation");
|
|
883
|
+
if (request.params.path === "push") return caller.push(request.params.input.json);
|
|
884
|
+
if (request.params.path === "api") return caller.api(request.params.input.json);
|
|
885
|
+
throw new Error("Unsupported operation");
|
|
886
|
+
}
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region src/history-watermarks.ts
|
|
889
|
+
const COLLECTION_WATERMARKS_TABLE = "_lsync_collection_watermarks";
|
|
890
|
+
function ensureCollectionWatermarkTable(sql) {
|
|
891
|
+
sql.exec(`
|
|
892
|
+
CREATE TABLE IF NOT EXISTS ${quoteIdentifier$1(COLLECTION_WATERMARKS_TABLE)} (
|
|
893
|
+
scope TEXT PRIMARY KEY,
|
|
894
|
+
collection TEXT NOT NULL,
|
|
895
|
+
latest_sequence INTEGER NOT NULL
|
|
896
|
+
)
|
|
897
|
+
`);
|
|
898
|
+
}
|
|
899
|
+
function recordCollectionWatermark(sql, collection, scope, sequence) {
|
|
900
|
+
sql.exec(`
|
|
901
|
+
INSERT INTO ${quoteIdentifier$1(COLLECTION_WATERMARKS_TABLE)}
|
|
902
|
+
(scope, collection, latest_sequence)
|
|
903
|
+
VALUES (?, ?, ?)
|
|
904
|
+
ON CONFLICT(scope) DO UPDATE SET
|
|
905
|
+
collection = excluded.collection,
|
|
906
|
+
latest_sequence = excluded.latest_sequence
|
|
907
|
+
`, scope, collection, sequence);
|
|
908
|
+
}
|
|
909
|
+
function readCollectionWatermark(sql, scope) {
|
|
910
|
+
return sql.exec(`
|
|
911
|
+
SELECT latest_sequence FROM ${quoteIdentifier$1(COLLECTION_WATERMARKS_TABLE)}
|
|
912
|
+
WHERE scope = ?
|
|
913
|
+
`, scope).toArray()[0]?.latest_sequence ?? 0;
|
|
914
|
+
}
|
|
915
|
+
function quoteIdentifier$1(identifier) {
|
|
916
|
+
return `"${identifier.replaceAll(`"`, `""`)}"`;
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/history.ts
|
|
920
|
+
const CHANGES_TABLE = "_lsync_changes";
|
|
921
|
+
function persistSQLiteJsonBatchWithHistory(sql, batch, collections = {}, options = {}) {
|
|
922
|
+
ensureSQLiteJsonTables(sql, collections);
|
|
923
|
+
ensureChangesTable(sql);
|
|
924
|
+
applySQLiteJsonBatch(sql, batch, collections);
|
|
925
|
+
const updates = batch.updates.map((update) => appendChange(sql, update, collections));
|
|
926
|
+
const watermark = updates.at(-1)?.sequence ?? readWatermark(sql);
|
|
927
|
+
pruneChanges(sql, watermark, options.maxChanges ?? 1e4);
|
|
928
|
+
return {
|
|
929
|
+
updates,
|
|
930
|
+
watermark
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function readSQLiteJsonChanges(sql, query, collections = {}) {
|
|
934
|
+
ensureChangesTable(sql);
|
|
935
|
+
const requested = Object.entries(query.collections).map(([collection, sequence]) => ({
|
|
936
|
+
collection,
|
|
937
|
+
sequence,
|
|
938
|
+
scope: scopeForCollection(collection, collections)
|
|
939
|
+
}));
|
|
940
|
+
const watermark = readWatermark(sql);
|
|
941
|
+
const staleCollections = requested.flatMap((item) => isCursorStale(sql, item.scope, item.sequence) ? [item.collection] : []);
|
|
942
|
+
if (staleCollections.length > 0) return {
|
|
943
|
+
type: "resyncRequired",
|
|
944
|
+
collections: staleCollections,
|
|
945
|
+
watermark
|
|
946
|
+
};
|
|
947
|
+
if (requested.length === 0) return {
|
|
948
|
+
type: "changes",
|
|
949
|
+
updates: [],
|
|
950
|
+
watermark,
|
|
951
|
+
hasMore: false
|
|
952
|
+
};
|
|
953
|
+
const limit = query.limit ?? 1e3;
|
|
954
|
+
const rows = selectChanges(sql, requested, limit + 1);
|
|
955
|
+
return {
|
|
956
|
+
type: "changes",
|
|
957
|
+
updates: rows.slice(0, limit).map(rowToUpdate),
|
|
958
|
+
watermark,
|
|
959
|
+
hasMore: rows.length > limit
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function ensureChangesTable(sql) {
|
|
963
|
+
sql.exec(`
|
|
964
|
+
CREATE TABLE IF NOT EXISTS ${quoteIdentifier(CHANGES_TABLE)} (
|
|
965
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
966
|
+
update_id TEXT NOT NULL,
|
|
967
|
+
collection TEXT NOT NULL,
|
|
968
|
+
scope TEXT NOT NULL,
|
|
969
|
+
"key" TEXT NOT NULL,
|
|
970
|
+
type TEXT NOT NULL,
|
|
971
|
+
value TEXT,
|
|
972
|
+
previous_value TEXT,
|
|
973
|
+
client_id TEXT,
|
|
974
|
+
client_created_at INTEGER NOT NULL,
|
|
975
|
+
server_created_at TEXT NOT NULL
|
|
976
|
+
)
|
|
977
|
+
`);
|
|
978
|
+
sql.exec(`
|
|
979
|
+
CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${CHANGES_TABLE}_scope_sequence_idx`)}
|
|
980
|
+
ON ${quoteIdentifier(CHANGES_TABLE)} (scope, sequence)
|
|
981
|
+
`);
|
|
982
|
+
ensureCollectionWatermarkTable(sql);
|
|
983
|
+
}
|
|
984
|
+
function appendChange(sql, update, collections) {
|
|
985
|
+
const serverCreatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
986
|
+
const scope = scopeForCollection(update.collection, collections);
|
|
987
|
+
const row = sql.exec(`
|
|
988
|
+
INSERT INTO ${quoteIdentifier(CHANGES_TABLE)}
|
|
989
|
+
(update_id, collection, scope, "key", type, value, previous_value, client_id,
|
|
990
|
+
client_created_at, server_created_at)
|
|
991
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
992
|
+
RETURNING sequence
|
|
993
|
+
`, update.id, update.collection, scope, JSON.stringify(update.key), update.type, update.value === void 0 ? null : JSON.stringify(update.value), update.previousValue === void 0 ? null : JSON.stringify(update.previousValue), update.clientId ?? null, update.createdAt, serverCreatedAt).toArray()[0];
|
|
994
|
+
if (!row) throw new Error("Unable to append sync history row");
|
|
995
|
+
recordCollectionWatermark(sql, update.collection, scope, row.sequence);
|
|
996
|
+
return {
|
|
997
|
+
...update,
|
|
998
|
+
sequence: row.sequence,
|
|
999
|
+
serverCreatedAt
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function selectChanges(sql, requested, limit) {
|
|
1003
|
+
const bindings = requested.flatMap((item) => [item.scope, item.sequence]);
|
|
1004
|
+
bindings.push(limit);
|
|
1005
|
+
return sql.exec(`
|
|
1006
|
+
SELECT sequence, update_id, collection, "key", type, value, previous_value, client_id,
|
|
1007
|
+
client_created_at, server_created_at
|
|
1008
|
+
FROM ${quoteIdentifier(CHANGES_TABLE)}
|
|
1009
|
+
WHERE ${requested.map(() => "(scope = ? AND sequence > ?)").join(" OR ")}
|
|
1010
|
+
ORDER BY sequence
|
|
1011
|
+
LIMIT ?
|
|
1012
|
+
`, ...bindings).toArray();
|
|
1013
|
+
}
|
|
1014
|
+
function rowToUpdate(row) {
|
|
1015
|
+
const update = {
|
|
1016
|
+
id: row.update_id,
|
|
1017
|
+
collection: row.collection,
|
|
1018
|
+
key: JSON.parse(row.key),
|
|
1019
|
+
type: row.type,
|
|
1020
|
+
createdAt: row.client_created_at,
|
|
1021
|
+
sequence: row.sequence,
|
|
1022
|
+
serverCreatedAt: row.server_created_at
|
|
1023
|
+
};
|
|
1024
|
+
if (row.value !== null) update.value = JSON.parse(row.value);
|
|
1025
|
+
if (row.previous_value !== null) update.previousValue = JSON.parse(row.previous_value);
|
|
1026
|
+
if (row.client_id !== null) update.clientId = row.client_id;
|
|
1027
|
+
return update;
|
|
1028
|
+
}
|
|
1029
|
+
function isCursorStale(sql, scope, sequence) {
|
|
1030
|
+
const latest = readCollectionWatermark(sql, scope);
|
|
1031
|
+
if (latest === 0) return false;
|
|
1032
|
+
const oldest = sql.exec(`SELECT MIN(sequence) as sequence FROM ${quoteIdentifier(CHANGES_TABLE)} WHERE scope = ?`, scope).toArray()[0]?.sequence;
|
|
1033
|
+
if (oldest === null || oldest === void 0) return sequence < latest;
|
|
1034
|
+
return sequence < oldest - 1;
|
|
1035
|
+
}
|
|
1036
|
+
function readWatermark(sql) {
|
|
1037
|
+
return sql.exec(`SELECT COALESCE(MAX(sequence), 0) as watermark FROM ${quoteIdentifier(CHANGES_TABLE)}`).toArray()[0]?.watermark ?? 0;
|
|
1038
|
+
}
|
|
1039
|
+
function pruneChanges(sql, watermark, maxChanges) {
|
|
1040
|
+
if (!Number.isFinite(maxChanges) || maxChanges < 0) return;
|
|
1041
|
+
sql.exec(`DELETE FROM ${quoteIdentifier(CHANGES_TABLE)} WHERE sequence <= ?`, watermark - maxChanges);
|
|
1042
|
+
}
|
|
1043
|
+
function scopeForCollection(collection, collections) {
|
|
1044
|
+
const resolved = resolveCollection(collection, collections);
|
|
1045
|
+
if (resolved) return resolved.scope;
|
|
1046
|
+
if (Object.keys(collections).length > 0) throw new Error(`Unknown collection: ${collection}`);
|
|
1047
|
+
return collectionScope(collection);
|
|
1048
|
+
}
|
|
1049
|
+
function quoteIdentifier(identifier) {
|
|
1050
|
+
return `"${identifier.replaceAll(`"`, `""`)}"`;
|
|
1051
|
+
}
|
|
1052
|
+
//#endregion
|
|
1053
|
+
//#region src/messages.ts
|
|
1054
|
+
function sendRpcResult(ws, id, result) {
|
|
1055
|
+
sendServerMessage(ws, {
|
|
1056
|
+
id,
|
|
1057
|
+
result: {
|
|
1058
|
+
type: "data",
|
|
1059
|
+
data: { json: result }
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
function sendRpcError(ws, id, error) {
|
|
1064
|
+
sendServerMessage(ws, {
|
|
1065
|
+
id,
|
|
1066
|
+
error: { message: error instanceof Error ? error.message : "Unknown error" }
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
//#endregion
|
|
1070
|
+
//#region src/socket-attachment.ts
|
|
1071
|
+
function webSocketAttachment(ws) {
|
|
1072
|
+
const result = webSocketAttachmentSchema.safeParse(ws.deserializeAttachment());
|
|
1073
|
+
return result.success ? result.data : void 0;
|
|
1074
|
+
}
|
|
1075
|
+
function webSocketAuth(ws) {
|
|
1076
|
+
const attachment = webSocketAttachment(ws);
|
|
1077
|
+
if (!attachment) return {};
|
|
1078
|
+
return {
|
|
1079
|
+
clientId: attachment.clientId,
|
|
1080
|
+
...attachment.auth
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function updateSocketSubscriptions(ws, collection, normalize, update) {
|
|
1084
|
+
const attachment = webSocketAttachment(ws);
|
|
1085
|
+
if (!attachment) throw new Error("Missing WebSocket attachment");
|
|
1086
|
+
const normalized = normalize(collection);
|
|
1087
|
+
const subscriptions = new Set(attachment.subscriptions);
|
|
1088
|
+
update(subscriptions, normalized);
|
|
1089
|
+
const next = [...subscriptions];
|
|
1090
|
+
ws.serializeAttachment({
|
|
1091
|
+
...attachment,
|
|
1092
|
+
subscriptions: next
|
|
1093
|
+
});
|
|
1094
|
+
return {
|
|
1095
|
+
collection: normalized,
|
|
1096
|
+
subscriptions: next
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
//#endregion
|
|
1100
|
+
//#region src/access-invalidation.ts
|
|
1101
|
+
function invalidatesDependency(update, dependencies) {
|
|
1102
|
+
return dependencies.some((dependency) => {
|
|
1103
|
+
if (collectionScope(update.collection) !== collectionScope(dependency.collection)) return false;
|
|
1104
|
+
if (String(update.key) !== String(dependency.key)) return false;
|
|
1105
|
+
return fieldChanged(update.previousValue, update.value, dependency.field);
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
function fieldChanged(previous, next, field) {
|
|
1109
|
+
if (!previous || !next) return true;
|
|
1110
|
+
return valueAtPath(previous, field) !== valueAtPath(next, field);
|
|
1111
|
+
}
|
|
1112
|
+
function valueAtPath(value, field) {
|
|
1113
|
+
return field.split(".").reduce((current, segment) => {
|
|
1114
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
1115
|
+
return current[segment];
|
|
1116
|
+
}, value);
|
|
1117
|
+
}
|
|
1118
|
+
//#endregion
|
|
1119
|
+
//#region src/subscription-invalidation.ts
|
|
1120
|
+
function subscribedInvalidations(subscriptions, batch, collections, auth, store) {
|
|
1121
|
+
const invalidations = /* @__PURE__ */ new Set();
|
|
1122
|
+
for (const collection of subscriptions) {
|
|
1123
|
+
const decision = readAccessDecision(collection, collections, auth, store);
|
|
1124
|
+
if (decision.dependencies.length === 0) continue;
|
|
1125
|
+
if (batch.updates.some((update) => invalidatesDependency(update, decision.dependencies))) invalidations.add(collection);
|
|
1126
|
+
}
|
|
1127
|
+
return [...invalidations].map((collection) => ({ collection }));
|
|
1128
|
+
}
|
|
1129
|
+
//#endregion
|
|
1130
|
+
//#region src/validation.ts
|
|
1131
|
+
function validateBatch(batch, collections) {
|
|
1132
|
+
for (const update of batch.updates) {
|
|
1133
|
+
const schema = schemaFor(update.collection, collections);
|
|
1134
|
+
if (!schema || update.type === "delete") continue;
|
|
1135
|
+
if (update.type === "update" && schema instanceof z.ZodObject) {
|
|
1136
|
+
schema.partial().parse(update.value);
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
schema.parse(update.value);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
function schemaFor(collection, collections) {
|
|
1143
|
+
const configured = resolveCollection(collection, collections);
|
|
1144
|
+
if (!configured) {
|
|
1145
|
+
if (collections && Object.keys(collections).length > 0) throw new Error(`Unknown collection: ${collection}`);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
return configured.collection.schema;
|
|
1149
|
+
}
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region src/durable-object.ts
|
|
1152
|
+
var CollectionShardDurableObject = class {
|
|
1153
|
+
state;
|
|
1154
|
+
env;
|
|
1155
|
+
options;
|
|
1156
|
+
static from = collectionShardOptionsFrom;
|
|
1157
|
+
apiHandlers;
|
|
1158
|
+
constructor(state, env, options = {}) {
|
|
1159
|
+
this.state = state;
|
|
1160
|
+
this.env = env;
|
|
1161
|
+
this.options = options;
|
|
1162
|
+
this.apiHandlers = collectionApiHandlers(options.collections);
|
|
1163
|
+
}
|
|
1164
|
+
async fetch(request) {
|
|
1165
|
+
if (request.headers.get("Upgrade") !== "websocket") return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
1166
|
+
const pair = new WebSocketPair();
|
|
1167
|
+
const client = pair[0];
|
|
1168
|
+
const server = pair[1];
|
|
1169
|
+
const clientId = new URL(request.url).searchParams.get("clientId") ?? crypto.randomUUID();
|
|
1170
|
+
const auth = await this.options.authenticate?.({
|
|
1171
|
+
clientId,
|
|
1172
|
+
request
|
|
1173
|
+
}) ?? { clientId };
|
|
1174
|
+
this.state.acceptWebSocket(server);
|
|
1175
|
+
server.serializeAttachment({
|
|
1176
|
+
clientId,
|
|
1177
|
+
connectedAt: Date.now(),
|
|
1178
|
+
auth,
|
|
1179
|
+
subscriptions: []
|
|
1180
|
+
});
|
|
1181
|
+
return new Response(null, {
|
|
1182
|
+
status: 101,
|
|
1183
|
+
webSocket: client
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
async webSocketMessage(ws, message) {
|
|
1187
|
+
try {
|
|
1188
|
+
const request = parseClientRpcRequest(message);
|
|
1189
|
+
const result = await callRouter(router.createCaller({
|
|
1190
|
+
shardId: this.shardId(),
|
|
1191
|
+
validate: (input) => this.validate(input),
|
|
1192
|
+
persist: (input) => this.persist(input),
|
|
1193
|
+
publish: (input) => this.publish(input),
|
|
1194
|
+
mutate: (input) => this.mutate(input, webSocketAuth(ws)),
|
|
1195
|
+
subscribe: (input) => this.subscribe(ws, input),
|
|
1196
|
+
unsubscribe: (input) => this.unsubscribe(ws, input),
|
|
1197
|
+
read: (input) => this.read(input, webSocketAuth(ws)),
|
|
1198
|
+
changes: (input) => this.readChanges(input, webSocketAuth(ws)),
|
|
1199
|
+
callApi: (input) => this.callApi(ws, input)
|
|
1200
|
+
}), request);
|
|
1201
|
+
sendRpcResult(ws, request.id, result);
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
sendRpcError(ws, readRpcId(message), error);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
publish(batch) {
|
|
1207
|
+
const payload = {
|
|
1208
|
+
type: "updates",
|
|
1209
|
+
shardId: this.shardId(),
|
|
1210
|
+
updates: batch.updates,
|
|
1211
|
+
watermark: batch.watermark
|
|
1212
|
+
};
|
|
1213
|
+
for (const socket of this.state.getWebSockets()) {
|
|
1214
|
+
const updates = this.subscribedUpdates(socket, batch);
|
|
1215
|
+
const invalidations = this.subscribedInvalidations(socket, batch);
|
|
1216
|
+
if (updates.length === 0 && invalidations.length === 0) continue;
|
|
1217
|
+
sendServerMessage(socket, {
|
|
1218
|
+
method: "subscription",
|
|
1219
|
+
params: {
|
|
1220
|
+
path: "updates",
|
|
1221
|
+
input: { json: {
|
|
1222
|
+
...payload,
|
|
1223
|
+
...invalidations.length > 0 ? { invalidations } : {},
|
|
1224
|
+
updates
|
|
1225
|
+
} }
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
validate(batch) {
|
|
1231
|
+
validateBatch(batch, this.options.collections);
|
|
1232
|
+
}
|
|
1233
|
+
persist(batch) {
|
|
1234
|
+
return this.state.storage.transactionSync(() => persistSQLiteJsonBatchWithHistory(this.state.storage.sql, batch, this.options.collections, this.options.history));
|
|
1235
|
+
}
|
|
1236
|
+
read(query, auth = {}) {
|
|
1237
|
+
const authorized = authorizeReadQuery(query, this.options.collections, auth, this.accessStore());
|
|
1238
|
+
return authorized ? readSQLiteJsonRows(this.state.storage.sql, authorized, this.options.collections) : { rows: [] };
|
|
1239
|
+
}
|
|
1240
|
+
readChanges(query, auth = {}) {
|
|
1241
|
+
const result = readSQLiteJsonChanges(this.state.storage.sql, query, this.options.collections);
|
|
1242
|
+
if (result.type === "resyncRequired") return result;
|
|
1243
|
+
return {
|
|
1244
|
+
...result,
|
|
1245
|
+
updates: result.updates.flatMap((update) => {
|
|
1246
|
+
const visible = visibleUpdateForAuth(update, this.options.collections, auth, this.accessStore());
|
|
1247
|
+
return visible ? [{
|
|
1248
|
+
...visible,
|
|
1249
|
+
sequence: update.sequence,
|
|
1250
|
+
serverCreatedAt: update.serverCreatedAt
|
|
1251
|
+
}] : [];
|
|
1252
|
+
})
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
subscribe(ws, input) {
|
|
1256
|
+
return this.updateSubscriptions(ws, input.collection, (subscriptions, collection) => {
|
|
1257
|
+
subscriptions.add(collection);
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
unsubscribe(ws, input) {
|
|
1261
|
+
return this.updateSubscriptions(ws, input.collection, (subscriptions, collection) => {
|
|
1262
|
+
subscriptions.delete(collection);
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
async callApi(ws, call) {
|
|
1266
|
+
const handler = this.apiHandlers[call.path];
|
|
1267
|
+
if (!handler) throw new Error(`Unknown API path: ${call.path}`);
|
|
1268
|
+
const clientId = this.clientId(ws);
|
|
1269
|
+
const auth = webSocketAuth(ws);
|
|
1270
|
+
const args = {
|
|
1271
|
+
shardId: this.shardId(),
|
|
1272
|
+
input: call.input,
|
|
1273
|
+
auth,
|
|
1274
|
+
validate: (input) => this.validate(input),
|
|
1275
|
+
persist: (input) => this.persist(input),
|
|
1276
|
+
publish: (input) => this.publish(input),
|
|
1277
|
+
subscribe: (input) => this.subscribe(ws, input),
|
|
1278
|
+
unsubscribe: (input) => this.unsubscribe(ws, input),
|
|
1279
|
+
mutate: (input) => this.mutate(input, auth),
|
|
1280
|
+
read: (input) => this.read(input, auth),
|
|
1281
|
+
changes: (input) => this.readChanges(input, auth)
|
|
1282
|
+
};
|
|
1283
|
+
if (clientId) args.clientId = clientId;
|
|
1284
|
+
return handler(args);
|
|
1285
|
+
}
|
|
1286
|
+
mutate(batch, auth = {}) {
|
|
1287
|
+
this.validate(batch);
|
|
1288
|
+
authorizeWriteBatch(batch, this.options.collections, auth, this.accessStore());
|
|
1289
|
+
const persisted = this.persist(batch);
|
|
1290
|
+
this.publish(persisted);
|
|
1291
|
+
return {
|
|
1292
|
+
accepted: batch.updates.length,
|
|
1293
|
+
watermark: persisted.watermark
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
clientId(ws) {
|
|
1297
|
+
return webSocketAttachment(ws)?.clientId;
|
|
1298
|
+
}
|
|
1299
|
+
updateSubscriptions(ws, collection, update) {
|
|
1300
|
+
return updateSocketSubscriptions(ws, collection, (input) => normalizeCollection(input, this.options.collections), update);
|
|
1301
|
+
}
|
|
1302
|
+
subscribedUpdates(ws, batch) {
|
|
1303
|
+
const attachment = webSocketAttachment(ws);
|
|
1304
|
+
if (!attachment || attachment.subscriptions.length === 0) return [];
|
|
1305
|
+
const subscriptions = new Set(attachment.subscriptions);
|
|
1306
|
+
const auth = webSocketAuth(ws);
|
|
1307
|
+
return batch.updates.flatMap((update) => {
|
|
1308
|
+
if (!subscriptions.has(normalizeCollection(update.collection, this.options.collections))) return [];
|
|
1309
|
+
const visible = visibleUpdateForAuth(update, this.options.collections, auth, this.accessStore());
|
|
1310
|
+
return visible ? [{
|
|
1311
|
+
...visible,
|
|
1312
|
+
sequence: update.sequence,
|
|
1313
|
+
serverCreatedAt: update.serverCreatedAt
|
|
1314
|
+
}] : [];
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
subscribedInvalidations(ws, batch) {
|
|
1318
|
+
const attachment = webSocketAttachment(ws);
|
|
1319
|
+
if (!attachment || attachment.subscriptions.length === 0) return [];
|
|
1320
|
+
return subscribedInvalidations(attachment.subscriptions, batch, this.options.collections, webSocketAuth(ws), this.accessStore());
|
|
1321
|
+
}
|
|
1322
|
+
accessStore() {
|
|
1323
|
+
return { read: (collection, key) => readSQLiteJsonRow(this.state.storage.sql, collection, key, this.options.collections) };
|
|
1324
|
+
}
|
|
1325
|
+
shardId() {
|
|
1326
|
+
return this.state.id.toString();
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
//#endregion
|
|
1330
|
+
//#region src/worker.ts
|
|
1331
|
+
function createWorkerHandler(options = {}) {
|
|
1332
|
+
const binding = options.binding ?? "SYNC_SHARDS";
|
|
1333
|
+
const routePattern = options.routePattern ?? /^\/sync\/([^/]+)$/;
|
|
1334
|
+
return { fetch(request, env) {
|
|
1335
|
+
const match = new URL(request.url).pathname.match(routePattern);
|
|
1336
|
+
if (!match?.[1]) return new Response("Not found", { status: 404 });
|
|
1337
|
+
const namespace = env[binding];
|
|
1338
|
+
if (!namespace) return new Response(`Missing Durable Object binding: ${String(binding)}`, { status: 500 });
|
|
1339
|
+
const id = namespace.idFromName(decodeURIComponent(match[1]));
|
|
1340
|
+
return namespace.get(id).fetch(request);
|
|
1341
|
+
} };
|
|
1342
|
+
}
|
|
1343
|
+
//#endregion
|
|
1344
|
+
export { CollectionBuilder, CollectionConfigsBuilder, CollectionShardDefinitionBuilder, CollectionShardDurableObject, Collections, ServerCollectionBuilder, and, applySQLiteJsonBatch, authorizeApiCall, authorizeReadQuery, authorizeWriteBatch, collectionApiHandlers, collectionBuilder, collectionConfigsBuilder, collectionScope, compileReadExpression, createWorkerHandler, defaultCollectionApiPath, ensureSQLiteJsonTables, eq, field, gt, gte, inArray, isCollectionPattern, lt, lte, matchesReadPredicate, ne, not, or, persistSQLiteJsonBatchWithHistory, readAccessDecision, readExpressionReferences, readExpressionRow, readSQLiteJsonChanges, readSQLiteJsonRow, readSQLiteJsonRows, resolveCollection, router, sqliteJsonTable, val, validateBatch, visibleUpdateForAuth };
|
|
1345
|
+
|
|
1346
|
+
//# sourceMappingURL=index.mjs.map
|