js-bao 0.5.1 → 0.6.0
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 +128 -30
- package/dist/browser.cjs +4759 -719
- package/dist/browser.d.cts +2290 -50
- package/dist/browser.d.ts +2290 -50
- package/dist/browser.js +4766 -722
- package/dist/client.cjs +383 -6
- package/dist/client.d.cts +810 -12
- package/dist/client.d.ts +810 -12
- package/dist/client.js +378 -4
- package/dist/cloudflare-do.cjs +1307 -219
- package/dist/cloudflare-do.d.cts +1195 -13
- package/dist/cloudflare-do.d.ts +1195 -13
- package/dist/cloudflare-do.js +1303 -217
- package/dist/cloudflare.cjs +619 -23
- package/dist/cloudflare.d.cts +1026 -13
- package/dist/cloudflare.d.ts +1026 -13
- package/dist/cloudflare.js +614 -21
- package/dist/codegen-v2.cjs +393 -19
- package/dist/codegen.cjs +2 -1
- package/dist/index.cjs +4830 -679
- package/dist/index.d.cts +2343 -52
- package/dist/index.d.ts +2343 -52
- package/dist/index.js +4746 -650
- package/dist/node.cjs +5016 -724
- package/dist/node.d.cts +2458 -58
- package/dist/node.d.ts +2458 -58
- package/dist/node.js +4906 -676
- package/package.json +2 -1
package/dist/cloudflare-do.js
CHANGED
|
@@ -229,21 +229,36 @@ var init_CursorManager = __esm({
|
|
|
229
229
|
return fields;
|
|
230
230
|
}
|
|
231
231
|
/**
|
|
232
|
-
* Build ORDER BY clause from sort specification
|
|
232
|
+
* Build ORDER BY clause from sort specification.
|
|
233
|
+
*
|
|
234
|
+
* `direction` is the pagination direction (1 = forward, -1 = backward).
|
|
235
|
+
* For backward pagination each field's effective ORDER BY direction is
|
|
236
|
+
* reversed so the cursor's flipped inequality (built by
|
|
237
|
+
* `buildPaginationConditions`) returns the page adjacent to the cursor,
|
|
238
|
+
* not the globally smallest/largest window. Without this,
|
|
239
|
+
* `field < cursor ... ORDER BY field ASC LIMIT n` returns the n smallest
|
|
240
|
+
* rows overall instead of the n rows just below the cursor.
|
|
241
|
+
*
|
|
242
|
+
* The returned `directions` array carries the *declared* per-field sort
|
|
243
|
+
* directions (unaffected by `direction`) — `buildPaginationConditions`
|
|
244
|
+
* needs the declared directions to pick each field's comparison operator.
|
|
233
245
|
*/
|
|
234
|
-
static buildOrderClause(sort, fieldFormatter = (field) => field) {
|
|
246
|
+
static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
|
|
235
247
|
const fields = this.extractSortFields(sort);
|
|
236
248
|
const clauses = [];
|
|
237
249
|
const directions = [];
|
|
250
|
+
const sqlDir = (declared) => {
|
|
251
|
+
const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
|
|
252
|
+
return effective === 1 ? "ASC" : "DESC";
|
|
253
|
+
};
|
|
238
254
|
if (sort && Object.keys(sort).length > 0) {
|
|
239
255
|
for (const [field, dir] of Object.entries(sort)) {
|
|
240
|
-
|
|
241
|
-
clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
|
|
256
|
+
clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
|
|
242
257
|
directions.push(dir);
|
|
243
258
|
}
|
|
244
259
|
}
|
|
245
260
|
if (!sort || !sort.hasOwnProperty("id")) {
|
|
246
|
-
clauses.push(`${fieldFormatter("id")}
|
|
261
|
+
clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
|
|
247
262
|
directions.push(1);
|
|
248
263
|
} else if (sort && sort.hasOwnProperty("id")) {
|
|
249
264
|
directions.push(sort["id"]);
|
|
@@ -266,18 +281,27 @@ var init_CursorManager = __esm({
|
|
|
266
281
|
/**
|
|
267
282
|
* Generate next and previous cursors from result set
|
|
268
283
|
*/
|
|
269
|
-
static generateResultCursors(results, sortFields,
|
|
284
|
+
static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
|
|
270
285
|
if (results.length === 0) {
|
|
271
286
|
return {};
|
|
272
287
|
}
|
|
273
288
|
const cursors = {};
|
|
274
289
|
if (hasMore && results.length > 0) {
|
|
275
290
|
const lastResult = results[results.length - 1];
|
|
276
|
-
cursors.nextCursor = this.generateCursor(
|
|
291
|
+
cursors.nextCursor = this.generateCursor(
|
|
292
|
+
lastResult,
|
|
293
|
+
sortFields,
|
|
294
|
+
requestedDirection
|
|
295
|
+
);
|
|
277
296
|
}
|
|
278
297
|
if (results.length > 0 && !isFirstPage) {
|
|
279
298
|
const firstResult = results[0];
|
|
280
|
-
|
|
299
|
+
const opposite = requestedDirection === 1 ? -1 : 1;
|
|
300
|
+
cursors.prevCursor = this.generateCursor(
|
|
301
|
+
firstResult,
|
|
302
|
+
sortFields,
|
|
303
|
+
opposite
|
|
304
|
+
);
|
|
281
305
|
}
|
|
282
306
|
return cursors;
|
|
283
307
|
}
|
|
@@ -329,6 +353,9 @@ var init_patterns = __esm({
|
|
|
329
353
|
|
|
330
354
|
// src/utils/yjsValues.ts
|
|
331
355
|
import * as Y from "yjs";
|
|
356
|
+
function isYjsComposite(value) {
|
|
357
|
+
return value instanceof Y.Map || value instanceof Y.Array || value instanceof Y.Text;
|
|
358
|
+
}
|
|
332
359
|
function isStringSetYMap(value) {
|
|
333
360
|
if (!(value instanceof Y.Map)) return false;
|
|
334
361
|
for (const v of value.values()) {
|
|
@@ -367,8 +394,33 @@ var init_DocumentQueryTranslator = __esm({
|
|
|
367
394
|
}
|
|
368
395
|
});
|
|
369
396
|
|
|
370
|
-
// src/models/
|
|
397
|
+
// src/models/writeLifecycle.ts
|
|
371
398
|
import * as Y3 from "yjs";
|
|
399
|
+
import { ulid as ulid2 } from "ulid";
|
|
400
|
+
function registerFunctionDefaultGenerator(name, fn) {
|
|
401
|
+
FUNCTION_DEFAULT_GENERATORS.set(name, fn);
|
|
402
|
+
}
|
|
403
|
+
var KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
|
|
404
|
+
var init_writeLifecycle = __esm({
|
|
405
|
+
"src/models/writeLifecycle.ts"() {
|
|
406
|
+
"use strict";
|
|
407
|
+
KNOWN_FIELD_TYPE_MAP = {
|
|
408
|
+
string: true,
|
|
409
|
+
number: true,
|
|
410
|
+
boolean: true,
|
|
411
|
+
date: true,
|
|
412
|
+
id: true,
|
|
413
|
+
stringset: true
|
|
414
|
+
};
|
|
415
|
+
KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
|
|
416
|
+
FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
|
|
417
|
+
registerFunctionDefaultGenerator("generate_ulid", () => ulid2());
|
|
418
|
+
UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// src/models/metaSync.ts
|
|
423
|
+
import * as Y4 from "yjs";
|
|
372
424
|
function inferFieldType(value) {
|
|
373
425
|
if (isStringSetYMap(value)) return "stringset";
|
|
374
426
|
switch (typeof value) {
|
|
@@ -389,7 +441,10 @@ function encodeDefault(value) {
|
|
|
389
441
|
if (value === void 0 || value === null) return void 0;
|
|
390
442
|
if (typeof value === "function") {
|
|
391
443
|
const name = KNOWN_FUNCTION_DEFAULTS.get(value);
|
|
392
|
-
return name ? `$${name}` :
|
|
444
|
+
return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
|
|
445
|
+
}
|
|
446
|
+
if (typeof value === "string" && value.startsWith("$")) {
|
|
447
|
+
return `$${value}`;
|
|
393
448
|
}
|
|
394
449
|
return value;
|
|
395
450
|
}
|
|
@@ -415,7 +470,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
415
470
|
if (compoundConstraints.length > 0) {
|
|
416
471
|
let constraints = meta.get("_constraints");
|
|
417
472
|
if (!constraints) {
|
|
418
|
-
constraints = new
|
|
473
|
+
constraints = new Y4.Map();
|
|
419
474
|
meta.set("_constraints", constraints);
|
|
420
475
|
}
|
|
421
476
|
for (const constraint of compoundConstraints) {
|
|
@@ -426,7 +481,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
426
481
|
if (relationships && Object.keys(relationships).length > 0) {
|
|
427
482
|
let rels = meta.get("_relationships");
|
|
428
483
|
if (!rels) {
|
|
429
|
-
rels = new
|
|
484
|
+
rels = new Y4.Map();
|
|
430
485
|
meta.set("_relationships", rels);
|
|
431
486
|
}
|
|
432
487
|
for (const [relName, relConfig] of Object.entries(relationships)) {
|
|
@@ -438,7 +493,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
438
493
|
function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
439
494
|
let fieldMeta = metaMap.get(fieldName);
|
|
440
495
|
if (!fieldMeta) {
|
|
441
|
-
fieldMeta = new
|
|
496
|
+
fieldMeta = new Y4.Map();
|
|
442
497
|
metaMap.set(fieldName, fieldMeta);
|
|
443
498
|
}
|
|
444
499
|
setIfChanged(fieldMeta, "type", fieldOpts.type);
|
|
@@ -455,7 +510,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
455
510
|
function syncConstraintMeta(constraintsMap, constraint) {
|
|
456
511
|
let cMeta = constraintsMap.get(constraint.name);
|
|
457
512
|
if (!cMeta) {
|
|
458
|
-
cMeta = new
|
|
513
|
+
cMeta = new Y4.Map();
|
|
459
514
|
constraintsMap.set(constraint.name, cMeta);
|
|
460
515
|
}
|
|
461
516
|
setIfChanged(cMeta, "type", "unique");
|
|
@@ -465,7 +520,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
|
|
|
465
520
|
function syncRelationshipMeta(relsMap, relName, relConfig) {
|
|
466
521
|
let relMeta = relsMap.get(relName);
|
|
467
522
|
if (!relMeta) {
|
|
468
|
-
relMeta = new
|
|
523
|
+
relMeta = new Y4.Map();
|
|
469
524
|
relsMap.set(relName, relMeta);
|
|
470
525
|
}
|
|
471
526
|
for (const [key, value] of Object.entries(relConfig)) {
|
|
@@ -480,7 +535,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
|
|
|
480
535
|
if (fieldName.startsWith("_")) continue;
|
|
481
536
|
let fieldMeta = meta.get(fieldName);
|
|
482
537
|
if (!fieldMeta) {
|
|
483
|
-
fieldMeta = new
|
|
538
|
+
fieldMeta = new Y4.Map();
|
|
484
539
|
meta.set(fieldName, fieldMeta);
|
|
485
540
|
}
|
|
486
541
|
if (!fieldMeta.has("type")) {
|
|
@@ -501,16 +556,537 @@ var init_metaSync = __esm({
|
|
|
501
556
|
"src/models/metaSync.ts"() {
|
|
502
557
|
"use strict";
|
|
503
558
|
init_yjsValues();
|
|
559
|
+
init_writeLifecycle();
|
|
504
560
|
KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
|
|
505
561
|
_syncedCache = /* @__PURE__ */ new WeakMap();
|
|
506
562
|
}
|
|
507
563
|
});
|
|
508
564
|
|
|
565
|
+
// src/utils/uniqueIndex.ts
|
|
566
|
+
import * as Y5 from "yjs";
|
|
567
|
+
function fieldUniqueConstraintName(modelName, fieldName) {
|
|
568
|
+
return `${modelName}_${fieldName}_unique`;
|
|
569
|
+
}
|
|
570
|
+
var init_uniqueIndex = __esm({
|
|
571
|
+
"src/utils/uniqueIndex.ts"() {
|
|
572
|
+
"use strict";
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// src/utils/largeDocuments.ts
|
|
577
|
+
import * as Y6 from "yjs";
|
|
578
|
+
function encodeOverlaySegment(segment) {
|
|
579
|
+
const escaped = segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
580
|
+
return escaped.startsWith("_") ? `~2${escaped.slice(1)}` : escaped;
|
|
581
|
+
}
|
|
582
|
+
function decodeOverlaySegment(segment) {
|
|
583
|
+
const withUnderscore = segment.startsWith("~2") ? `_${segment.slice(2)}` : segment;
|
|
584
|
+
return withUnderscore.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
585
|
+
}
|
|
586
|
+
function overlayFieldKey(recordId, field) {
|
|
587
|
+
return `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}${encodeOverlaySegment(field)}`;
|
|
588
|
+
}
|
|
589
|
+
function overlayMemberKey(recordId, field, member) {
|
|
590
|
+
return `${overlayFieldKey(recordId, field)}${OVERLAY_KEY_SEPARATOR}${encodeOverlaySegment(member)}`;
|
|
591
|
+
}
|
|
592
|
+
function overlayMarkerKey(recordId, marker) {
|
|
593
|
+
return `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}${marker}`;
|
|
594
|
+
}
|
|
595
|
+
function parseOverlayKey(key) {
|
|
596
|
+
const parts = key.split(OVERLAY_KEY_SEPARATOR);
|
|
597
|
+
if (parts.length === 2) {
|
|
598
|
+
const [rawId, rawSecond] = parts;
|
|
599
|
+
const recordId = decodeOverlaySegment(rawId);
|
|
600
|
+
if (OVERLAY_MARKERS.has(rawSecond)) return { recordId, marker: rawSecond };
|
|
601
|
+
return { recordId, field: decodeOverlaySegment(rawSecond) };
|
|
602
|
+
}
|
|
603
|
+
if (parts.length === 3) {
|
|
604
|
+
return {
|
|
605
|
+
recordId: decodeOverlaySegment(parts[0]),
|
|
606
|
+
field: decodeOverlaySegment(parts[1]),
|
|
607
|
+
member: decodeOverlaySegment(parts[2])
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
function emptyEntry(id) {
|
|
613
|
+
return { id, fields: {}, stringSets: {}, replace: false, deleted: false };
|
|
614
|
+
}
|
|
615
|
+
function groupOverlayEntries(entries) {
|
|
616
|
+
const out = /* @__PURE__ */ new Map();
|
|
617
|
+
for (const [key, value] of entries) {
|
|
618
|
+
const parsed = parseOverlayKey(key);
|
|
619
|
+
if (!parsed) continue;
|
|
620
|
+
let entry = out.get(parsed.recordId);
|
|
621
|
+
if (!entry) {
|
|
622
|
+
entry = emptyEntry(parsed.recordId);
|
|
623
|
+
out.set(parsed.recordId, entry);
|
|
624
|
+
}
|
|
625
|
+
if (parsed.marker === OVERLAY_MARKER_REPLACE) {
|
|
626
|
+
entry.replace = value === true;
|
|
627
|
+
} else if (parsed.marker === OVERLAY_MARKER_DELETED) {
|
|
628
|
+
entry.deleted = value === true;
|
|
629
|
+
} else if (parsed.member !== void 0) {
|
|
630
|
+
const field = parsed.field;
|
|
631
|
+
(entry.stringSets[field] ||= {})[parsed.member] = value === true;
|
|
632
|
+
} else if (parsed.field !== void 0) {
|
|
633
|
+
entry.fields[parsed.field] = value;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
638
|
+
function readOverlayRecord(modelMap, recordId) {
|
|
639
|
+
return collectOverlayRecords(modelMap, /* @__PURE__ */ new Set([recordId])).get(recordId);
|
|
640
|
+
}
|
|
641
|
+
function completeOverlayEntries(modelMap, entries) {
|
|
642
|
+
const rebuild = /* @__PURE__ */ new Set();
|
|
643
|
+
for (const [id, entry] of entries) {
|
|
644
|
+
if (!entry.replace || entry.deleted) continue;
|
|
645
|
+
rebuild.add(id);
|
|
646
|
+
}
|
|
647
|
+
if (rebuild.size === 0) return entries;
|
|
648
|
+
for (const [id, entry] of collectOverlayRecords(modelMap, rebuild)) {
|
|
649
|
+
entries.set(id, entry);
|
|
650
|
+
}
|
|
651
|
+
return entries;
|
|
652
|
+
}
|
|
653
|
+
function collectOverlayRecords(modelMap, recordIds) {
|
|
654
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
655
|
+
for (const id of recordIds) wanted.set(encodeOverlaySegment(id), id);
|
|
656
|
+
const own = /* @__PURE__ */ new Map();
|
|
657
|
+
for (const [key, value] of modelMap.entries()) {
|
|
658
|
+
if (typeof key !== "string") continue;
|
|
659
|
+
const separator = key.indexOf(OVERLAY_KEY_SEPARATOR);
|
|
660
|
+
if (separator <= 0) continue;
|
|
661
|
+
const id = wanted.get(key.slice(0, separator));
|
|
662
|
+
if (id === void 0) continue;
|
|
663
|
+
if (!parseOverlayKey(key)) continue;
|
|
664
|
+
const keys = own.get(id);
|
|
665
|
+
if (keys) keys.push([key, value]);
|
|
666
|
+
else own.set(id, [[key, value]]);
|
|
667
|
+
}
|
|
668
|
+
const out = /* @__PURE__ */ new Map();
|
|
669
|
+
for (const id of recordIds) {
|
|
670
|
+
out.set(id, groupOverlayEntries(own.get(id) ?? []).get(id) ?? emptyEntry(id));
|
|
671
|
+
}
|
|
672
|
+
return out;
|
|
673
|
+
}
|
|
674
|
+
function baseMembers(value) {
|
|
675
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
676
|
+
if (value && typeof value === "object") {
|
|
677
|
+
return Object.entries(value).filter(([, present]) => Boolean(present)).map(([member]) => member);
|
|
678
|
+
}
|
|
679
|
+
return [];
|
|
680
|
+
}
|
|
681
|
+
function materializeOverlayRecord(base, overlay) {
|
|
682
|
+
if (overlay.deleted) return null;
|
|
683
|
+
const out = overlay.replace ? {} : { ...base ?? {} };
|
|
684
|
+
for (const [field, value] of Object.entries(overlay.fields)) {
|
|
685
|
+
if (value === null) delete out[field];
|
|
686
|
+
else out[field] = value;
|
|
687
|
+
}
|
|
688
|
+
for (const [field, members] of Object.entries(overlay.stringSets)) {
|
|
689
|
+
const current = overlay.replace ? [] : baseMembers(out[field]);
|
|
690
|
+
const next = current.slice();
|
|
691
|
+
for (const [member, present] of Object.entries(members)) {
|
|
692
|
+
const at = next.indexOf(member);
|
|
693
|
+
if (present && at === -1) next.push(member);
|
|
694
|
+
else if (!present && at !== -1) next.splice(at, 1);
|
|
695
|
+
}
|
|
696
|
+
out[field] = next;
|
|
697
|
+
}
|
|
698
|
+
out.id = overlay.id;
|
|
699
|
+
return out;
|
|
700
|
+
}
|
|
701
|
+
function encodeOverlayMutation(mutation) {
|
|
702
|
+
const { id, kind } = mutation;
|
|
703
|
+
const entries = [];
|
|
704
|
+
if (kind === "delete") {
|
|
705
|
+
entries.push([overlayMarkerKey(id, OVERLAY_MARKER_DELETED), true]);
|
|
706
|
+
return entries;
|
|
707
|
+
}
|
|
708
|
+
if (kind === "create") {
|
|
709
|
+
entries.push([overlayMarkerKey(id, OVERLAY_MARKER_REPLACE), true]);
|
|
710
|
+
entries.push([overlayMarkerKey(id, OVERLAY_MARKER_DELETED), false]);
|
|
711
|
+
}
|
|
712
|
+
for (const [field, value] of Object.entries(mutation.fields ?? {})) {
|
|
713
|
+
entries.push([overlayFieldKey(id, field), value === void 0 ? null : value]);
|
|
714
|
+
}
|
|
715
|
+
for (const [field, members] of Object.entries(mutation.stringSetDeltas ?? {})) {
|
|
716
|
+
for (const [member, present] of Object.entries(members)) {
|
|
717
|
+
entries.push([overlayMemberKey(id, field, member), present]);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return entries;
|
|
721
|
+
}
|
|
722
|
+
function applyOverlayMutation(modelMap, mutation) {
|
|
723
|
+
if (mutation.kind === "create") {
|
|
724
|
+
clearStaleRecordOverlay(modelMap, mutation.id);
|
|
725
|
+
}
|
|
726
|
+
for (const [key, value] of encodeOverlayMutation(mutation)) {
|
|
727
|
+
modelMap.set(key, value);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function clearStaleRecordOverlay(modelMap, recordId) {
|
|
731
|
+
if (!modelMap.has(overlayMarkerKey(recordId, OVERLAY_MARKER_DELETED))) return;
|
|
732
|
+
const prefix = `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}`;
|
|
733
|
+
const stale = [];
|
|
734
|
+
for (const key of modelMap.keys()) {
|
|
735
|
+
if (key.startsWith(prefix)) stale.push(key);
|
|
736
|
+
}
|
|
737
|
+
for (const key of stale) modelMap.delete(key);
|
|
738
|
+
}
|
|
739
|
+
function assertNoFormat2CompositeFields(modelName, recordId, fields) {
|
|
740
|
+
if (!fields) return;
|
|
741
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
742
|
+
if (isYjsComposite(value)) {
|
|
743
|
+
throw new Format2CompositeFieldError(modelName, recordId, field);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function isFormat2StringSetYMap(value) {
|
|
748
|
+
if (!(value instanceof Y6.Map)) return false;
|
|
749
|
+
for (const member of value.values()) {
|
|
750
|
+
if (member !== true && member !== false) return false;
|
|
751
|
+
}
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
function format2StringSetMembers(value) {
|
|
755
|
+
if (!(value instanceof Y6.Map)) return [];
|
|
756
|
+
return Array.from(value.entries()).filter(([, present]) => present === true).map(([member]) => member);
|
|
757
|
+
}
|
|
758
|
+
var OVERLAY_KEY_SEPARATOR, OVERLAY_MARKER_REPLACE, OVERLAY_MARKER_DELETED, OVERLAY_MARKERS, Format2CompositeFieldError;
|
|
759
|
+
var init_largeDocuments = __esm({
|
|
760
|
+
"src/utils/largeDocuments.ts"() {
|
|
761
|
+
"use strict";
|
|
762
|
+
init_yjsValues();
|
|
763
|
+
OVERLAY_KEY_SEPARATOR = "/";
|
|
764
|
+
OVERLAY_MARKER_REPLACE = "_replace";
|
|
765
|
+
OVERLAY_MARKER_DELETED = "_deleted";
|
|
766
|
+
OVERLAY_MARKERS = /* @__PURE__ */ new Set([
|
|
767
|
+
OVERLAY_MARKER_REPLACE,
|
|
768
|
+
OVERLAY_MARKER_DELETED
|
|
769
|
+
]);
|
|
770
|
+
Format2CompositeFieldError = class extends Error {
|
|
771
|
+
modelName;
|
|
772
|
+
recordId;
|
|
773
|
+
field;
|
|
774
|
+
constructor(modelName, recordId, field) {
|
|
775
|
+
super(
|
|
776
|
+
`Composite field values are not supported on large documents (format 2): ${modelName}.${field} (id=${recordId})`
|
|
777
|
+
);
|
|
778
|
+
this.name = "Format2CompositeFieldError";
|
|
779
|
+
this.modelName = modelName;
|
|
780
|
+
this.recordId = recordId;
|
|
781
|
+
this.field = field;
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// src/models/OrmContext.ts
|
|
788
|
+
var OrmContext, defaultOrmContext, liveOrmContexts;
|
|
789
|
+
var init_OrmContext = __esm({
|
|
790
|
+
"src/models/OrmContext.ts"() {
|
|
791
|
+
"use strict";
|
|
792
|
+
OrmContext = class _OrmContext {
|
|
793
|
+
/** Declared before the field that reads it: static initialization is ordered. */
|
|
794
|
+
static instanceCounter = 0;
|
|
795
|
+
/**
|
|
796
|
+
* Which instance this is, for diagnostics. A process holding several clients
|
|
797
|
+
* needs a way to say WHICH one a warning is about, so every context takes the
|
|
798
|
+
* next number as it is created and keeps it for its lifetime.
|
|
799
|
+
*/
|
|
800
|
+
instanceId = ++_OrmContext.instanceCounter;
|
|
801
|
+
/** The engine this instance's models read and write through. */
|
|
802
|
+
dbEngine = null;
|
|
803
|
+
connectedDocuments = /* @__PURE__ */ new Map();
|
|
804
|
+
/** `docId_modelName` → the Y.Map that document holds for that model. */
|
|
805
|
+
documentYMaps = /* @__PURE__ */ new Map();
|
|
806
|
+
/** `subscribe()` callbacks, by model name. */
|
|
807
|
+
listenersMap = /* @__PURE__ */ new Map();
|
|
808
|
+
// Default-document resolution
|
|
809
|
+
modelNameToDefaultDocId = /* @__PURE__ */ new Map();
|
|
810
|
+
globalDefaultDocId = void 0;
|
|
811
|
+
defaultDocChangedListeners = /* @__PURE__ */ new Set();
|
|
812
|
+
modelDocMappingChangedListeners = /* @__PURE__ */ new Set();
|
|
813
|
+
/** Format-2 ("large document", #2816) record stores by document id. */
|
|
814
|
+
format2Documents = /* @__PURE__ */ new Map();
|
|
815
|
+
/** Format-2 epoch moves in flight (#2816). */
|
|
816
|
+
format2Transitions = /* @__PURE__ */ new Map();
|
|
817
|
+
/**
|
|
818
|
+
* Nested-Y.Map stringset fields this context has already observed. Per
|
|
819
|
+
* context, not global: a Y.Doc reconnected to a fresh instance has to be
|
|
820
|
+
* observed again by that instance, and a global guard would silently
|
|
821
|
+
* suppress it.
|
|
822
|
+
*/
|
|
823
|
+
observedStringSetMaps = /* @__PURE__ */ new WeakSet();
|
|
824
|
+
/**
|
|
825
|
+
* The registry this instance's schema lookups use. `null` on the standing
|
|
826
|
+
* default context, where callers fall back to the global singleton.
|
|
827
|
+
*/
|
|
828
|
+
modelRegistry = null;
|
|
829
|
+
/** Model classes routed to this context. */
|
|
830
|
+
boundModels = /* @__PURE__ */ new Set();
|
|
831
|
+
/** True once {@link destroy} has run; bound classes keep pointing here. */
|
|
832
|
+
destroyed = false;
|
|
833
|
+
/** Yjs `unobserve` thunks by owning document id. */
|
|
834
|
+
observerDisposers = /* @__PURE__ */ new Map();
|
|
835
|
+
/** Teardown the owner (the document manager) registers for `destroy()`. */
|
|
836
|
+
teardownHooks = /* @__PURE__ */ new Set();
|
|
837
|
+
static NO_DOCUMENT_KEY = "__no_document__";
|
|
838
|
+
/**
|
|
839
|
+
* Record how to remove an observer this context installed. Every `observe()`
|
|
840
|
+
* in the ORM pairs with one of these: without it a torn-down instance's
|
|
841
|
+
* callbacks keep firing on a Y.Doc that outlives it.
|
|
842
|
+
*/
|
|
843
|
+
registerObserverDisposer(docId, dispose) {
|
|
844
|
+
const key = docId ?? _OrmContext.NO_DOCUMENT_KEY;
|
|
845
|
+
const list = this.observerDisposers.get(key);
|
|
846
|
+
if (list) list.push(dispose);
|
|
847
|
+
else this.observerDisposers.set(key, [dispose]);
|
|
848
|
+
}
|
|
849
|
+
/** Remove every observer this context installed for `docId`. */
|
|
850
|
+
disposeObserversForDocument(docId) {
|
|
851
|
+
const list = this.observerDisposers.get(docId);
|
|
852
|
+
if (!list) return;
|
|
853
|
+
this.observerDisposers.delete(docId);
|
|
854
|
+
for (const dispose of list) {
|
|
855
|
+
try {
|
|
856
|
+
dispose();
|
|
857
|
+
} catch {
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
disposeAllObservers() {
|
|
862
|
+
const buckets = Array.from(this.observerDisposers.values());
|
|
863
|
+
this.observerDisposers.clear();
|
|
864
|
+
for (const list of buckets) {
|
|
865
|
+
for (const dispose of list) {
|
|
866
|
+
try {
|
|
867
|
+
dispose();
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/** Run `hook` as part of `destroy()` (the format-2 binding registers here). */
|
|
874
|
+
onDestroy(hook) {
|
|
875
|
+
this.teardownHooks.add(hook);
|
|
876
|
+
}
|
|
877
|
+
/** The record store for `docId`, or `null` when the document is format 1. */
|
|
878
|
+
getFormat2Store(docId) {
|
|
879
|
+
if (!docId) return null;
|
|
880
|
+
return this.format2Documents.get(docId) ?? null;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* The epoch move a write to `docId` has to wait out (#2816, behavior 10), or
|
|
884
|
+
* `null` when none is in flight.
|
|
885
|
+
*/
|
|
886
|
+
format2WritesHeld(docId) {
|
|
887
|
+
if (!docId) return null;
|
|
888
|
+
return this.format2Transitions.get(docId) ?? null;
|
|
889
|
+
}
|
|
890
|
+
/** Run an epoch move with `docId`'s local writes held back (#2816). */
|
|
891
|
+
async holdFormat2Writes(docId, run) {
|
|
892
|
+
const inFlight = this.format2Transitions.get(docId);
|
|
893
|
+
if (inFlight) await inFlight.catch(() => {
|
|
894
|
+
});
|
|
895
|
+
let release;
|
|
896
|
+
const held = new Promise((resolve) => {
|
|
897
|
+
release = resolve;
|
|
898
|
+
});
|
|
899
|
+
this.format2Transitions.set(docId, held);
|
|
900
|
+
try {
|
|
901
|
+
return await run();
|
|
902
|
+
} finally {
|
|
903
|
+
if (this.format2Transitions.get(docId) === held) {
|
|
904
|
+
this.format2Transitions.delete(docId);
|
|
905
|
+
}
|
|
906
|
+
release();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/** Notify `subscribe()` callbacks registered on this context for `modelName`. */
|
|
910
|
+
notifyListeners(modelName, log, onError) {
|
|
911
|
+
const listeners = this.listenersMap.get(modelName);
|
|
912
|
+
if (!listeners) return;
|
|
913
|
+
log?.(`Notifying ${listeners.size} listeners for ${modelName}`);
|
|
914
|
+
listeners.forEach((callback) => {
|
|
915
|
+
try {
|
|
916
|
+
callback();
|
|
917
|
+
} catch (e) {
|
|
918
|
+
onError?.(e);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
// ---- Default-document resolution (the `BaseModel` statics delegate here) ----
|
|
923
|
+
setModelDefaultDocumentId(modelName, docId) {
|
|
924
|
+
const previous = this.modelNameToDefaultDocId.get(modelName);
|
|
925
|
+
this.modelNameToDefaultDocId.set(modelName, docId);
|
|
926
|
+
this.announceModelDocMapping(modelName, previous, docId);
|
|
927
|
+
}
|
|
928
|
+
removeModelDefaultDocumentId(modelName) {
|
|
929
|
+
const previous = this.modelNameToDefaultDocId.get(modelName);
|
|
930
|
+
this.modelNameToDefaultDocId.delete(modelName);
|
|
931
|
+
this.announceModelDocMapping(modelName, previous, void 0);
|
|
932
|
+
}
|
|
933
|
+
clearModelDefaultDocumentIds() {
|
|
934
|
+
const entries = Array.from(this.modelNameToDefaultDocId.entries());
|
|
935
|
+
this.modelNameToDefaultDocId.clear();
|
|
936
|
+
for (const [modelName, previous] of entries) {
|
|
937
|
+
this.announceModelDocMapping(modelName, previous, void 0);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
setGlobalDefaultDocumentId(docId) {
|
|
941
|
+
const previous = this.globalDefaultDocId;
|
|
942
|
+
this.globalDefaultDocId = docId;
|
|
943
|
+
this.announceDefaultDoc(previous, docId);
|
|
944
|
+
}
|
|
945
|
+
clearGlobalDefaultDocumentId() {
|
|
946
|
+
const previous = this.globalDefaultDocId;
|
|
947
|
+
this.globalDefaultDocId = void 0;
|
|
948
|
+
this.announceDefaultDoc(previous, void 0);
|
|
949
|
+
}
|
|
950
|
+
getModelDefaultDocumentMapping() {
|
|
951
|
+
return Object.fromEntries(this.modelNameToDefaultDocId.entries());
|
|
952
|
+
}
|
|
953
|
+
getDocumentIdForModel(modelName) {
|
|
954
|
+
return this.modelNameToDefaultDocId.get(modelName);
|
|
955
|
+
}
|
|
956
|
+
getGlobalDefaultDocumentId() {
|
|
957
|
+
return this.globalDefaultDocId;
|
|
958
|
+
}
|
|
959
|
+
onDefaultDocChanged(listener) {
|
|
960
|
+
this.defaultDocChangedListeners.add(listener);
|
|
961
|
+
return () => this.defaultDocChangedListeners.delete(listener);
|
|
962
|
+
}
|
|
963
|
+
onModelDocMappingChanged(listener) {
|
|
964
|
+
this.modelDocMappingChangedListeners.add(listener);
|
|
965
|
+
return () => this.modelDocMappingChangedListeners.delete(listener);
|
|
966
|
+
}
|
|
967
|
+
/** Drop the model→document defaults that point at a disconnected document. */
|
|
968
|
+
clearMappingsForDocId(docId) {
|
|
969
|
+
const toRemove = [];
|
|
970
|
+
for (const [modelName, mappedDocId] of this.modelNameToDefaultDocId) {
|
|
971
|
+
if (mappedDocId === docId) toRemove.push(modelName);
|
|
972
|
+
}
|
|
973
|
+
for (const modelName of toRemove) {
|
|
974
|
+
this.removeModelDefaultDocumentId(modelName);
|
|
975
|
+
}
|
|
976
|
+
if (this.globalDefaultDocId === docId) {
|
|
977
|
+
this.clearGlobalDefaultDocumentId();
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
announceDefaultDoc(previous, current) {
|
|
981
|
+
for (const listener of this.defaultDocChangedListeners) {
|
|
982
|
+
try {
|
|
983
|
+
listener({ previous, current });
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
announceModelDocMapping(modelName, previous, current) {
|
|
989
|
+
for (const listener of this.modelDocMappingChangedListeners) {
|
|
990
|
+
try {
|
|
991
|
+
listener({ modelName, previous, current });
|
|
992
|
+
} catch {
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Tear this instance down: observers off, engine destroyed, state cleared.
|
|
998
|
+
*
|
|
999
|
+
* Deliberately does NOT unbind its model classes. They keep resolving this
|
|
1000
|
+
* context, whose engine is now null, so an operation through them throws the
|
|
1001
|
+
* ordinary not-initialized error instead of silently landing on whatever
|
|
1002
|
+
* other instance happens to be live (#3039, behavior 7). Only a later
|
|
1003
|
+
* `initJsBao` may rebind a class whose context is destroyed.
|
|
1004
|
+
*/
|
|
1005
|
+
async destroy() {
|
|
1006
|
+
if (this.destroyed) return;
|
|
1007
|
+
this.destroyed = true;
|
|
1008
|
+
this.disposeAllObservers();
|
|
1009
|
+
const hooks = Array.from(this.teardownHooks);
|
|
1010
|
+
this.teardownHooks.clear();
|
|
1011
|
+
for (const hook of hooks) {
|
|
1012
|
+
try {
|
|
1013
|
+
await hook();
|
|
1014
|
+
} catch {
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const engine = this.dbEngine;
|
|
1018
|
+
this.dbEngine = null;
|
|
1019
|
+
if (engine) {
|
|
1020
|
+
try {
|
|
1021
|
+
await engine.destroy();
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
this.connectedDocuments.clear();
|
|
1026
|
+
this.documentYMaps.clear();
|
|
1027
|
+
this.listenersMap.clear();
|
|
1028
|
+
this.modelNameToDefaultDocId.clear();
|
|
1029
|
+
this.globalDefaultDocId = void 0;
|
|
1030
|
+
this.defaultDocChangedListeners.clear();
|
|
1031
|
+
this.modelDocMappingChangedListeners.clear();
|
|
1032
|
+
this.format2Documents.clear();
|
|
1033
|
+
this.format2Transitions.clear();
|
|
1034
|
+
this.observedStringSetMaps = /* @__PURE__ */ new WeakSet();
|
|
1035
|
+
liveOrmContexts.delete(this);
|
|
1036
|
+
if (defaultOrmContext === this) {
|
|
1037
|
+
defaultOrmContext = new _OrmContext();
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
defaultOrmContext = new OrmContext();
|
|
1042
|
+
liveOrmContexts = /* @__PURE__ */ new Set();
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// src/models/format2QueryProjection.ts
|
|
1047
|
+
var FORMAT2_QUERY_PROJECTION_DDL;
|
|
1048
|
+
var init_format2QueryProjection = __esm({
|
|
1049
|
+
"src/models/format2QueryProjection.ts"() {
|
|
1050
|
+
"use strict";
|
|
1051
|
+
FORMAT2_QUERY_PROJECTION_DDL = `
|
|
1052
|
+
CREATE TABLE IF NOT EXISTS _query_projection (
|
|
1053
|
+
doc_id TEXT NOT NULL,
|
|
1054
|
+
model TEXT NOT NULL,
|
|
1055
|
+
PRIMARY KEY (doc_id, model)
|
|
1056
|
+
)`.trim();
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
// src/models/format2Observer.ts
|
|
1061
|
+
import * as Y7 from "yjs";
|
|
1062
|
+
var init_format2Observer = __esm({
|
|
1063
|
+
"src/models/format2Observer.ts"() {
|
|
1064
|
+
"use strict";
|
|
1065
|
+
init_largeDocuments();
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// src/models/format2ClientWrite.ts
|
|
1070
|
+
var init_format2ClientWrite = __esm({
|
|
1071
|
+
"src/models/format2ClientWrite.ts"() {
|
|
1072
|
+
"use strict";
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// src/models/format2OfflineWindow.ts
|
|
1077
|
+
var DAY_MS;
|
|
1078
|
+
var init_format2OfflineWindow = __esm({
|
|
1079
|
+
"src/models/format2OfflineWindow.ts"() {
|
|
1080
|
+
"use strict";
|
|
1081
|
+
DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
|
|
509
1085
|
// src/models/BaseModel.ts
|
|
510
|
-
import * as
|
|
511
|
-
import { ulid as
|
|
1086
|
+
import * as Y8 from "yjs";
|
|
1087
|
+
import { ulid as ulid3 } from "ulid";
|
|
512
1088
|
function generateULID() {
|
|
513
|
-
return
|
|
1089
|
+
return ulid3();
|
|
514
1090
|
}
|
|
515
1091
|
var init_BaseModel = __esm({
|
|
516
1092
|
"src/models/BaseModel.ts"() {
|
|
@@ -521,7 +1097,15 @@ var init_BaseModel = __esm({
|
|
|
521
1097
|
init_CursorManager();
|
|
522
1098
|
init_sql();
|
|
523
1099
|
init_metaSync();
|
|
1100
|
+
init_writeLifecycle();
|
|
524
1101
|
init_yjsValues();
|
|
1102
|
+
init_uniqueIndex();
|
|
1103
|
+
init_largeDocuments();
|
|
1104
|
+
init_OrmContext();
|
|
1105
|
+
init_format2QueryProjection();
|
|
1106
|
+
init_format2Observer();
|
|
1107
|
+
init_format2ClientWrite();
|
|
1108
|
+
init_format2OfflineWindow();
|
|
525
1109
|
registerFunctionDefault(generateULID, "generate_ulid");
|
|
526
1110
|
}
|
|
527
1111
|
});
|
|
@@ -540,6 +1124,7 @@ var init_ModelRegistry = __esm({
|
|
|
540
1124
|
"use strict";
|
|
541
1125
|
init_BaseModel();
|
|
542
1126
|
init_relationshipManager();
|
|
1127
|
+
init_format2QueryProjection();
|
|
543
1128
|
}
|
|
544
1129
|
});
|
|
545
1130
|
|
|
@@ -575,6 +1160,32 @@ var DatabaseEngine = class {
|
|
|
575
1160
|
getTableName(_modelName) {
|
|
576
1161
|
throw new Error("Method not implemented.");
|
|
577
1162
|
}
|
|
1163
|
+
/**
|
|
1164
|
+
* A SYNCHRONOUS SQL executor over this engine's database, used by the
|
|
1165
|
+
* large-document (format 2) record store. That store commits a merged
|
|
1166
|
+
* record row and its pending-operation entry as one transaction — the
|
|
1167
|
+
* barrier a `save()` resolves behind — which the async `query()` API cannot
|
|
1168
|
+
* express. Engines whose underlying SQLite is synchronous (better-sqlite3,
|
|
1169
|
+
* SQL.js) provide it; anything else leaves it undefined and cannot host a
|
|
1170
|
+
* large document.
|
|
1171
|
+
*/
|
|
1172
|
+
createFormat2Sql;
|
|
1173
|
+
/**
|
|
1174
|
+
* Whether what {@link createFormat2Sql} writes survives this process.
|
|
1175
|
+
*
|
|
1176
|
+
* On a large document the merged record view IS the document: the Y.Doc
|
|
1177
|
+
* holds one epoch's overlay, and everything else — including a record a
|
|
1178
|
+
* `save()` has already resolved, and the pending write that still owes it to
|
|
1179
|
+
* the server — lives only in this store. An engine whose database is in
|
|
1180
|
+
* memory therefore cannot host one: a reload does not cost a cache, it costs
|
|
1181
|
+
* the document and the writes nobody has acknowledged yet.
|
|
1182
|
+
*
|
|
1183
|
+
* The platform is asked separately about its storage quota; this answers the
|
|
1184
|
+
* half only the engine knows. An engine that does not implement it is taken
|
|
1185
|
+
* at the platform's word, which is what keeps hosts with storage of their
|
|
1186
|
+
* own — a worker handed a file store, a test — working unchanged.
|
|
1187
|
+
*/
|
|
1188
|
+
format2StorageIsDurable;
|
|
578
1189
|
// Transaction support
|
|
579
1190
|
async withTransaction(_callback) {
|
|
580
1191
|
throw new Error("Method not implemented.");
|
|
@@ -1551,6 +2162,45 @@ var DurableObjectEngine = class extends JsonSchemaEngine {
|
|
|
1551
2162
|
init_queryTypes();
|
|
1552
2163
|
init_CursorManager();
|
|
1553
2164
|
init_sql();
|
|
2165
|
+
|
|
2166
|
+
// src/query/recordRow.ts
|
|
2167
|
+
var PROJECTED_JSON_ALIAS_PREFIX = "_json_";
|
|
2168
|
+
function projectedJsonAlias(fieldName) {
|
|
2169
|
+
return `${PROJECTED_JSON_ALIAS_PREFIX}${fieldName}`;
|
|
2170
|
+
}
|
|
2171
|
+
function parseRecordRow(row) {
|
|
2172
|
+
const { _id, _type, _data, ...rest } = row;
|
|
2173
|
+
let parsed = {};
|
|
2174
|
+
if (_data) {
|
|
2175
|
+
try {
|
|
2176
|
+
parsed = JSON.parse(_data);
|
|
2177
|
+
} catch (e) {
|
|
2178
|
+
console.warn("Failed to parse _data:", e);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return { id: _id, type: _type, ...parsed, ...decodeProjectedColumns(rest) };
|
|
2182
|
+
}
|
|
2183
|
+
function decodeProjectedColumns(columns) {
|
|
2184
|
+
const decoded = {};
|
|
2185
|
+
for (const [column, value] of Object.entries(columns)) {
|
|
2186
|
+
if (!column.startsWith(PROJECTED_JSON_ALIAS_PREFIX)) {
|
|
2187
|
+
decoded[column] = value;
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
decoded[column.slice(PROJECTED_JSON_ALIAS_PREFIX.length)] = parseJsonColumn(value);
|
|
2191
|
+
}
|
|
2192
|
+
return decoded;
|
|
2193
|
+
}
|
|
2194
|
+
function parseJsonColumn(value) {
|
|
2195
|
+
if (typeof value !== "string") return value;
|
|
2196
|
+
try {
|
|
2197
|
+
return JSON.parse(value);
|
|
2198
|
+
} catch {
|
|
2199
|
+
return value;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// src/query/JsonQueryTranslator.ts
|
|
1554
2204
|
init_patterns();
|
|
1555
2205
|
var SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
|
|
1556
2206
|
var JsonQueryTranslator = class {
|
|
@@ -1595,7 +2245,8 @@ var JsonQueryTranslator = class {
|
|
|
1595
2245
|
const whereClause = this.translateFilter(filter);
|
|
1596
2246
|
const orderClause = CursorManager.buildOrderClause(
|
|
1597
2247
|
options?.sort,
|
|
1598
|
-
(field) => this.getFieldSql(field)
|
|
2248
|
+
(field) => this.getFieldSql(field),
|
|
2249
|
+
options?.direction || 1
|
|
1599
2250
|
);
|
|
1600
2251
|
const limitClause = this.buildLimitClause(options);
|
|
1601
2252
|
const paginationClause = this.buildPaginationClause(
|
|
@@ -1868,8 +2519,11 @@ var JsonQueryTranslator = class {
|
|
|
1868
2519
|
};
|
|
1869
2520
|
case "$ne":
|
|
1870
2521
|
this.validateFieldValue(fieldName, value);
|
|
2522
|
+
if (value === null || value === void 0) {
|
|
2523
|
+
return { sql: `${fieldSql} IS NOT NULL`, params: [] };
|
|
2524
|
+
}
|
|
1871
2525
|
return {
|
|
1872
|
-
sql:
|
|
2526
|
+
sql: `(${fieldSql} IS NULL OR ${fieldSql} != ?)`,
|
|
1873
2527
|
params: [this.convertValueForSQLite(value)]
|
|
1874
2528
|
};
|
|
1875
2529
|
case "$gt":
|
|
@@ -1952,12 +2606,23 @@ var JsonQueryTranslator = class {
|
|
|
1952
2606
|
if (value.length === 0) {
|
|
1953
2607
|
return { sql: "", params: [] };
|
|
1954
2608
|
}
|
|
1955
|
-
const
|
|
1956
|
-
const
|
|
2609
|
+
const ninHasNull = value.some((v) => v === null || v === void 0);
|
|
2610
|
+
const ninValues = value.filter((v) => v !== null && v !== void 0);
|
|
2611
|
+
const ninPlaceholders = ninValues.map(() => "?").join(",");
|
|
2612
|
+
const convertedNinValues = ninValues.map(
|
|
1957
2613
|
(v) => this.convertValueForSQLite(v)
|
|
1958
2614
|
);
|
|
2615
|
+
if (ninHasNull) {
|
|
2616
|
+
if (ninValues.length === 0) {
|
|
2617
|
+
return { sql: `${fieldSql} IS NOT NULL`, params: [] };
|
|
2618
|
+
}
|
|
2619
|
+
return {
|
|
2620
|
+
sql: `(${fieldSql} IS NOT NULL AND ${fieldSql} NOT IN (${ninPlaceholders}))`,
|
|
2621
|
+
params: convertedNinValues
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
1959
2624
|
return {
|
|
1960
|
-
sql:
|
|
2625
|
+
sql: `(${fieldSql} IS NULL OR ${fieldSql} NOT IN (${ninPlaceholders}))`,
|
|
1961
2626
|
params: convertedNinValues
|
|
1962
2627
|
};
|
|
1963
2628
|
case "$exists":
|
|
@@ -2046,7 +2711,7 @@ var JsonQueryTranslator = class {
|
|
|
2046
2711
|
for (const field of includeFields) {
|
|
2047
2712
|
if (field === "id" || field === "type") continue;
|
|
2048
2713
|
selectParts.push(
|
|
2049
|
-
`
|
|
2714
|
+
`_data -> '$.${field}' AS ${quoteIdentifier(projectedJsonAlias(field))}`
|
|
2050
2715
|
);
|
|
2051
2716
|
}
|
|
2052
2717
|
return selectParts.join(", ");
|
|
@@ -2233,6 +2898,13 @@ var JsonQueryTranslator = class {
|
|
|
2233
2898
|
init_CursorManager();
|
|
2234
2899
|
init_sql();
|
|
2235
2900
|
import { ulid } from "ulid";
|
|
2901
|
+
var AtomicBatchAbort = class extends Error {
|
|
2902
|
+
constructor(failedIndex) {
|
|
2903
|
+
super(`atomic batch aborted at operation ${failedIndex}`);
|
|
2904
|
+
this.failedIndex = failedIndex;
|
|
2905
|
+
this.name = "AtomicBatchAbort";
|
|
2906
|
+
}
|
|
2907
|
+
};
|
|
2236
2908
|
function checkReservedFieldName(fieldName) {
|
|
2237
2909
|
if (fieldName === "id") return null;
|
|
2238
2910
|
if (fieldName === "type") {
|
|
@@ -2277,6 +2949,35 @@ function applyAutoStamps(data, autoStamps, isNew) {
|
|
|
2277
2949
|
}
|
|
2278
2950
|
}
|
|
2279
2951
|
}
|
|
2952
|
+
function collectAppliedStamps(data, autoStamps, isNew) {
|
|
2953
|
+
if (!autoStamps || !data) return void 0;
|
|
2954
|
+
const stamps = {};
|
|
2955
|
+
const take = (fields) => {
|
|
2956
|
+
if (!fields) return;
|
|
2957
|
+
for (const field of fields) {
|
|
2958
|
+
const value = data[field];
|
|
2959
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2960
|
+
stamps[field] = value;
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
};
|
|
2964
|
+
if (isNew) take(autoStamps.create);
|
|
2965
|
+
take(autoStamps.update);
|
|
2966
|
+
return Object.keys(stamps).length > 0 ? stamps : void 0;
|
|
2967
|
+
}
|
|
2968
|
+
function collectAppliedValues(data, fieldNames) {
|
|
2969
|
+
if (!data || !fieldNames || fieldNames.length === 0) return void 0;
|
|
2970
|
+
const out = {};
|
|
2971
|
+
for (const name of fieldNames) {
|
|
2972
|
+
const value = data[name];
|
|
2973
|
+
if (value !== void 0) out[name] = value;
|
|
2974
|
+
}
|
|
2975
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2976
|
+
}
|
|
2977
|
+
function mergeAppliedFields(stamps, applied) {
|
|
2978
|
+
if (!stamps && !applied) return void 0;
|
|
2979
|
+
return { ...stamps ?? {}, ...applied ?? {} };
|
|
2980
|
+
}
|
|
2280
2981
|
function createDatabaseDO(config = {}) {
|
|
2281
2982
|
const hooks = config.hooks;
|
|
2282
2983
|
return class DocumentDO {
|
|
@@ -2434,6 +3135,16 @@ function createDatabaseDO(config = {}) {
|
|
|
2434
3135
|
hasMore,
|
|
2435
3136
|
isFirstPage
|
|
2436
3137
|
);
|
|
3138
|
+
if (options?.projection && Object.keys(options.projection).length > 0) {
|
|
3139
|
+
const isExclusion = Object.values(options.projection).some(
|
|
3140
|
+
(v) => v === 0
|
|
3141
|
+
);
|
|
3142
|
+
if (isExclusion) {
|
|
3143
|
+
data = data.map(
|
|
3144
|
+
(rec) => this._applyProjection(rec, options.projection)
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
2437
3148
|
const response = {
|
|
2438
3149
|
data,
|
|
2439
3150
|
hasMore,
|
|
@@ -2536,6 +3247,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2536
3247
|
}
|
|
2537
3248
|
const isNew = !this._engine.recordExists(modelName, id);
|
|
2538
3249
|
applyAutoStamps(data, autoStamps, isNew);
|
|
3250
|
+
let hookAppliedNames;
|
|
2539
3251
|
if (hooks?.beforeSave) {
|
|
2540
3252
|
const ctx = {
|
|
2541
3253
|
modelName,
|
|
@@ -2551,6 +3263,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2551
3263
|
if (!result.allow) {
|
|
2552
3264
|
return this._errorResponse(result.reason || "Save denied", 403);
|
|
2553
3265
|
}
|
|
3266
|
+
hookAppliedNames = result.serverAppliedFieldNames;
|
|
2554
3267
|
}
|
|
2555
3268
|
const existingForUnique = this._engine.execSqlSync(
|
|
2556
3269
|
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
@@ -2580,6 +3293,11 @@ function createDatabaseDO(config = {}) {
|
|
|
2580
3293
|
}
|
|
2581
3294
|
this._engine.trackModelFields(modelName, data);
|
|
2582
3295
|
const response = { success: true, id };
|
|
3296
|
+
const appliedFields = mergeAppliedFields(
|
|
3297
|
+
collectAppliedStamps(data, autoStamps, isNew),
|
|
3298
|
+
collectAppliedValues(data, hookAppliedNames)
|
|
3299
|
+
);
|
|
3300
|
+
if (appliedFields) response.appliedFields = appliedFields;
|
|
2583
3301
|
return Response.json(response);
|
|
2584
3302
|
}
|
|
2585
3303
|
/** @internal — Partial update: merge provided fields into existing record */
|
|
@@ -2609,6 +3327,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2609
3327
|
);
|
|
2610
3328
|
}
|
|
2611
3329
|
applyAutoStamps(data, autoStamps, false);
|
|
3330
|
+
let hookAppliedNames;
|
|
2612
3331
|
if (hooks?.beforeSave) {
|
|
2613
3332
|
const ctx = {
|
|
2614
3333
|
modelName,
|
|
@@ -2624,6 +3343,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2624
3343
|
if (!result.allow) {
|
|
2625
3344
|
return this._errorResponse(result.reason || "Save denied", 403);
|
|
2626
3345
|
}
|
|
3346
|
+
hookAppliedNames = result.serverAppliedFieldNames;
|
|
2627
3347
|
}
|
|
2628
3348
|
const existing = this._engine.execSqlSync(
|
|
2629
3349
|
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
@@ -2647,6 +3367,11 @@ function createDatabaseDO(config = {}) {
|
|
|
2647
3367
|
}
|
|
2648
3368
|
this._engine.trackModelFields(modelName, data);
|
|
2649
3369
|
const response = { success: true, id };
|
|
3370
|
+
const appliedFields = mergeAppliedFields(
|
|
3371
|
+
collectAppliedStamps(data, autoStamps, false),
|
|
3372
|
+
collectAppliedValues(data, hookAppliedNames)
|
|
3373
|
+
);
|
|
3374
|
+
if (appliedFields) response.appliedFields = appliedFields;
|
|
2650
3375
|
return Response.json(response);
|
|
2651
3376
|
}
|
|
2652
3377
|
/** @internal */
|
|
@@ -2688,7 +3413,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2688
3413
|
/** @internal — Execute multiple save/patch/delete operations in a single transaction */
|
|
2689
3414
|
async _handleBatch(request, docId) {
|
|
2690
3415
|
const body = await request.json();
|
|
2691
|
-
const { operations } = body;
|
|
3416
|
+
const { operations, atomic = false } = body;
|
|
2692
3417
|
if (!operations || !Array.isArray(operations) || operations.length === 0) {
|
|
2693
3418
|
return this._errorResponse("operations array is required and must not be empty", 400);
|
|
2694
3419
|
}
|
|
@@ -2696,6 +3421,8 @@ function createDatabaseDO(config = {}) {
|
|
|
2696
3421
|
const resolvedBatchIds = /* @__PURE__ */ new Map();
|
|
2697
3422
|
const opErrors = /* @__PURE__ */ new Map();
|
|
2698
3423
|
const batchCreatedIds = /* @__PURE__ */ new Set();
|
|
3424
|
+
const batchIsNew = /* @__PURE__ */ new Map();
|
|
3425
|
+
const batchAppliedNames = /* @__PURE__ */ new Map();
|
|
2699
3426
|
for (let i = 0; i < operations.length; i++) {
|
|
2700
3427
|
const op = operations[i];
|
|
2701
3428
|
if (!op.modelName || !op.id && !(op.op === "save" && op.upsertOn) || !op.op) {
|
|
@@ -2769,6 +3496,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2769
3496
|
if (op.op === "save" && isNew && effectiveId) {
|
|
2770
3497
|
batchCreatedIds.add(`${op.modelName}/${effectiveId}`);
|
|
2771
3498
|
}
|
|
3499
|
+
batchIsNew.set(i, isNew);
|
|
2772
3500
|
applyAutoStamps(op.data, op.autoStamps, isNew);
|
|
2773
3501
|
if (hooks?.beforeSave) {
|
|
2774
3502
|
const ctx = {
|
|
@@ -2784,6 +3512,8 @@ function createDatabaseDO(config = {}) {
|
|
|
2784
3512
|
const result = await hooks.beforeSave(ctx);
|
|
2785
3513
|
if (!result.allow) {
|
|
2786
3514
|
hookDenials.set(i, result.reason || "Save denied");
|
|
3515
|
+
} else if (result.serverAppliedFieldNames) {
|
|
3516
|
+
batchAppliedNames.set(i, result.serverAppliedFieldNames);
|
|
2787
3517
|
}
|
|
2788
3518
|
}
|
|
2789
3519
|
} else if (op.op === "delete") {
|
|
@@ -2832,205 +3562,263 @@ function createDatabaseDO(config = {}) {
|
|
|
2832
3562
|
}
|
|
2833
3563
|
const results = [];
|
|
2834
3564
|
const fieldsToTrack = [];
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
|
|
2857
|
-
results.push({
|
|
2858
|
-
success: false,
|
|
2859
|
-
id: conditionId,
|
|
2860
|
-
error: `Condition not met for ${op.modelName}/${conditionId}`
|
|
2861
|
-
});
|
|
2862
|
-
continue;
|
|
2863
|
-
}
|
|
2864
|
-
if (op.op === "save") {
|
|
2865
|
-
let opId = resolvedBatchIds.get(i) || op.id;
|
|
2866
|
-
if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
|
|
2867
|
-
results.push({
|
|
2868
|
-
success: false,
|
|
2869
|
-
id: opId,
|
|
2870
|
-
error: `Record ${op.modelName}/${opId} already exists`
|
|
3565
|
+
const pushFailure = (index, failure) => {
|
|
3566
|
+
results.push({
|
|
3567
|
+
success: false,
|
|
3568
|
+
id: failure.id,
|
|
3569
|
+
error: failure.error,
|
|
3570
|
+
status: failure.status,
|
|
3571
|
+
code: failure.code
|
|
3572
|
+
});
|
|
3573
|
+
if (atomic) throw new AtomicBatchAbort(index);
|
|
3574
|
+
};
|
|
3575
|
+
try {
|
|
3576
|
+
this._engine.transactionSync(() => {
|
|
3577
|
+
for (let i = 0; i < operations.length; i++) {
|
|
3578
|
+
const op = operations[i];
|
|
3579
|
+
if (hookDenials.has(i)) {
|
|
3580
|
+
const effectiveId = resolvedBatchIds.get(i) || op.id || "";
|
|
3581
|
+
pushFailure(i, {
|
|
3582
|
+
id: effectiveId,
|
|
3583
|
+
error: hookDenials.get(i),
|
|
3584
|
+
status: 403,
|
|
3585
|
+
code: "HOOK_DENIED"
|
|
2871
3586
|
});
|
|
2872
3587
|
continue;
|
|
2873
3588
|
}
|
|
2874
|
-
if (
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3589
|
+
if (opErrors.has(i)) {
|
|
3590
|
+
pushFailure(i, {
|
|
3591
|
+
id: op.id || "",
|
|
3592
|
+
error: opErrors.get(i),
|
|
3593
|
+
status: 400,
|
|
3594
|
+
code: "VALIDATION_ERROR"
|
|
3595
|
+
});
|
|
3596
|
+
continue;
|
|
3597
|
+
}
|
|
3598
|
+
const conditionId = resolvedBatchIds.get(i) || op.id;
|
|
3599
|
+
if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
|
|
3600
|
+
pushFailure(i, {
|
|
3601
|
+
id: conditionId,
|
|
3602
|
+
error: `Condition not met for ${op.modelName}/${conditionId}`,
|
|
3603
|
+
status: 409,
|
|
3604
|
+
code: "CONDITION_NOT_MET"
|
|
3605
|
+
});
|
|
3606
|
+
continue;
|
|
3607
|
+
}
|
|
3608
|
+
if (op.op === "save") {
|
|
3609
|
+
let opId = resolvedBatchIds.get(i) || op.id;
|
|
3610
|
+
if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
|
|
3611
|
+
pushFailure(i, {
|
|
2892
3612
|
id: opId,
|
|
2893
|
-
error:
|
|
3613
|
+
error: `Record ${op.modelName}/${opId} already exists`,
|
|
3614
|
+
status: 409,
|
|
3615
|
+
code: "ALREADY_EXISTS"
|
|
2894
3616
|
});
|
|
2895
3617
|
continue;
|
|
2896
3618
|
}
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
const dataJson = JSON.stringify(jsonFields);
|
|
2902
|
-
this._engine.execSqlSync(
|
|
2903
|
-
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
2904
|
-
[opId, op.modelName, dataJson]
|
|
2905
|
-
);
|
|
2906
|
-
for (const [fieldName, values] of Object.entries(op.stringSets)) {
|
|
2907
|
-
this._engine.execSqlSync(
|
|
2908
|
-
"DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
|
|
2909
|
-
[op.modelName, opId, fieldName]
|
|
3619
|
+
if (op.data) {
|
|
3620
|
+
const existingForUnique = this._engine.execSqlSync(
|
|
3621
|
+
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
3622
|
+
[opId, op.modelName]
|
|
2910
3623
|
);
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
);
|
|
3624
|
+
let dataForUnique = op.data;
|
|
3625
|
+
if (existingForUnique.length > 0) {
|
|
3626
|
+
const existingData = JSON.parse(existingForUnique[0]._data);
|
|
3627
|
+
dataForUnique = { ...existingData, ...op.data };
|
|
2916
3628
|
}
|
|
2917
|
-
}
|
|
2918
|
-
} else {
|
|
2919
|
-
const { id: _id, ...fieldsForJson } = op.data;
|
|
2920
|
-
const dataJson = JSON.stringify(fieldsForJson);
|
|
2921
|
-
this._engine.execSqlSync(
|
|
2922
|
-
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
2923
|
-
[opId, op.modelName, dataJson]
|
|
2924
|
-
);
|
|
2925
|
-
}
|
|
2926
|
-
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
2927
|
-
results.push({ success: true, id: opId });
|
|
2928
|
-
} else if (op.op === "patch") {
|
|
2929
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
2930
|
-
results.push({
|
|
2931
|
-
success: false,
|
|
2932
|
-
id: op.id,
|
|
2933
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
2934
|
-
});
|
|
2935
|
-
continue;
|
|
2936
|
-
}
|
|
2937
|
-
if (op.data) {
|
|
2938
|
-
const existing = this._engine.execSqlSync(
|
|
2939
|
-
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
2940
|
-
[op.id, op.modelName]
|
|
2941
|
-
);
|
|
2942
|
-
if (existing.length > 0) {
|
|
2943
|
-
const existingData = JSON.parse(existing[0]._data);
|
|
2944
|
-
const mergedData = { ...existingData, ...op.data };
|
|
2945
3629
|
const violation = this._engine.checkUniqueConstraints(
|
|
2946
3630
|
op.modelName,
|
|
2947
|
-
|
|
2948
|
-
|
|
3631
|
+
opId,
|
|
3632
|
+
dataForUnique
|
|
2949
3633
|
);
|
|
2950
3634
|
if (violation) {
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3635
|
+
pushFailure(i, {
|
|
3636
|
+
id: opId,
|
|
3637
|
+
error: violation,
|
|
3638
|
+
status: 409,
|
|
3639
|
+
code: "UNIQUE_VIOLATION"
|
|
2955
3640
|
});
|
|
2956
3641
|
continue;
|
|
2957
3642
|
}
|
|
2958
3643
|
}
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3644
|
+
if (op.stringSets && Object.keys(op.stringSets).length > 0) {
|
|
3645
|
+
const { id: _id, ...fieldsForJson } = op.data;
|
|
3646
|
+
const jsonFields = { ...fieldsForJson, ...op.stringSets };
|
|
3647
|
+
const dataJson = JSON.stringify(jsonFields);
|
|
3648
|
+
this._engine.execSqlSync(
|
|
3649
|
+
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
3650
|
+
[opId, op.modelName, dataJson]
|
|
3651
|
+
);
|
|
3652
|
+
for (const [fieldName, values] of Object.entries(op.stringSets)) {
|
|
3653
|
+
this._engine.execSqlSync(
|
|
3654
|
+
"DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
|
|
3655
|
+
[op.modelName, opId, fieldName]
|
|
3656
|
+
);
|
|
3657
|
+
for (const value of values) {
|
|
3658
|
+
this._engine.execSqlSync(
|
|
3659
|
+
"INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
|
|
3660
|
+
[opId, op.modelName, fieldName, value]
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
} else {
|
|
3665
|
+
const { id: _id, ...fieldsForJson } = op.data;
|
|
3666
|
+
const dataJson = JSON.stringify(fieldsForJson);
|
|
3667
|
+
this._engine.execSqlSync(
|
|
3668
|
+
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
3669
|
+
[opId, op.modelName, dataJson]
|
|
3670
|
+
);
|
|
3671
|
+
}
|
|
2973
3672
|
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
3673
|
+
const saveResult = { success: true, id: opId };
|
|
3674
|
+
const saveApplied = mergeAppliedFields(
|
|
3675
|
+
collectAppliedStamps(
|
|
3676
|
+
op.data,
|
|
3677
|
+
op.autoStamps,
|
|
3678
|
+
batchIsNew.get(i) ?? false
|
|
3679
|
+
),
|
|
3680
|
+
collectAppliedValues(op.data, batchAppliedNames.get(i))
|
|
3681
|
+
);
|
|
3682
|
+
if (saveApplied) saveResult.appliedFields = saveApplied;
|
|
3683
|
+
results.push(saveResult);
|
|
3684
|
+
} else if (op.op === "patch") {
|
|
3685
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3686
|
+
pushFailure(i, {
|
|
3687
|
+
id: op.id,
|
|
3688
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3689
|
+
status: 404,
|
|
3690
|
+
code: "NOT_FOUND"
|
|
3691
|
+
});
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3694
|
+
if (op.data) {
|
|
3695
|
+
const existing = this._engine.execSqlSync(
|
|
3696
|
+
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
3697
|
+
[op.id, op.modelName]
|
|
3698
|
+
);
|
|
3699
|
+
if (existing.length > 0) {
|
|
3700
|
+
const existingData = JSON.parse(existing[0]._data);
|
|
3701
|
+
const mergedData = { ...existingData, ...op.data };
|
|
3702
|
+
const violation = this._engine.checkUniqueConstraints(
|
|
3703
|
+
op.modelName,
|
|
3704
|
+
op.id,
|
|
3705
|
+
mergedData
|
|
3706
|
+
);
|
|
3707
|
+
if (violation) {
|
|
3708
|
+
pushFailure(i, {
|
|
3709
|
+
id: op.id,
|
|
3710
|
+
error: violation,
|
|
3711
|
+
status: 409,
|
|
3712
|
+
code: "UNIQUE_VIOLATION"
|
|
3713
|
+
});
|
|
3714
|
+
continue;
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
const found = this._engine.patchRecordRaw(
|
|
3719
|
+
op.modelName,
|
|
3720
|
+
op.id,
|
|
3721
|
+
op.data,
|
|
3722
|
+
op.stringSets
|
|
3723
|
+
);
|
|
3724
|
+
if (!found) {
|
|
3725
|
+
pushFailure(i, {
|
|
3726
|
+
id: op.id,
|
|
3727
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3728
|
+
status: 404,
|
|
3729
|
+
code: "NOT_FOUND"
|
|
3730
|
+
});
|
|
3731
|
+
} else {
|
|
3732
|
+
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
3733
|
+
const patchResult = {
|
|
3734
|
+
success: true,
|
|
3735
|
+
id: op.id
|
|
3736
|
+
};
|
|
3737
|
+
const patchApplied = mergeAppliedFields(
|
|
3738
|
+
collectAppliedStamps(op.data, op.autoStamps, false),
|
|
3739
|
+
collectAppliedValues(op.data, batchAppliedNames.get(i))
|
|
3740
|
+
);
|
|
3741
|
+
if (patchApplied) patchResult.appliedFields = patchApplied;
|
|
3742
|
+
results.push(patchResult);
|
|
3743
|
+
}
|
|
3744
|
+
} else if (op.op === "delete") {
|
|
3745
|
+
this._engine.execSqlSync(
|
|
3746
|
+
"DELETE FROM records WHERE _id = ? AND _type = ?",
|
|
3747
|
+
[op.id, op.modelName]
|
|
3748
|
+
);
|
|
3749
|
+
this._engine.execSqlSync(
|
|
3750
|
+
"DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
|
|
3751
|
+
[op.id, op.modelName]
|
|
3752
|
+
);
|
|
3753
|
+
results.push({ success: true, id: op.id });
|
|
3754
|
+
} else if (op.op === "increment") {
|
|
3755
|
+
const newValues = this._engine.incrementFieldsRaw(
|
|
3756
|
+
op.modelName,
|
|
3757
|
+
op.id,
|
|
3758
|
+
op.fields
|
|
3759
|
+
);
|
|
3760
|
+
if (!newValues) {
|
|
3761
|
+
pushFailure(i, {
|
|
3762
|
+
id: op.id,
|
|
3763
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3764
|
+
status: 404,
|
|
3765
|
+
code: "NOT_FOUND"
|
|
3766
|
+
});
|
|
3767
|
+
} else {
|
|
3768
|
+
results.push({ success: true, id: op.id, values: newValues });
|
|
3769
|
+
}
|
|
3770
|
+
} else if (op.op === "addToSet") {
|
|
3771
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3772
|
+
pushFailure(i, {
|
|
3773
|
+
id: op.id,
|
|
3774
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3775
|
+
status: 404,
|
|
3776
|
+
code: "NOT_FOUND"
|
|
3777
|
+
});
|
|
3778
|
+
continue;
|
|
3779
|
+
}
|
|
3780
|
+
this._engine.addToStringSetsRaw(
|
|
3781
|
+
op.modelName,
|
|
3782
|
+
op.id,
|
|
3783
|
+
op.stringSets
|
|
3784
|
+
);
|
|
3785
|
+
results.push({ success: true, id: op.id });
|
|
3786
|
+
} else if (op.op === "removeFromSet") {
|
|
3787
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3788
|
+
pushFailure(i, {
|
|
3789
|
+
id: op.id,
|
|
3790
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3791
|
+
status: 404,
|
|
3792
|
+
code: "NOT_FOUND"
|
|
3793
|
+
});
|
|
3794
|
+
continue;
|
|
3795
|
+
}
|
|
3796
|
+
this._engine.removeFromStringSetsRaw(
|
|
3797
|
+
op.modelName,
|
|
3798
|
+
op.id,
|
|
3799
|
+
op.stringSets
|
|
3800
|
+
);
|
|
2974
3801
|
results.push({ success: true, id: op.id });
|
|
2975
3802
|
}
|
|
2976
|
-
} else if (op.op === "delete") {
|
|
2977
|
-
this._engine.execSqlSync(
|
|
2978
|
-
"DELETE FROM records WHERE _id = ? AND _type = ?",
|
|
2979
|
-
[op.id, op.modelName]
|
|
2980
|
-
);
|
|
2981
|
-
this._engine.execSqlSync(
|
|
2982
|
-
"DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
|
|
2983
|
-
[op.id, op.modelName]
|
|
2984
|
-
);
|
|
2985
|
-
results.push({ success: true, id: op.id });
|
|
2986
|
-
} else if (op.op === "increment") {
|
|
2987
|
-
const newValues = this._engine.incrementFieldsRaw(
|
|
2988
|
-
op.modelName,
|
|
2989
|
-
op.id,
|
|
2990
|
-
op.fields
|
|
2991
|
-
);
|
|
2992
|
-
if (!newValues) {
|
|
2993
|
-
results.push({
|
|
2994
|
-
success: false,
|
|
2995
|
-
id: op.id,
|
|
2996
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
2997
|
-
});
|
|
2998
|
-
} else {
|
|
2999
|
-
results.push({ success: true, id: op.id, values: newValues });
|
|
3000
|
-
}
|
|
3001
|
-
} else if (op.op === "addToSet") {
|
|
3002
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3003
|
-
results.push({
|
|
3004
|
-
success: false,
|
|
3005
|
-
id: op.id,
|
|
3006
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
3007
|
-
});
|
|
3008
|
-
continue;
|
|
3009
|
-
}
|
|
3010
|
-
this._engine.addToStringSetsRaw(
|
|
3011
|
-
op.modelName,
|
|
3012
|
-
op.id,
|
|
3013
|
-
op.stringSets
|
|
3014
|
-
);
|
|
3015
|
-
results.push({ success: true, id: op.id });
|
|
3016
|
-
} else if (op.op === "removeFromSet") {
|
|
3017
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3018
|
-
results.push({
|
|
3019
|
-
success: false,
|
|
3020
|
-
id: op.id,
|
|
3021
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
3022
|
-
});
|
|
3023
|
-
continue;
|
|
3024
|
-
}
|
|
3025
|
-
this._engine.removeFromStringSetsRaw(
|
|
3026
|
-
op.modelName,
|
|
3027
|
-
op.id,
|
|
3028
|
-
op.stringSets
|
|
3029
|
-
);
|
|
3030
|
-
results.push({ success: true, id: op.id });
|
|
3031
3803
|
}
|
|
3804
|
+
});
|
|
3805
|
+
} catch (err) {
|
|
3806
|
+
if (err instanceof AtomicBatchAbort) {
|
|
3807
|
+
const failed = results[err.failedIndex];
|
|
3808
|
+
const failedOp = operations[err.failedIndex];
|
|
3809
|
+
console.warn(
|
|
3810
|
+
`[js-bao batch] atomic rollback: operation ${err.failedIndex} (${failedOp?.op}) failed \u2014 ${failed?.error ?? "unknown error"}`
|
|
3811
|
+
);
|
|
3812
|
+
const errorBody = {
|
|
3813
|
+
error: `Operation ${err.failedIndex} (${failedOp?.op}) failed: ${failed?.error ?? "unknown error"}`,
|
|
3814
|
+
failedIndex: err.failedIndex,
|
|
3815
|
+
op: failedOp?.op,
|
|
3816
|
+
code: failed?.code
|
|
3817
|
+
};
|
|
3818
|
+
return Response.json(errorBody, { status: failed?.status ?? 409 });
|
|
3032
3819
|
}
|
|
3033
|
-
|
|
3820
|
+
throw err;
|
|
3821
|
+
}
|
|
3034
3822
|
for (const entry of fieldsToTrack) {
|
|
3035
3823
|
this._engine.trackModelFields(entry.modelName, entry.data);
|
|
3036
3824
|
}
|
|
@@ -3400,6 +4188,18 @@ function createDatabaseDO(config = {}) {
|
|
|
3400
4188
|
}
|
|
3401
4189
|
return facetResult;
|
|
3402
4190
|
}
|
|
4191
|
+
if (aggOptions.groupBy.length === 0) {
|
|
4192
|
+
const row = results[0];
|
|
4193
|
+
const flat = {};
|
|
4194
|
+
for (const op of aggOptions.operations) {
|
|
4195
|
+
if (op.type === "count") {
|
|
4196
|
+
flat.count = row.count;
|
|
4197
|
+
} else {
|
|
4198
|
+
flat[`${op.type}_${op.field}`] = row[`${op.type}_${op.field}`];
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
return flat;
|
|
4202
|
+
}
|
|
3403
4203
|
const nestedResult = {};
|
|
3404
4204
|
const aliasLookup = /* @__PURE__ */ new Map();
|
|
3405
4205
|
for (const entry of aliasMap) {
|
|
@@ -3727,16 +4527,7 @@ function createDatabaseDO(config = {}) {
|
|
|
3727
4527
|
}
|
|
3728
4528
|
/** @internal */
|
|
3729
4529
|
_parseRow(row) {
|
|
3730
|
-
|
|
3731
|
-
let parsed = {};
|
|
3732
|
-
if (_data) {
|
|
3733
|
-
try {
|
|
3734
|
-
parsed = JSON.parse(_data);
|
|
3735
|
-
} catch (e) {
|
|
3736
|
-
console.warn("Failed to parse _data:", e);
|
|
3737
|
-
}
|
|
3738
|
-
}
|
|
3739
|
-
return { id: _id, type: _type, ...parsed, ...rest };
|
|
4530
|
+
return parseRecordRow(row);
|
|
3740
4531
|
}
|
|
3741
4532
|
// ─── Include (Related Data Loading) ──────────────────────────────
|
|
3742
4533
|
/** @internal — Validate an IncludeSpec, returning an error message or null */
|
|
@@ -3866,8 +4657,9 @@ function createDatabaseDO(config = {}) {
|
|
|
3866
4657
|
spec.filter,
|
|
3867
4658
|
{ projection: spec.projection }
|
|
3868
4659
|
);
|
|
4660
|
+
const relatedProjected = spec.projection ? related.map((r) => this._applyProjection(r, spec.projection)) : related;
|
|
3869
4661
|
const lookupMap = /* @__PURE__ */ new Map();
|
|
3870
|
-
for (const r of
|
|
4662
|
+
for (const r of relatedProjected) {
|
|
3871
4663
|
lookupMap.set(r.id, r);
|
|
3872
4664
|
}
|
|
3873
4665
|
for (const rec of records) {
|
|
@@ -3900,8 +4692,9 @@ function createDatabaseDO(config = {}) {
|
|
|
3900
4692
|
spec.filter,
|
|
3901
4693
|
{ projection: spec.projection }
|
|
3902
4694
|
);
|
|
4695
|
+
const targetsProjected = spec.projection ? targets.map((t) => this._applyProjection(t, spec.projection)) : targets;
|
|
3903
4696
|
const targetLookup = /* @__PURE__ */ new Map();
|
|
3904
|
-
for (const t of
|
|
4697
|
+
for (const t of targetsProjected) {
|
|
3905
4698
|
targetLookup.set(t.id, t);
|
|
3906
4699
|
}
|
|
3907
4700
|
for (const rec of records) {
|
|
@@ -4045,6 +4838,14 @@ function createDatabaseDO(config = {}) {
|
|
|
4045
4838
|
return allResults;
|
|
4046
4839
|
}
|
|
4047
4840
|
/** @internal — Apply projection to a single record */
|
|
4841
|
+
// Apply a projection to a single record. Assumes the projection has
|
|
4842
|
+
// already been validated upstream (JsonQueryTranslator.validateProjection /
|
|
4843
|
+
// buildSelectClause reject mixed inclusion+exclusion): `isInclusion` treats
|
|
4844
|
+
// "any value === 1" as inclusion mode, so an unvalidated mixed projection
|
|
4845
|
+
// would silently ignore the excludes. `id` and `type` are system-identity
|
|
4846
|
+
// fields and are always kept in BOTH modes (a record without its `id` is
|
|
4847
|
+
// unusable downstream). `_meta` is NOT forced — an explicit `{ _meta: 0 }`
|
|
4848
|
+
// is honored.
|
|
4048
4849
|
_applyProjection(record, projection) {
|
|
4049
4850
|
const fields = Object.entries(projection);
|
|
4050
4851
|
if (fields.length === 0) return record;
|
|
@@ -4063,7 +4864,7 @@ function createDatabaseDO(config = {}) {
|
|
|
4063
4864
|
} else {
|
|
4064
4865
|
const result = { ...record };
|
|
4065
4866
|
for (const [f, v] of fields) {
|
|
4066
|
-
if (v === 0) delete result[f];
|
|
4867
|
+
if (v === 0 && f !== "id" && f !== "type") delete result[f];
|
|
4067
4868
|
}
|
|
4068
4869
|
return result;
|
|
4069
4870
|
}
|
|
@@ -4154,6 +4955,12 @@ function discoverSchema(yDoc) {
|
|
|
4154
4955
|
}
|
|
4155
4956
|
return { models };
|
|
4156
4957
|
}
|
|
4958
|
+
function discoverDeclaredModel(yDoc, modelName) {
|
|
4959
|
+
const key = `_meta_${modelName}`;
|
|
4960
|
+
if (!yDoc.share.has(key)) return void 0;
|
|
4961
|
+
const map = materializeMap(yDoc, key);
|
|
4962
|
+
return map ? readModelMeta(map) : void 0;
|
|
4963
|
+
}
|
|
4157
4964
|
function discoverModelNames(yDoc) {
|
|
4158
4965
|
const names = [];
|
|
4159
4966
|
for (const key of yDoc.share.keys()) {
|
|
@@ -4341,6 +5148,7 @@ import { parse as parseToml } from "smol-toml";
|
|
|
4341
5148
|
// src/models/schema.ts
|
|
4342
5149
|
init_BaseModel();
|
|
4343
5150
|
init_ModelRegistry();
|
|
5151
|
+
init_uniqueIndex();
|
|
4344
5152
|
function defineModelSchema(input) {
|
|
4345
5153
|
const { name, fields } = input;
|
|
4346
5154
|
const options = {
|
|
@@ -4383,7 +5191,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
|
|
|
4383
5191
|
for (const [fieldName, options] of fields.entries()) {
|
|
4384
5192
|
if (options.unique) {
|
|
4385
5193
|
resolved.push({
|
|
4386
|
-
name:
|
|
5194
|
+
name: fieldUniqueConstraintName(modelName, fieldName),
|
|
4387
5195
|
fields: [fieldName]
|
|
4388
5196
|
});
|
|
4389
5197
|
}
|
|
@@ -4631,24 +5439,302 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
|
4631
5439
|
// src/cloudflare-do.ts
|
|
4632
5440
|
init_metaSync();
|
|
4633
5441
|
init_sql();
|
|
5442
|
+
init_largeDocuments();
|
|
5443
|
+
|
|
5444
|
+
// src/utils/overlaySql.ts
|
|
5445
|
+
init_sql();
|
|
5446
|
+
var DEFAULT_OVERLAY_TABLES = {
|
|
5447
|
+
records: "records",
|
|
5448
|
+
stringSetIndex: "stringset_index"
|
|
5449
|
+
};
|
|
5450
|
+
var statementCache = /* @__PURE__ */ new Map();
|
|
5451
|
+
function statementsFor(tables) {
|
|
5452
|
+
const cacheKey = `${tables.records}\0${tables.stringSetIndex}`;
|
|
5453
|
+
const cached = statementCache.get(cacheKey);
|
|
5454
|
+
if (cached) return cached;
|
|
5455
|
+
assertValidIdentifier(tables.records, "overlay records table");
|
|
5456
|
+
assertValidIdentifier(tables.stringSetIndex, "overlay stringset table");
|
|
5457
|
+
const records = tables.records;
|
|
5458
|
+
const members = tables.stringSetIndex;
|
|
5459
|
+
const statements = {
|
|
5460
|
+
deleteRecord: `DELETE FROM ${records} WHERE _type = ? AND _id = ?`,
|
|
5461
|
+
deleteAllMembers: `DELETE FROM ${members} WHERE _type = ? AND _record_id = ?`,
|
|
5462
|
+
deleteMember: `DELETE FROM ${members} WHERE _type = ? AND _record_id = ? AND field = ? AND value = ?`,
|
|
5463
|
+
insertMember: `INSERT OR IGNORE INTO ${members} (_record_id, _type, field, value) VALUES (?, ?, ?, ?)`,
|
|
5464
|
+
selectMembers: `SELECT value FROM ${members} WHERE _type = ? AND _record_id = ? AND field = ? ORDER BY rowid`,
|
|
5465
|
+
mergeUpsert: `
|
|
5466
|
+
INSERT INTO ${records} (_id, _type, _data)
|
|
5467
|
+
VALUES (?, ?, json_patch('{}', ?))
|
|
5468
|
+
ON CONFLICT(_type, _id) DO UPDATE SET
|
|
5469
|
+
_data = json_patch(${records}._data, ?)`.trim(),
|
|
5470
|
+
replaceUpsert: `
|
|
5471
|
+
INSERT INTO ${records} (_id, _type, _data)
|
|
5472
|
+
VALUES (?, ?, json_patch('{}', ?))
|
|
5473
|
+
ON CONFLICT(_type, _id) DO UPDATE SET
|
|
5474
|
+
_data = json_patch('{}', ?)`.trim()
|
|
5475
|
+
};
|
|
5476
|
+
statementCache.set(cacheKey, statements);
|
|
5477
|
+
return statements;
|
|
5478
|
+
}
|
|
5479
|
+
function projectOverlayEntry(sql, modelName, entry, tables = DEFAULT_OVERLAY_TABLES) {
|
|
5480
|
+
const id = entry.id;
|
|
5481
|
+
const {
|
|
5482
|
+
deleteRecord: DELETE_RECORD,
|
|
5483
|
+
deleteAllMembers: DELETE_ALL_MEMBERS,
|
|
5484
|
+
deleteMember: DELETE_MEMBER,
|
|
5485
|
+
insertMember: INSERT_MEMBER,
|
|
5486
|
+
selectMembers: SELECT_MEMBERS,
|
|
5487
|
+
mergeUpsert: MERGE_UPSERT,
|
|
5488
|
+
replaceUpsert: REPLACE_UPSERT
|
|
5489
|
+
} = statementsFor(tables);
|
|
5490
|
+
if (entry.deleted) {
|
|
5491
|
+
sql.exec(DELETE_RECORD, modelName, id);
|
|
5492
|
+
sql.exec(DELETE_ALL_MEMBERS, modelName, id);
|
|
5493
|
+
return;
|
|
5494
|
+
}
|
|
5495
|
+
const touchedFields = Object.keys(entry.fields);
|
|
5496
|
+
const touchedSets = Object.keys(entry.stringSets);
|
|
5497
|
+
if (!entry.replace && touchedFields.length === 0 && touchedSets.length === 0) {
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5500
|
+
if (entry.replace) {
|
|
5501
|
+
sql.exec(DELETE_ALL_MEMBERS, modelName, id);
|
|
5502
|
+
}
|
|
5503
|
+
const patch = { ...entry.fields, id };
|
|
5504
|
+
for (const [field, members] of Object.entries(entry.stringSets)) {
|
|
5505
|
+
for (const [member, present] of Object.entries(members)) {
|
|
5506
|
+
if (present) sql.exec(INSERT_MEMBER, id, modelName, field, member);
|
|
5507
|
+
else sql.exec(DELETE_MEMBER, modelName, id, field, member);
|
|
5508
|
+
}
|
|
5509
|
+
const rows = Array.from(sql.exec(SELECT_MEMBERS, modelName, id, field));
|
|
5510
|
+
patch[field] = rows.map((row) => row.value);
|
|
5511
|
+
}
|
|
5512
|
+
const patchJson = JSON.stringify(patch);
|
|
5513
|
+
sql.exec(
|
|
5514
|
+
entry.replace ? REPLACE_UPSERT : MERGE_UPSERT,
|
|
5515
|
+
id,
|
|
5516
|
+
modelName,
|
|
5517
|
+
patchJson,
|
|
5518
|
+
patchJson
|
|
5519
|
+
);
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5522
|
+
// src/utils/snapshotChunks.ts
|
|
5523
|
+
var SnapshotChunkIntegrityError = class extends Error {
|
|
5524
|
+
constructor(key, reason) {
|
|
5525
|
+
super(`Snapshot chunk ${key} does not match its manifest entry: ${reason}`);
|
|
5526
|
+
this.key = key;
|
|
5527
|
+
this.name = "SnapshotChunkIntegrityError";
|
|
5528
|
+
}
|
|
5529
|
+
};
|
|
5530
|
+
var SnapshotRowFormatError = class extends Error {
|
|
5531
|
+
constructor(model, id) {
|
|
5532
|
+
super(
|
|
5533
|
+
`Record ${model}/${id} cannot be written to a snapshot chunk: its id or data contains a newline or tab, which the line format reserves`
|
|
5534
|
+
);
|
|
5535
|
+
this.name = "SnapshotRowFormatError";
|
|
5536
|
+
}
|
|
5537
|
+
};
|
|
5538
|
+
function snapshotStringSetFields(manifest, model) {
|
|
5539
|
+
const declared = manifest.schema?.[model]?.stringSetFields;
|
|
5540
|
+
return new Set(Array.isArray(declared) ? declared.map(String) : []);
|
|
5541
|
+
}
|
|
5542
|
+
function snapshotDeclaredSchema(manifest, model) {
|
|
5543
|
+
const declared = manifest.schema?.[model] ?? {};
|
|
5544
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
5545
|
+
const declaration = declared.declaration && typeof declared.declaration === "object" ? {
|
|
5546
|
+
fields: declared.declaration.fields ?? {},
|
|
5547
|
+
constraints: declared.declaration.constraints ?? {}
|
|
5548
|
+
} : void 0;
|
|
5549
|
+
return {
|
|
5550
|
+
...declaration ? { declaration } : {},
|
|
5551
|
+
stringSetFields: list(declared.stringSetFields).map(String),
|
|
5552
|
+
uniqueConstraints: list(declared.uniqueConstraints).filter(
|
|
5553
|
+
(constraint) => constraint?.name && Array.isArray(constraint.fields) && constraint.fields.length > 0
|
|
5554
|
+
).map((constraint) => ({
|
|
5555
|
+
name: String(constraint.name),
|
|
5556
|
+
fields: constraint.fields.map(String)
|
|
5557
|
+
})),
|
|
5558
|
+
indexedFields: list(declared.indexedFields).filter((index) => index?.field).map((index) => ({
|
|
5559
|
+
field: String(index.field),
|
|
5560
|
+
type: String(index.type || "string"),
|
|
5561
|
+
unique: !!index.unique
|
|
5562
|
+
}))
|
|
5563
|
+
};
|
|
5564
|
+
}
|
|
5565
|
+
function snapshotRowAsOverlayEntry(id, data, stringSetFields) {
|
|
5566
|
+
const fields = {};
|
|
5567
|
+
const stringSets = {};
|
|
5568
|
+
for (const [field, value] of Object.entries(data)) {
|
|
5569
|
+
if (field === "id") continue;
|
|
5570
|
+
if (stringSetFields.has(field) && Array.isArray(value)) {
|
|
5571
|
+
const members = {};
|
|
5572
|
+
for (const member of value) members[String(member)] = true;
|
|
5573
|
+
stringSets[field] = members;
|
|
5574
|
+
continue;
|
|
5575
|
+
}
|
|
5576
|
+
fields[field] = value;
|
|
5577
|
+
}
|
|
5578
|
+
return { id, fields, stringSets, replace: true, deleted: false };
|
|
5579
|
+
}
|
|
5580
|
+
var encoder = new TextEncoder();
|
|
5581
|
+
var decoder = new TextDecoder();
|
|
5582
|
+
function assertLineSafe(row) {
|
|
5583
|
+
if (/[\n\r\t]/.test(row._id) || /[\n\r\t]/.test(row._data)) {
|
|
5584
|
+
throw new SnapshotRowFormatError(row._type, row._id);
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
function snapshotChunkLines(rows) {
|
|
5588
|
+
let body = "";
|
|
5589
|
+
for (const row of rows) {
|
|
5590
|
+
assertLineSafe(row);
|
|
5591
|
+
body += `${row._id} ${row._data}
|
|
5592
|
+
`;
|
|
5593
|
+
}
|
|
5594
|
+
return body;
|
|
5595
|
+
}
|
|
5596
|
+
async function gzip(bytes) {
|
|
5597
|
+
return await collect(
|
|
5598
|
+
new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"))
|
|
5599
|
+
);
|
|
5600
|
+
}
|
|
5601
|
+
async function gunzip(bytes) {
|
|
5602
|
+
return await collect(
|
|
5603
|
+
new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"))
|
|
5604
|
+
);
|
|
5605
|
+
}
|
|
5606
|
+
async function collect(stream) {
|
|
5607
|
+
const parts = [];
|
|
5608
|
+
let total = 0;
|
|
5609
|
+
const reader = stream.getReader();
|
|
5610
|
+
for (; ; ) {
|
|
5611
|
+
const { done, value } = await reader.read();
|
|
5612
|
+
if (done) break;
|
|
5613
|
+
parts.push(value);
|
|
5614
|
+
total += value.byteLength;
|
|
5615
|
+
}
|
|
5616
|
+
const out = new Uint8Array(total);
|
|
5617
|
+
let at = 0;
|
|
5618
|
+
for (const part of parts) {
|
|
5619
|
+
out.set(part, at);
|
|
5620
|
+
at += part.byteLength;
|
|
5621
|
+
}
|
|
5622
|
+
return out;
|
|
5623
|
+
}
|
|
5624
|
+
async function sha256Hex(bytes) {
|
|
5625
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
5626
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5627
|
+
}
|
|
5628
|
+
async function encodeSnapshotChunk(input) {
|
|
5629
|
+
const lines = encoder.encode(snapshotChunkLines(input.rows));
|
|
5630
|
+
const body = await gzip(lines);
|
|
5631
|
+
return {
|
|
5632
|
+
body,
|
|
5633
|
+
entry: {
|
|
5634
|
+
key: input.key,
|
|
5635
|
+
model: input.model,
|
|
5636
|
+
bytes: body.byteLength,
|
|
5637
|
+
rawBytes: lines.byteLength,
|
|
5638
|
+
rows: input.rows.length,
|
|
5639
|
+
firstId: input.rows.length > 0 ? input.rows[0]._id : "",
|
|
5640
|
+
lastId: input.rows.length > 0 ? input.rows[input.rows.length - 1]._id : "",
|
|
5641
|
+
sha256: await sha256Hex(body)
|
|
5642
|
+
}
|
|
5643
|
+
};
|
|
5644
|
+
}
|
|
5645
|
+
async function readSnapshotChunk(body, descriptor) {
|
|
5646
|
+
const entry = "sha256" in descriptor ? descriptor : null;
|
|
5647
|
+
if (entry) {
|
|
5648
|
+
if (body.byteLength !== entry.bytes) {
|
|
5649
|
+
throw new SnapshotChunkIntegrityError(
|
|
5650
|
+
entry.key,
|
|
5651
|
+
`expected ${entry.bytes} bytes, got ${body.byteLength}`
|
|
5652
|
+
);
|
|
5653
|
+
}
|
|
5654
|
+
const digest = await sha256Hex(body);
|
|
5655
|
+
if (digest !== entry.sha256) {
|
|
5656
|
+
throw new SnapshotChunkIntegrityError(
|
|
5657
|
+
entry.key,
|
|
5658
|
+
`expected sha256 ${entry.sha256}, got ${digest}`
|
|
5659
|
+
);
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
const text = decoder.decode(await gunzip(body));
|
|
5663
|
+
const rows = [];
|
|
5664
|
+
for (const line of text.split("\n")) {
|
|
5665
|
+
if (line.length === 0) continue;
|
|
5666
|
+
const tab = line.indexOf(" ");
|
|
5667
|
+
if (tab < 0) {
|
|
5668
|
+
throw new SnapshotChunkIntegrityError(
|
|
5669
|
+
entry?.key ?? descriptor.model,
|
|
5670
|
+
"a line carries no id/data separator"
|
|
5671
|
+
);
|
|
5672
|
+
}
|
|
5673
|
+
rows.push({
|
|
5674
|
+
_type: descriptor.model,
|
|
5675
|
+
_id: line.slice(0, tab),
|
|
5676
|
+
_data: line.slice(tab + 1)
|
|
5677
|
+
});
|
|
5678
|
+
}
|
|
5679
|
+
if (entry && rows.length !== entry.rows) {
|
|
5680
|
+
throw new SnapshotChunkIntegrityError(
|
|
5681
|
+
entry.key,
|
|
5682
|
+
`expected ${entry.rows} rows, got ${rows.length}`
|
|
5683
|
+
);
|
|
5684
|
+
}
|
|
5685
|
+
return rows;
|
|
5686
|
+
}
|
|
4634
5687
|
export {
|
|
5688
|
+
DEFAULT_OVERLAY_TABLES,
|
|
4635
5689
|
DurableObjectEngine,
|
|
5690
|
+
Format2CompositeFieldError,
|
|
4636
5691
|
JsonQueryTranslator,
|
|
4637
5692
|
JsonSchemaDDL,
|
|
5693
|
+
OVERLAY_KEY_SEPARATOR,
|
|
5694
|
+
OVERLAY_MARKER_DELETED,
|
|
5695
|
+
OVERLAY_MARKER_REPLACE,
|
|
5696
|
+
PROJECTED_JSON_ALIAS_PREFIX,
|
|
5697
|
+
SnapshotChunkIntegrityError,
|
|
5698
|
+
SnapshotRowFormatError,
|
|
5699
|
+
applyOverlayMutation,
|
|
5700
|
+
assertNoFormat2CompositeFields,
|
|
4638
5701
|
buildSafeAlias,
|
|
4639
5702
|
checkAutoStampsReservedFields,
|
|
4640
5703
|
checkReservedFieldName,
|
|
4641
5704
|
clearMetaSyncCache,
|
|
5705
|
+
completeOverlayEntries,
|
|
4642
5706
|
createDatabaseDO,
|
|
4643
5707
|
createDocumentDO,
|
|
5708
|
+
decodeOverlaySegment,
|
|
4644
5709
|
deterministicIdentifierHash,
|
|
5710
|
+
discoverDeclaredModel,
|
|
4645
5711
|
discoverModelNames,
|
|
4646
5712
|
discoverSchema,
|
|
5713
|
+
encodeOverlayMutation,
|
|
5714
|
+
encodeOverlaySegment,
|
|
5715
|
+
encodeSnapshotChunk,
|
|
5716
|
+
format2StringSetMembers,
|
|
5717
|
+
groupOverlayEntries,
|
|
4647
5718
|
handleRequest,
|
|
4648
5719
|
inferFieldType,
|
|
5720
|
+
isFormat2StringSetYMap,
|
|
4649
5721
|
loadSchemaFromTomlString,
|
|
5722
|
+
materializeOverlayRecord,
|
|
5723
|
+
overlayFieldKey,
|
|
5724
|
+
overlayMarkerKey,
|
|
5725
|
+
overlayMemberKey,
|
|
5726
|
+
parseOverlayKey,
|
|
5727
|
+
parseRecordRow,
|
|
5728
|
+
projectOverlayEntry,
|
|
5729
|
+
projectedJsonAlias,
|
|
5730
|
+
readOverlayRecord,
|
|
5731
|
+
readSnapshotChunk,
|
|
4650
5732
|
registerFunctionDefault,
|
|
4651
5733
|
schemaToToml,
|
|
5734
|
+
snapshotChunkLines,
|
|
5735
|
+
snapshotDeclaredSchema,
|
|
5736
|
+
snapshotRowAsOverlayEntry,
|
|
5737
|
+
snapshotStringSetFields,
|
|
4652
5738
|
syncInferredMeta,
|
|
4653
5739
|
syncModelMeta
|
|
4654
5740
|
};
|