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.cjs
CHANGED
|
@@ -256,21 +256,36 @@ var init_CursorManager = __esm({
|
|
|
256
256
|
return fields;
|
|
257
257
|
}
|
|
258
258
|
/**
|
|
259
|
-
* Build ORDER BY clause from sort specification
|
|
259
|
+
* Build ORDER BY clause from sort specification.
|
|
260
|
+
*
|
|
261
|
+
* `direction` is the pagination direction (1 = forward, -1 = backward).
|
|
262
|
+
* For backward pagination each field's effective ORDER BY direction is
|
|
263
|
+
* reversed so the cursor's flipped inequality (built by
|
|
264
|
+
* `buildPaginationConditions`) returns the page adjacent to the cursor,
|
|
265
|
+
* not the globally smallest/largest window. Without this,
|
|
266
|
+
* `field < cursor ... ORDER BY field ASC LIMIT n` returns the n smallest
|
|
267
|
+
* rows overall instead of the n rows just below the cursor.
|
|
268
|
+
*
|
|
269
|
+
* The returned `directions` array carries the *declared* per-field sort
|
|
270
|
+
* directions (unaffected by `direction`) — `buildPaginationConditions`
|
|
271
|
+
* needs the declared directions to pick each field's comparison operator.
|
|
260
272
|
*/
|
|
261
|
-
static buildOrderClause(sort, fieldFormatter = (field) => field) {
|
|
273
|
+
static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
|
|
262
274
|
const fields = this.extractSortFields(sort);
|
|
263
275
|
const clauses = [];
|
|
264
276
|
const directions = [];
|
|
277
|
+
const sqlDir = (declared) => {
|
|
278
|
+
const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
|
|
279
|
+
return effective === 1 ? "ASC" : "DESC";
|
|
280
|
+
};
|
|
265
281
|
if (sort && Object.keys(sort).length > 0) {
|
|
266
282
|
for (const [field, dir] of Object.entries(sort)) {
|
|
267
|
-
|
|
268
|
-
clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
|
|
283
|
+
clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
|
|
269
284
|
directions.push(dir);
|
|
270
285
|
}
|
|
271
286
|
}
|
|
272
287
|
if (!sort || !sort.hasOwnProperty("id")) {
|
|
273
|
-
clauses.push(`${fieldFormatter("id")}
|
|
288
|
+
clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
|
|
274
289
|
directions.push(1);
|
|
275
290
|
} else if (sort && sort.hasOwnProperty("id")) {
|
|
276
291
|
directions.push(sort["id"]);
|
|
@@ -293,18 +308,27 @@ var init_CursorManager = __esm({
|
|
|
293
308
|
/**
|
|
294
309
|
* Generate next and previous cursors from result set
|
|
295
310
|
*/
|
|
296
|
-
static generateResultCursors(results, sortFields,
|
|
311
|
+
static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
|
|
297
312
|
if (results.length === 0) {
|
|
298
313
|
return {};
|
|
299
314
|
}
|
|
300
315
|
const cursors = {};
|
|
301
316
|
if (hasMore && results.length > 0) {
|
|
302
317
|
const lastResult = results[results.length - 1];
|
|
303
|
-
cursors.nextCursor = this.generateCursor(
|
|
318
|
+
cursors.nextCursor = this.generateCursor(
|
|
319
|
+
lastResult,
|
|
320
|
+
sortFields,
|
|
321
|
+
requestedDirection
|
|
322
|
+
);
|
|
304
323
|
}
|
|
305
324
|
if (results.length > 0 && !isFirstPage) {
|
|
306
325
|
const firstResult = results[0];
|
|
307
|
-
|
|
326
|
+
const opposite = requestedDirection === 1 ? -1 : 1;
|
|
327
|
+
cursors.prevCursor = this.generateCursor(
|
|
328
|
+
firstResult,
|
|
329
|
+
sortFields,
|
|
330
|
+
opposite
|
|
331
|
+
);
|
|
308
332
|
}
|
|
309
333
|
return cursors;
|
|
310
334
|
}
|
|
@@ -355,6 +379,9 @@ var init_patterns = __esm({
|
|
|
355
379
|
});
|
|
356
380
|
|
|
357
381
|
// src/utils/yjsValues.ts
|
|
382
|
+
function isYjsComposite(value) {
|
|
383
|
+
return value instanceof Y.Map || value instanceof Y.Array || value instanceof Y.Text;
|
|
384
|
+
}
|
|
358
385
|
function isStringSetYMap(value) {
|
|
359
386
|
if (!(value instanceof Y.Map)) return false;
|
|
360
387
|
for (const v of value.values()) {
|
|
@@ -395,6 +422,31 @@ var init_DocumentQueryTranslator = __esm({
|
|
|
395
422
|
}
|
|
396
423
|
});
|
|
397
424
|
|
|
425
|
+
// src/models/writeLifecycle.ts
|
|
426
|
+
function registerFunctionDefaultGenerator(name, fn) {
|
|
427
|
+
FUNCTION_DEFAULT_GENERATORS.set(name, fn);
|
|
428
|
+
}
|
|
429
|
+
var Y3, import_ulid2, KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
|
|
430
|
+
var init_writeLifecycle = __esm({
|
|
431
|
+
"src/models/writeLifecycle.ts"() {
|
|
432
|
+
"use strict";
|
|
433
|
+
Y3 = __toESM(require("yjs"), 1);
|
|
434
|
+
import_ulid2 = require("ulid");
|
|
435
|
+
KNOWN_FIELD_TYPE_MAP = {
|
|
436
|
+
string: true,
|
|
437
|
+
number: true,
|
|
438
|
+
boolean: true,
|
|
439
|
+
date: true,
|
|
440
|
+
id: true,
|
|
441
|
+
stringset: true
|
|
442
|
+
};
|
|
443
|
+
KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
|
|
444
|
+
FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
|
|
445
|
+
registerFunctionDefaultGenerator("generate_ulid", () => (0, import_ulid2.ulid)());
|
|
446
|
+
UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
398
450
|
// src/models/metaSync.ts
|
|
399
451
|
function inferFieldType(value) {
|
|
400
452
|
if (isStringSetYMap(value)) return "stringset";
|
|
@@ -416,7 +468,10 @@ function encodeDefault(value) {
|
|
|
416
468
|
if (value === void 0 || value === null) return void 0;
|
|
417
469
|
if (typeof value === "function") {
|
|
418
470
|
const name = KNOWN_FUNCTION_DEFAULTS.get(value);
|
|
419
|
-
return name ? `$${name}` :
|
|
471
|
+
return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
|
|
472
|
+
}
|
|
473
|
+
if (typeof value === "string" && value.startsWith("$")) {
|
|
474
|
+
return `$${value}`;
|
|
420
475
|
}
|
|
421
476
|
return value;
|
|
422
477
|
}
|
|
@@ -442,7 +497,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
442
497
|
if (compoundConstraints.length > 0) {
|
|
443
498
|
let constraints = meta.get("_constraints");
|
|
444
499
|
if (!constraints) {
|
|
445
|
-
constraints = new
|
|
500
|
+
constraints = new Y4.Map();
|
|
446
501
|
meta.set("_constraints", constraints);
|
|
447
502
|
}
|
|
448
503
|
for (const constraint of compoundConstraints) {
|
|
@@ -453,7 +508,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
453
508
|
if (relationships && Object.keys(relationships).length > 0) {
|
|
454
509
|
let rels = meta.get("_relationships");
|
|
455
510
|
if (!rels) {
|
|
456
|
-
rels = new
|
|
511
|
+
rels = new Y4.Map();
|
|
457
512
|
meta.set("_relationships", rels);
|
|
458
513
|
}
|
|
459
514
|
for (const [relName, relConfig] of Object.entries(relationships)) {
|
|
@@ -465,7 +520,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
465
520
|
function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
466
521
|
let fieldMeta = metaMap.get(fieldName);
|
|
467
522
|
if (!fieldMeta) {
|
|
468
|
-
fieldMeta = new
|
|
523
|
+
fieldMeta = new Y4.Map();
|
|
469
524
|
metaMap.set(fieldName, fieldMeta);
|
|
470
525
|
}
|
|
471
526
|
setIfChanged(fieldMeta, "type", fieldOpts.type);
|
|
@@ -482,7 +537,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
482
537
|
function syncConstraintMeta(constraintsMap, constraint) {
|
|
483
538
|
let cMeta = constraintsMap.get(constraint.name);
|
|
484
539
|
if (!cMeta) {
|
|
485
|
-
cMeta = new
|
|
540
|
+
cMeta = new Y4.Map();
|
|
486
541
|
constraintsMap.set(constraint.name, cMeta);
|
|
487
542
|
}
|
|
488
543
|
setIfChanged(cMeta, "type", "unique");
|
|
@@ -492,7 +547,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
|
|
|
492
547
|
function syncRelationshipMeta(relsMap, relName, relConfig) {
|
|
493
548
|
let relMeta = relsMap.get(relName);
|
|
494
549
|
if (!relMeta) {
|
|
495
|
-
relMeta = new
|
|
550
|
+
relMeta = new Y4.Map();
|
|
496
551
|
relsMap.set(relName, relMeta);
|
|
497
552
|
}
|
|
498
553
|
for (const [key, value] of Object.entries(relConfig)) {
|
|
@@ -507,7 +562,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
|
|
|
507
562
|
if (fieldName.startsWith("_")) continue;
|
|
508
563
|
let fieldMeta = meta.get(fieldName);
|
|
509
564
|
if (!fieldMeta) {
|
|
510
|
-
fieldMeta = new
|
|
565
|
+
fieldMeta = new Y4.Map();
|
|
511
566
|
meta.set(fieldName, fieldMeta);
|
|
512
567
|
}
|
|
513
568
|
if (!fieldMeta.has("type")) {
|
|
@@ -523,34 +578,565 @@ function setIfChanged(map, key, value) {
|
|
|
523
578
|
map.set(key, value);
|
|
524
579
|
}
|
|
525
580
|
}
|
|
526
|
-
var
|
|
581
|
+
var Y4, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
|
|
527
582
|
var init_metaSync = __esm({
|
|
528
583
|
"src/models/metaSync.ts"() {
|
|
529
584
|
"use strict";
|
|
530
|
-
|
|
585
|
+
Y4 = __toESM(require("yjs"), 1);
|
|
531
586
|
init_yjsValues();
|
|
587
|
+
init_writeLifecycle();
|
|
532
588
|
KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
|
|
533
589
|
_syncedCache = /* @__PURE__ */ new WeakMap();
|
|
534
590
|
}
|
|
535
591
|
});
|
|
536
592
|
|
|
593
|
+
// src/utils/uniqueIndex.ts
|
|
594
|
+
function fieldUniqueConstraintName(modelName, fieldName) {
|
|
595
|
+
return `${modelName}_${fieldName}_unique`;
|
|
596
|
+
}
|
|
597
|
+
var Y5;
|
|
598
|
+
var init_uniqueIndex = __esm({
|
|
599
|
+
"src/utils/uniqueIndex.ts"() {
|
|
600
|
+
"use strict";
|
|
601
|
+
Y5 = __toESM(require("yjs"), 1);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// src/utils/largeDocuments.ts
|
|
606
|
+
function encodeOverlaySegment(segment) {
|
|
607
|
+
const escaped = segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
608
|
+
return escaped.startsWith("_") ? `~2${escaped.slice(1)}` : escaped;
|
|
609
|
+
}
|
|
610
|
+
function decodeOverlaySegment(segment) {
|
|
611
|
+
const withUnderscore = segment.startsWith("~2") ? `_${segment.slice(2)}` : segment;
|
|
612
|
+
return withUnderscore.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
613
|
+
}
|
|
614
|
+
function overlayFieldKey(recordId, field) {
|
|
615
|
+
return `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}${encodeOverlaySegment(field)}`;
|
|
616
|
+
}
|
|
617
|
+
function overlayMemberKey(recordId, field, member) {
|
|
618
|
+
return `${overlayFieldKey(recordId, field)}${OVERLAY_KEY_SEPARATOR}${encodeOverlaySegment(member)}`;
|
|
619
|
+
}
|
|
620
|
+
function overlayMarkerKey(recordId, marker) {
|
|
621
|
+
return `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}${marker}`;
|
|
622
|
+
}
|
|
623
|
+
function parseOverlayKey(key) {
|
|
624
|
+
const parts = key.split(OVERLAY_KEY_SEPARATOR);
|
|
625
|
+
if (parts.length === 2) {
|
|
626
|
+
const [rawId, rawSecond] = parts;
|
|
627
|
+
const recordId = decodeOverlaySegment(rawId);
|
|
628
|
+
if (OVERLAY_MARKERS.has(rawSecond)) return { recordId, marker: rawSecond };
|
|
629
|
+
return { recordId, field: decodeOverlaySegment(rawSecond) };
|
|
630
|
+
}
|
|
631
|
+
if (parts.length === 3) {
|
|
632
|
+
return {
|
|
633
|
+
recordId: decodeOverlaySegment(parts[0]),
|
|
634
|
+
field: decodeOverlaySegment(parts[1]),
|
|
635
|
+
member: decodeOverlaySegment(parts[2])
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
function emptyEntry(id) {
|
|
641
|
+
return { id, fields: {}, stringSets: {}, replace: false, deleted: false };
|
|
642
|
+
}
|
|
643
|
+
function groupOverlayEntries(entries) {
|
|
644
|
+
const out = /* @__PURE__ */ new Map();
|
|
645
|
+
for (const [key, value] of entries) {
|
|
646
|
+
const parsed = parseOverlayKey(key);
|
|
647
|
+
if (!parsed) continue;
|
|
648
|
+
let entry = out.get(parsed.recordId);
|
|
649
|
+
if (!entry) {
|
|
650
|
+
entry = emptyEntry(parsed.recordId);
|
|
651
|
+
out.set(parsed.recordId, entry);
|
|
652
|
+
}
|
|
653
|
+
if (parsed.marker === OVERLAY_MARKER_REPLACE) {
|
|
654
|
+
entry.replace = value === true;
|
|
655
|
+
} else if (parsed.marker === OVERLAY_MARKER_DELETED) {
|
|
656
|
+
entry.deleted = value === true;
|
|
657
|
+
} else if (parsed.member !== void 0) {
|
|
658
|
+
const field = parsed.field;
|
|
659
|
+
(entry.stringSets[field] ||= {})[parsed.member] = value === true;
|
|
660
|
+
} else if (parsed.field !== void 0) {
|
|
661
|
+
entry.fields[parsed.field] = value;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
function readOverlayRecord(modelMap, recordId) {
|
|
667
|
+
return collectOverlayRecords(modelMap, /* @__PURE__ */ new Set([recordId])).get(recordId);
|
|
668
|
+
}
|
|
669
|
+
function completeOverlayEntries(modelMap, entries) {
|
|
670
|
+
const rebuild = /* @__PURE__ */ new Set();
|
|
671
|
+
for (const [id, entry] of entries) {
|
|
672
|
+
if (!entry.replace || entry.deleted) continue;
|
|
673
|
+
rebuild.add(id);
|
|
674
|
+
}
|
|
675
|
+
if (rebuild.size === 0) return entries;
|
|
676
|
+
for (const [id, entry] of collectOverlayRecords(modelMap, rebuild)) {
|
|
677
|
+
entries.set(id, entry);
|
|
678
|
+
}
|
|
679
|
+
return entries;
|
|
680
|
+
}
|
|
681
|
+
function collectOverlayRecords(modelMap, recordIds) {
|
|
682
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
683
|
+
for (const id of recordIds) wanted.set(encodeOverlaySegment(id), id);
|
|
684
|
+
const own = /* @__PURE__ */ new Map();
|
|
685
|
+
for (const [key, value] of modelMap.entries()) {
|
|
686
|
+
if (typeof key !== "string") continue;
|
|
687
|
+
const separator = key.indexOf(OVERLAY_KEY_SEPARATOR);
|
|
688
|
+
if (separator <= 0) continue;
|
|
689
|
+
const id = wanted.get(key.slice(0, separator));
|
|
690
|
+
if (id === void 0) continue;
|
|
691
|
+
if (!parseOverlayKey(key)) continue;
|
|
692
|
+
const keys = own.get(id);
|
|
693
|
+
if (keys) keys.push([key, value]);
|
|
694
|
+
else own.set(id, [[key, value]]);
|
|
695
|
+
}
|
|
696
|
+
const out = /* @__PURE__ */ new Map();
|
|
697
|
+
for (const id of recordIds) {
|
|
698
|
+
out.set(id, groupOverlayEntries(own.get(id) ?? []).get(id) ?? emptyEntry(id));
|
|
699
|
+
}
|
|
700
|
+
return out;
|
|
701
|
+
}
|
|
702
|
+
function baseMembers(value) {
|
|
703
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
704
|
+
if (value && typeof value === "object") {
|
|
705
|
+
return Object.entries(value).filter(([, present]) => Boolean(present)).map(([member]) => member);
|
|
706
|
+
}
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
function materializeOverlayRecord(base, overlay) {
|
|
710
|
+
if (overlay.deleted) return null;
|
|
711
|
+
const out = overlay.replace ? {} : { ...base ?? {} };
|
|
712
|
+
for (const [field, value] of Object.entries(overlay.fields)) {
|
|
713
|
+
if (value === null) delete out[field];
|
|
714
|
+
else out[field] = value;
|
|
715
|
+
}
|
|
716
|
+
for (const [field, members] of Object.entries(overlay.stringSets)) {
|
|
717
|
+
const current = overlay.replace ? [] : baseMembers(out[field]);
|
|
718
|
+
const next = current.slice();
|
|
719
|
+
for (const [member, present] of Object.entries(members)) {
|
|
720
|
+
const at = next.indexOf(member);
|
|
721
|
+
if (present && at === -1) next.push(member);
|
|
722
|
+
else if (!present && at !== -1) next.splice(at, 1);
|
|
723
|
+
}
|
|
724
|
+
out[field] = next;
|
|
725
|
+
}
|
|
726
|
+
out.id = overlay.id;
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
function encodeOverlayMutation(mutation) {
|
|
730
|
+
const { id, kind } = mutation;
|
|
731
|
+
const entries = [];
|
|
732
|
+
if (kind === "delete") {
|
|
733
|
+
entries.push([overlayMarkerKey(id, OVERLAY_MARKER_DELETED), true]);
|
|
734
|
+
return entries;
|
|
735
|
+
}
|
|
736
|
+
if (kind === "create") {
|
|
737
|
+
entries.push([overlayMarkerKey(id, OVERLAY_MARKER_REPLACE), true]);
|
|
738
|
+
entries.push([overlayMarkerKey(id, OVERLAY_MARKER_DELETED), false]);
|
|
739
|
+
}
|
|
740
|
+
for (const [field, value] of Object.entries(mutation.fields ?? {})) {
|
|
741
|
+
entries.push([overlayFieldKey(id, field), value === void 0 ? null : value]);
|
|
742
|
+
}
|
|
743
|
+
for (const [field, members] of Object.entries(mutation.stringSetDeltas ?? {})) {
|
|
744
|
+
for (const [member, present] of Object.entries(members)) {
|
|
745
|
+
entries.push([overlayMemberKey(id, field, member), present]);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return entries;
|
|
749
|
+
}
|
|
750
|
+
function applyOverlayMutation(modelMap, mutation) {
|
|
751
|
+
if (mutation.kind === "create") {
|
|
752
|
+
clearStaleRecordOverlay(modelMap, mutation.id);
|
|
753
|
+
}
|
|
754
|
+
for (const [key, value] of encodeOverlayMutation(mutation)) {
|
|
755
|
+
modelMap.set(key, value);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function clearStaleRecordOverlay(modelMap, recordId) {
|
|
759
|
+
if (!modelMap.has(overlayMarkerKey(recordId, OVERLAY_MARKER_DELETED))) return;
|
|
760
|
+
const prefix = `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}`;
|
|
761
|
+
const stale = [];
|
|
762
|
+
for (const key of modelMap.keys()) {
|
|
763
|
+
if (key.startsWith(prefix)) stale.push(key);
|
|
764
|
+
}
|
|
765
|
+
for (const key of stale) modelMap.delete(key);
|
|
766
|
+
}
|
|
767
|
+
function assertNoFormat2CompositeFields(modelName, recordId, fields) {
|
|
768
|
+
if (!fields) return;
|
|
769
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
770
|
+
if (isYjsComposite(value)) {
|
|
771
|
+
throw new Format2CompositeFieldError(modelName, recordId, field);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function isFormat2StringSetYMap(value) {
|
|
776
|
+
if (!(value instanceof Y6.Map)) return false;
|
|
777
|
+
for (const member of value.values()) {
|
|
778
|
+
if (member !== true && member !== false) return false;
|
|
779
|
+
}
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
function format2StringSetMembers(value) {
|
|
783
|
+
if (!(value instanceof Y6.Map)) return [];
|
|
784
|
+
return Array.from(value.entries()).filter(([, present]) => present === true).map(([member]) => member);
|
|
785
|
+
}
|
|
786
|
+
var Y6, OVERLAY_KEY_SEPARATOR, OVERLAY_MARKER_REPLACE, OVERLAY_MARKER_DELETED, OVERLAY_MARKERS, Format2CompositeFieldError;
|
|
787
|
+
var init_largeDocuments = __esm({
|
|
788
|
+
"src/utils/largeDocuments.ts"() {
|
|
789
|
+
"use strict";
|
|
790
|
+
Y6 = __toESM(require("yjs"), 1);
|
|
791
|
+
init_yjsValues();
|
|
792
|
+
OVERLAY_KEY_SEPARATOR = "/";
|
|
793
|
+
OVERLAY_MARKER_REPLACE = "_replace";
|
|
794
|
+
OVERLAY_MARKER_DELETED = "_deleted";
|
|
795
|
+
OVERLAY_MARKERS = /* @__PURE__ */ new Set([
|
|
796
|
+
OVERLAY_MARKER_REPLACE,
|
|
797
|
+
OVERLAY_MARKER_DELETED
|
|
798
|
+
]);
|
|
799
|
+
Format2CompositeFieldError = class extends Error {
|
|
800
|
+
modelName;
|
|
801
|
+
recordId;
|
|
802
|
+
field;
|
|
803
|
+
constructor(modelName, recordId, field) {
|
|
804
|
+
super(
|
|
805
|
+
`Composite field values are not supported on large documents (format 2): ${modelName}.${field} (id=${recordId})`
|
|
806
|
+
);
|
|
807
|
+
this.name = "Format2CompositeFieldError";
|
|
808
|
+
this.modelName = modelName;
|
|
809
|
+
this.recordId = recordId;
|
|
810
|
+
this.field = field;
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// src/models/OrmContext.ts
|
|
817
|
+
var OrmContext, defaultOrmContext, liveOrmContexts;
|
|
818
|
+
var init_OrmContext = __esm({
|
|
819
|
+
"src/models/OrmContext.ts"() {
|
|
820
|
+
"use strict";
|
|
821
|
+
OrmContext = class _OrmContext {
|
|
822
|
+
/** Declared before the field that reads it: static initialization is ordered. */
|
|
823
|
+
static instanceCounter = 0;
|
|
824
|
+
/**
|
|
825
|
+
* Which instance this is, for diagnostics. A process holding several clients
|
|
826
|
+
* needs a way to say WHICH one a warning is about, so every context takes the
|
|
827
|
+
* next number as it is created and keeps it for its lifetime.
|
|
828
|
+
*/
|
|
829
|
+
instanceId = ++_OrmContext.instanceCounter;
|
|
830
|
+
/** The engine this instance's models read and write through. */
|
|
831
|
+
dbEngine = null;
|
|
832
|
+
connectedDocuments = /* @__PURE__ */ new Map();
|
|
833
|
+
/** `docId_modelName` → the Y.Map that document holds for that model. */
|
|
834
|
+
documentYMaps = /* @__PURE__ */ new Map();
|
|
835
|
+
/** `subscribe()` callbacks, by model name. */
|
|
836
|
+
listenersMap = /* @__PURE__ */ new Map();
|
|
837
|
+
// Default-document resolution
|
|
838
|
+
modelNameToDefaultDocId = /* @__PURE__ */ new Map();
|
|
839
|
+
globalDefaultDocId = void 0;
|
|
840
|
+
defaultDocChangedListeners = /* @__PURE__ */ new Set();
|
|
841
|
+
modelDocMappingChangedListeners = /* @__PURE__ */ new Set();
|
|
842
|
+
/** Format-2 ("large document", #2816) record stores by document id. */
|
|
843
|
+
format2Documents = /* @__PURE__ */ new Map();
|
|
844
|
+
/** Format-2 epoch moves in flight (#2816). */
|
|
845
|
+
format2Transitions = /* @__PURE__ */ new Map();
|
|
846
|
+
/**
|
|
847
|
+
* Nested-Y.Map stringset fields this context has already observed. Per
|
|
848
|
+
* context, not global: a Y.Doc reconnected to a fresh instance has to be
|
|
849
|
+
* observed again by that instance, and a global guard would silently
|
|
850
|
+
* suppress it.
|
|
851
|
+
*/
|
|
852
|
+
observedStringSetMaps = /* @__PURE__ */ new WeakSet();
|
|
853
|
+
/**
|
|
854
|
+
* The registry this instance's schema lookups use. `null` on the standing
|
|
855
|
+
* default context, where callers fall back to the global singleton.
|
|
856
|
+
*/
|
|
857
|
+
modelRegistry = null;
|
|
858
|
+
/** Model classes routed to this context. */
|
|
859
|
+
boundModels = /* @__PURE__ */ new Set();
|
|
860
|
+
/** True once {@link destroy} has run; bound classes keep pointing here. */
|
|
861
|
+
destroyed = false;
|
|
862
|
+
/** Yjs `unobserve` thunks by owning document id. */
|
|
863
|
+
observerDisposers = /* @__PURE__ */ new Map();
|
|
864
|
+
/** Teardown the owner (the document manager) registers for `destroy()`. */
|
|
865
|
+
teardownHooks = /* @__PURE__ */ new Set();
|
|
866
|
+
static NO_DOCUMENT_KEY = "__no_document__";
|
|
867
|
+
/**
|
|
868
|
+
* Record how to remove an observer this context installed. Every `observe()`
|
|
869
|
+
* in the ORM pairs with one of these: without it a torn-down instance's
|
|
870
|
+
* callbacks keep firing on a Y.Doc that outlives it.
|
|
871
|
+
*/
|
|
872
|
+
registerObserverDisposer(docId, dispose) {
|
|
873
|
+
const key = docId ?? _OrmContext.NO_DOCUMENT_KEY;
|
|
874
|
+
const list = this.observerDisposers.get(key);
|
|
875
|
+
if (list) list.push(dispose);
|
|
876
|
+
else this.observerDisposers.set(key, [dispose]);
|
|
877
|
+
}
|
|
878
|
+
/** Remove every observer this context installed for `docId`. */
|
|
879
|
+
disposeObserversForDocument(docId) {
|
|
880
|
+
const list = this.observerDisposers.get(docId);
|
|
881
|
+
if (!list) return;
|
|
882
|
+
this.observerDisposers.delete(docId);
|
|
883
|
+
for (const dispose of list) {
|
|
884
|
+
try {
|
|
885
|
+
dispose();
|
|
886
|
+
} catch {
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
disposeAllObservers() {
|
|
891
|
+
const buckets = Array.from(this.observerDisposers.values());
|
|
892
|
+
this.observerDisposers.clear();
|
|
893
|
+
for (const list of buckets) {
|
|
894
|
+
for (const dispose of list) {
|
|
895
|
+
try {
|
|
896
|
+
dispose();
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/** Run `hook` as part of `destroy()` (the format-2 binding registers here). */
|
|
903
|
+
onDestroy(hook) {
|
|
904
|
+
this.teardownHooks.add(hook);
|
|
905
|
+
}
|
|
906
|
+
/** The record store for `docId`, or `null` when the document is format 1. */
|
|
907
|
+
getFormat2Store(docId) {
|
|
908
|
+
if (!docId) return null;
|
|
909
|
+
return this.format2Documents.get(docId) ?? null;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* The epoch move a write to `docId` has to wait out (#2816, behavior 10), or
|
|
913
|
+
* `null` when none is in flight.
|
|
914
|
+
*/
|
|
915
|
+
format2WritesHeld(docId) {
|
|
916
|
+
if (!docId) return null;
|
|
917
|
+
return this.format2Transitions.get(docId) ?? null;
|
|
918
|
+
}
|
|
919
|
+
/** Run an epoch move with `docId`'s local writes held back (#2816). */
|
|
920
|
+
async holdFormat2Writes(docId, run) {
|
|
921
|
+
const inFlight = this.format2Transitions.get(docId);
|
|
922
|
+
if (inFlight) await inFlight.catch(() => {
|
|
923
|
+
});
|
|
924
|
+
let release;
|
|
925
|
+
const held = new Promise((resolve) => {
|
|
926
|
+
release = resolve;
|
|
927
|
+
});
|
|
928
|
+
this.format2Transitions.set(docId, held);
|
|
929
|
+
try {
|
|
930
|
+
return await run();
|
|
931
|
+
} finally {
|
|
932
|
+
if (this.format2Transitions.get(docId) === held) {
|
|
933
|
+
this.format2Transitions.delete(docId);
|
|
934
|
+
}
|
|
935
|
+
release();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
/** Notify `subscribe()` callbacks registered on this context for `modelName`. */
|
|
939
|
+
notifyListeners(modelName, log, onError) {
|
|
940
|
+
const listeners = this.listenersMap.get(modelName);
|
|
941
|
+
if (!listeners) return;
|
|
942
|
+
log?.(`Notifying ${listeners.size} listeners for ${modelName}`);
|
|
943
|
+
listeners.forEach((callback) => {
|
|
944
|
+
try {
|
|
945
|
+
callback();
|
|
946
|
+
} catch (e) {
|
|
947
|
+
onError?.(e);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
// ---- Default-document resolution (the `BaseModel` statics delegate here) ----
|
|
952
|
+
setModelDefaultDocumentId(modelName, docId) {
|
|
953
|
+
const previous = this.modelNameToDefaultDocId.get(modelName);
|
|
954
|
+
this.modelNameToDefaultDocId.set(modelName, docId);
|
|
955
|
+
this.announceModelDocMapping(modelName, previous, docId);
|
|
956
|
+
}
|
|
957
|
+
removeModelDefaultDocumentId(modelName) {
|
|
958
|
+
const previous = this.modelNameToDefaultDocId.get(modelName);
|
|
959
|
+
this.modelNameToDefaultDocId.delete(modelName);
|
|
960
|
+
this.announceModelDocMapping(modelName, previous, void 0);
|
|
961
|
+
}
|
|
962
|
+
clearModelDefaultDocumentIds() {
|
|
963
|
+
const entries = Array.from(this.modelNameToDefaultDocId.entries());
|
|
964
|
+
this.modelNameToDefaultDocId.clear();
|
|
965
|
+
for (const [modelName, previous] of entries) {
|
|
966
|
+
this.announceModelDocMapping(modelName, previous, void 0);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
setGlobalDefaultDocumentId(docId) {
|
|
970
|
+
const previous = this.globalDefaultDocId;
|
|
971
|
+
this.globalDefaultDocId = docId;
|
|
972
|
+
this.announceDefaultDoc(previous, docId);
|
|
973
|
+
}
|
|
974
|
+
clearGlobalDefaultDocumentId() {
|
|
975
|
+
const previous = this.globalDefaultDocId;
|
|
976
|
+
this.globalDefaultDocId = void 0;
|
|
977
|
+
this.announceDefaultDoc(previous, void 0);
|
|
978
|
+
}
|
|
979
|
+
getModelDefaultDocumentMapping() {
|
|
980
|
+
return Object.fromEntries(this.modelNameToDefaultDocId.entries());
|
|
981
|
+
}
|
|
982
|
+
getDocumentIdForModel(modelName) {
|
|
983
|
+
return this.modelNameToDefaultDocId.get(modelName);
|
|
984
|
+
}
|
|
985
|
+
getGlobalDefaultDocumentId() {
|
|
986
|
+
return this.globalDefaultDocId;
|
|
987
|
+
}
|
|
988
|
+
onDefaultDocChanged(listener) {
|
|
989
|
+
this.defaultDocChangedListeners.add(listener);
|
|
990
|
+
return () => this.defaultDocChangedListeners.delete(listener);
|
|
991
|
+
}
|
|
992
|
+
onModelDocMappingChanged(listener) {
|
|
993
|
+
this.modelDocMappingChangedListeners.add(listener);
|
|
994
|
+
return () => this.modelDocMappingChangedListeners.delete(listener);
|
|
995
|
+
}
|
|
996
|
+
/** Drop the model→document defaults that point at a disconnected document. */
|
|
997
|
+
clearMappingsForDocId(docId) {
|
|
998
|
+
const toRemove = [];
|
|
999
|
+
for (const [modelName, mappedDocId] of this.modelNameToDefaultDocId) {
|
|
1000
|
+
if (mappedDocId === docId) toRemove.push(modelName);
|
|
1001
|
+
}
|
|
1002
|
+
for (const modelName of toRemove) {
|
|
1003
|
+
this.removeModelDefaultDocumentId(modelName);
|
|
1004
|
+
}
|
|
1005
|
+
if (this.globalDefaultDocId === docId) {
|
|
1006
|
+
this.clearGlobalDefaultDocumentId();
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
announceDefaultDoc(previous, current) {
|
|
1010
|
+
for (const listener of this.defaultDocChangedListeners) {
|
|
1011
|
+
try {
|
|
1012
|
+
listener({ previous, current });
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
announceModelDocMapping(modelName, previous, current) {
|
|
1018
|
+
for (const listener of this.modelDocMappingChangedListeners) {
|
|
1019
|
+
try {
|
|
1020
|
+
listener({ modelName, previous, current });
|
|
1021
|
+
} catch {
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Tear this instance down: observers off, engine destroyed, state cleared.
|
|
1027
|
+
*
|
|
1028
|
+
* Deliberately does NOT unbind its model classes. They keep resolving this
|
|
1029
|
+
* context, whose engine is now null, so an operation through them throws the
|
|
1030
|
+
* ordinary not-initialized error instead of silently landing on whatever
|
|
1031
|
+
* other instance happens to be live (#3039, behavior 7). Only a later
|
|
1032
|
+
* `initJsBao` may rebind a class whose context is destroyed.
|
|
1033
|
+
*/
|
|
1034
|
+
async destroy() {
|
|
1035
|
+
if (this.destroyed) return;
|
|
1036
|
+
this.destroyed = true;
|
|
1037
|
+
this.disposeAllObservers();
|
|
1038
|
+
const hooks = Array.from(this.teardownHooks);
|
|
1039
|
+
this.teardownHooks.clear();
|
|
1040
|
+
for (const hook of hooks) {
|
|
1041
|
+
try {
|
|
1042
|
+
await hook();
|
|
1043
|
+
} catch {
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const engine = this.dbEngine;
|
|
1047
|
+
this.dbEngine = null;
|
|
1048
|
+
if (engine) {
|
|
1049
|
+
try {
|
|
1050
|
+
await engine.destroy();
|
|
1051
|
+
} catch {
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
this.connectedDocuments.clear();
|
|
1055
|
+
this.documentYMaps.clear();
|
|
1056
|
+
this.listenersMap.clear();
|
|
1057
|
+
this.modelNameToDefaultDocId.clear();
|
|
1058
|
+
this.globalDefaultDocId = void 0;
|
|
1059
|
+
this.defaultDocChangedListeners.clear();
|
|
1060
|
+
this.modelDocMappingChangedListeners.clear();
|
|
1061
|
+
this.format2Documents.clear();
|
|
1062
|
+
this.format2Transitions.clear();
|
|
1063
|
+
this.observedStringSetMaps = /* @__PURE__ */ new WeakSet();
|
|
1064
|
+
liveOrmContexts.delete(this);
|
|
1065
|
+
if (defaultOrmContext === this) {
|
|
1066
|
+
defaultOrmContext = new _OrmContext();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
defaultOrmContext = new OrmContext();
|
|
1071
|
+
liveOrmContexts = /* @__PURE__ */ new Set();
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
// src/models/format2QueryProjection.ts
|
|
1076
|
+
var FORMAT2_QUERY_PROJECTION_DDL;
|
|
1077
|
+
var init_format2QueryProjection = __esm({
|
|
1078
|
+
"src/models/format2QueryProjection.ts"() {
|
|
1079
|
+
"use strict";
|
|
1080
|
+
FORMAT2_QUERY_PROJECTION_DDL = `
|
|
1081
|
+
CREATE TABLE IF NOT EXISTS _query_projection (
|
|
1082
|
+
doc_id TEXT NOT NULL,
|
|
1083
|
+
model TEXT NOT NULL,
|
|
1084
|
+
PRIMARY KEY (doc_id, model)
|
|
1085
|
+
)`.trim();
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// src/models/format2Observer.ts
|
|
1090
|
+
var Y7;
|
|
1091
|
+
var init_format2Observer = __esm({
|
|
1092
|
+
"src/models/format2Observer.ts"() {
|
|
1093
|
+
"use strict";
|
|
1094
|
+
Y7 = __toESM(require("yjs"), 1);
|
|
1095
|
+
init_largeDocuments();
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// src/models/format2ClientWrite.ts
|
|
1100
|
+
var init_format2ClientWrite = __esm({
|
|
1101
|
+
"src/models/format2ClientWrite.ts"() {
|
|
1102
|
+
"use strict";
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// src/models/format2OfflineWindow.ts
|
|
1107
|
+
var DAY_MS;
|
|
1108
|
+
var init_format2OfflineWindow = __esm({
|
|
1109
|
+
"src/models/format2OfflineWindow.ts"() {
|
|
1110
|
+
"use strict";
|
|
1111
|
+
DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
537
1115
|
// src/models/BaseModel.ts
|
|
538
1116
|
function generateULID() {
|
|
539
|
-
return (0,
|
|
1117
|
+
return (0, import_ulid3.ulid)();
|
|
540
1118
|
}
|
|
541
|
-
var
|
|
1119
|
+
var Y8, import_ulid3;
|
|
542
1120
|
var init_BaseModel = __esm({
|
|
543
1121
|
"src/models/BaseModel.ts"() {
|
|
544
1122
|
"use strict";
|
|
545
|
-
|
|
546
|
-
|
|
1123
|
+
Y8 = __toESM(require("yjs"), 1);
|
|
1124
|
+
import_ulid3 = require("ulid");
|
|
547
1125
|
init_StringSet();
|
|
548
1126
|
init_documentTypes();
|
|
549
1127
|
init_DocumentQueryTranslator();
|
|
550
1128
|
init_CursorManager();
|
|
551
1129
|
init_sql();
|
|
552
1130
|
init_metaSync();
|
|
1131
|
+
init_writeLifecycle();
|
|
553
1132
|
init_yjsValues();
|
|
1133
|
+
init_uniqueIndex();
|
|
1134
|
+
init_largeDocuments();
|
|
1135
|
+
init_OrmContext();
|
|
1136
|
+
init_format2QueryProjection();
|
|
1137
|
+
init_format2Observer();
|
|
1138
|
+
init_format2ClientWrite();
|
|
1139
|
+
init_format2OfflineWindow();
|
|
554
1140
|
registerFunctionDefault(generateULID, "generate_ulid");
|
|
555
1141
|
}
|
|
556
1142
|
});
|
|
@@ -569,29 +1155,63 @@ var init_ModelRegistry = __esm({
|
|
|
569
1155
|
"use strict";
|
|
570
1156
|
init_BaseModel();
|
|
571
1157
|
init_relationshipManager();
|
|
1158
|
+
init_format2QueryProjection();
|
|
572
1159
|
}
|
|
573
1160
|
});
|
|
574
1161
|
|
|
575
1162
|
// src/cloudflare-do.ts
|
|
576
1163
|
var cloudflare_do_exports = {};
|
|
577
1164
|
__export(cloudflare_do_exports, {
|
|
1165
|
+
DEFAULT_OVERLAY_TABLES: () => DEFAULT_OVERLAY_TABLES,
|
|
578
1166
|
DurableObjectEngine: () => DurableObjectEngine,
|
|
1167
|
+
Format2CompositeFieldError: () => Format2CompositeFieldError,
|
|
579
1168
|
JsonQueryTranslator: () => JsonQueryTranslator,
|
|
580
1169
|
JsonSchemaDDL: () => JsonSchemaDDL,
|
|
1170
|
+
OVERLAY_KEY_SEPARATOR: () => OVERLAY_KEY_SEPARATOR,
|
|
1171
|
+
OVERLAY_MARKER_DELETED: () => OVERLAY_MARKER_DELETED,
|
|
1172
|
+
OVERLAY_MARKER_REPLACE: () => OVERLAY_MARKER_REPLACE,
|
|
1173
|
+
PROJECTED_JSON_ALIAS_PREFIX: () => PROJECTED_JSON_ALIAS_PREFIX,
|
|
1174
|
+
SnapshotChunkIntegrityError: () => SnapshotChunkIntegrityError,
|
|
1175
|
+
SnapshotRowFormatError: () => SnapshotRowFormatError,
|
|
1176
|
+
applyOverlayMutation: () => applyOverlayMutation,
|
|
1177
|
+
assertNoFormat2CompositeFields: () => assertNoFormat2CompositeFields,
|
|
581
1178
|
buildSafeAlias: () => buildSafeAlias,
|
|
582
1179
|
checkAutoStampsReservedFields: () => checkAutoStampsReservedFields,
|
|
583
1180
|
checkReservedFieldName: () => checkReservedFieldName,
|
|
584
1181
|
clearMetaSyncCache: () => clearMetaSyncCache,
|
|
1182
|
+
completeOverlayEntries: () => completeOverlayEntries,
|
|
585
1183
|
createDatabaseDO: () => createDatabaseDO,
|
|
586
1184
|
createDocumentDO: () => createDocumentDO,
|
|
1185
|
+
decodeOverlaySegment: () => decodeOverlaySegment,
|
|
587
1186
|
deterministicIdentifierHash: () => deterministicIdentifierHash,
|
|
1187
|
+
discoverDeclaredModel: () => discoverDeclaredModel,
|
|
588
1188
|
discoverModelNames: () => discoverModelNames,
|
|
589
1189
|
discoverSchema: () => discoverSchema,
|
|
1190
|
+
encodeOverlayMutation: () => encodeOverlayMutation,
|
|
1191
|
+
encodeOverlaySegment: () => encodeOverlaySegment,
|
|
1192
|
+
encodeSnapshotChunk: () => encodeSnapshotChunk,
|
|
1193
|
+
format2StringSetMembers: () => format2StringSetMembers,
|
|
1194
|
+
groupOverlayEntries: () => groupOverlayEntries,
|
|
590
1195
|
handleRequest: () => handleRequest,
|
|
591
1196
|
inferFieldType: () => inferFieldType,
|
|
1197
|
+
isFormat2StringSetYMap: () => isFormat2StringSetYMap,
|
|
592
1198
|
loadSchemaFromTomlString: () => loadSchemaFromTomlString,
|
|
1199
|
+
materializeOverlayRecord: () => materializeOverlayRecord,
|
|
1200
|
+
overlayFieldKey: () => overlayFieldKey,
|
|
1201
|
+
overlayMarkerKey: () => overlayMarkerKey,
|
|
1202
|
+
overlayMemberKey: () => overlayMemberKey,
|
|
1203
|
+
parseOverlayKey: () => parseOverlayKey,
|
|
1204
|
+
parseRecordRow: () => parseRecordRow,
|
|
1205
|
+
projectOverlayEntry: () => projectOverlayEntry,
|
|
1206
|
+
projectedJsonAlias: () => projectedJsonAlias,
|
|
1207
|
+
readOverlayRecord: () => readOverlayRecord,
|
|
1208
|
+
readSnapshotChunk: () => readSnapshotChunk,
|
|
593
1209
|
registerFunctionDefault: () => registerFunctionDefault,
|
|
594
1210
|
schemaToToml: () => schemaToToml,
|
|
1211
|
+
snapshotChunkLines: () => snapshotChunkLines,
|
|
1212
|
+
snapshotDeclaredSchema: () => snapshotDeclaredSchema,
|
|
1213
|
+
snapshotRowAsOverlayEntry: () => snapshotRowAsOverlayEntry,
|
|
1214
|
+
snapshotStringSetFields: () => snapshotStringSetFields,
|
|
595
1215
|
syncInferredMeta: () => syncInferredMeta,
|
|
596
1216
|
syncModelMeta: () => syncModelMeta
|
|
597
1217
|
});
|
|
@@ -629,6 +1249,32 @@ var DatabaseEngine = class {
|
|
|
629
1249
|
getTableName(_modelName) {
|
|
630
1250
|
throw new Error("Method not implemented.");
|
|
631
1251
|
}
|
|
1252
|
+
/**
|
|
1253
|
+
* A SYNCHRONOUS SQL executor over this engine's database, used by the
|
|
1254
|
+
* large-document (format 2) record store. That store commits a merged
|
|
1255
|
+
* record row and its pending-operation entry as one transaction — the
|
|
1256
|
+
* barrier a `save()` resolves behind — which the async `query()` API cannot
|
|
1257
|
+
* express. Engines whose underlying SQLite is synchronous (better-sqlite3,
|
|
1258
|
+
* SQL.js) provide it; anything else leaves it undefined and cannot host a
|
|
1259
|
+
* large document.
|
|
1260
|
+
*/
|
|
1261
|
+
createFormat2Sql;
|
|
1262
|
+
/**
|
|
1263
|
+
* Whether what {@link createFormat2Sql} writes survives this process.
|
|
1264
|
+
*
|
|
1265
|
+
* On a large document the merged record view IS the document: the Y.Doc
|
|
1266
|
+
* holds one epoch's overlay, and everything else — including a record a
|
|
1267
|
+
* `save()` has already resolved, and the pending write that still owes it to
|
|
1268
|
+
* the server — lives only in this store. An engine whose database is in
|
|
1269
|
+
* memory therefore cannot host one: a reload does not cost a cache, it costs
|
|
1270
|
+
* the document and the writes nobody has acknowledged yet.
|
|
1271
|
+
*
|
|
1272
|
+
* The platform is asked separately about its storage quota; this answers the
|
|
1273
|
+
* half only the engine knows. An engine that does not implement it is taken
|
|
1274
|
+
* at the platform's word, which is what keeps hosts with storage of their
|
|
1275
|
+
* own — a worker handed a file store, a test — working unchanged.
|
|
1276
|
+
*/
|
|
1277
|
+
format2StorageIsDurable;
|
|
632
1278
|
// Transaction support
|
|
633
1279
|
async withTransaction(_callback) {
|
|
634
1280
|
throw new Error("Method not implemented.");
|
|
@@ -1605,6 +2251,45 @@ var DurableObjectEngine = class extends JsonSchemaEngine {
|
|
|
1605
2251
|
init_queryTypes();
|
|
1606
2252
|
init_CursorManager();
|
|
1607
2253
|
init_sql();
|
|
2254
|
+
|
|
2255
|
+
// src/query/recordRow.ts
|
|
2256
|
+
var PROJECTED_JSON_ALIAS_PREFIX = "_json_";
|
|
2257
|
+
function projectedJsonAlias(fieldName) {
|
|
2258
|
+
return `${PROJECTED_JSON_ALIAS_PREFIX}${fieldName}`;
|
|
2259
|
+
}
|
|
2260
|
+
function parseRecordRow(row) {
|
|
2261
|
+
const { _id, _type, _data, ...rest } = row;
|
|
2262
|
+
let parsed = {};
|
|
2263
|
+
if (_data) {
|
|
2264
|
+
try {
|
|
2265
|
+
parsed = JSON.parse(_data);
|
|
2266
|
+
} catch (e) {
|
|
2267
|
+
console.warn("Failed to parse _data:", e);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
return { id: _id, type: _type, ...parsed, ...decodeProjectedColumns(rest) };
|
|
2271
|
+
}
|
|
2272
|
+
function decodeProjectedColumns(columns) {
|
|
2273
|
+
const decoded = {};
|
|
2274
|
+
for (const [column, value] of Object.entries(columns)) {
|
|
2275
|
+
if (!column.startsWith(PROJECTED_JSON_ALIAS_PREFIX)) {
|
|
2276
|
+
decoded[column] = value;
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
decoded[column.slice(PROJECTED_JSON_ALIAS_PREFIX.length)] = parseJsonColumn(value);
|
|
2280
|
+
}
|
|
2281
|
+
return decoded;
|
|
2282
|
+
}
|
|
2283
|
+
function parseJsonColumn(value) {
|
|
2284
|
+
if (typeof value !== "string") return value;
|
|
2285
|
+
try {
|
|
2286
|
+
return JSON.parse(value);
|
|
2287
|
+
} catch {
|
|
2288
|
+
return value;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// src/query/JsonQueryTranslator.ts
|
|
1608
2293
|
init_patterns();
|
|
1609
2294
|
var SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
|
|
1610
2295
|
var JsonQueryTranslator = class {
|
|
@@ -1649,7 +2334,8 @@ var JsonQueryTranslator = class {
|
|
|
1649
2334
|
const whereClause = this.translateFilter(filter);
|
|
1650
2335
|
const orderClause = CursorManager.buildOrderClause(
|
|
1651
2336
|
options?.sort,
|
|
1652
|
-
(field) => this.getFieldSql(field)
|
|
2337
|
+
(field) => this.getFieldSql(field),
|
|
2338
|
+
options?.direction || 1
|
|
1653
2339
|
);
|
|
1654
2340
|
const limitClause = this.buildLimitClause(options);
|
|
1655
2341
|
const paginationClause = this.buildPaginationClause(
|
|
@@ -1922,8 +2608,11 @@ var JsonQueryTranslator = class {
|
|
|
1922
2608
|
};
|
|
1923
2609
|
case "$ne":
|
|
1924
2610
|
this.validateFieldValue(fieldName, value);
|
|
2611
|
+
if (value === null || value === void 0) {
|
|
2612
|
+
return { sql: `${fieldSql} IS NOT NULL`, params: [] };
|
|
2613
|
+
}
|
|
1925
2614
|
return {
|
|
1926
|
-
sql:
|
|
2615
|
+
sql: `(${fieldSql} IS NULL OR ${fieldSql} != ?)`,
|
|
1927
2616
|
params: [this.convertValueForSQLite(value)]
|
|
1928
2617
|
};
|
|
1929
2618
|
case "$gt":
|
|
@@ -2006,12 +2695,23 @@ var JsonQueryTranslator = class {
|
|
|
2006
2695
|
if (value.length === 0) {
|
|
2007
2696
|
return { sql: "", params: [] };
|
|
2008
2697
|
}
|
|
2009
|
-
const
|
|
2010
|
-
const
|
|
2698
|
+
const ninHasNull = value.some((v) => v === null || v === void 0);
|
|
2699
|
+
const ninValues = value.filter((v) => v !== null && v !== void 0);
|
|
2700
|
+
const ninPlaceholders = ninValues.map(() => "?").join(",");
|
|
2701
|
+
const convertedNinValues = ninValues.map(
|
|
2011
2702
|
(v) => this.convertValueForSQLite(v)
|
|
2012
2703
|
);
|
|
2704
|
+
if (ninHasNull) {
|
|
2705
|
+
if (ninValues.length === 0) {
|
|
2706
|
+
return { sql: `${fieldSql} IS NOT NULL`, params: [] };
|
|
2707
|
+
}
|
|
2708
|
+
return {
|
|
2709
|
+
sql: `(${fieldSql} IS NOT NULL AND ${fieldSql} NOT IN (${ninPlaceholders}))`,
|
|
2710
|
+
params: convertedNinValues
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2013
2713
|
return {
|
|
2014
|
-
sql:
|
|
2714
|
+
sql: `(${fieldSql} IS NULL OR ${fieldSql} NOT IN (${ninPlaceholders}))`,
|
|
2015
2715
|
params: convertedNinValues
|
|
2016
2716
|
};
|
|
2017
2717
|
case "$exists":
|
|
@@ -2100,7 +2800,7 @@ var JsonQueryTranslator = class {
|
|
|
2100
2800
|
for (const field of includeFields) {
|
|
2101
2801
|
if (field === "id" || field === "type") continue;
|
|
2102
2802
|
selectParts.push(
|
|
2103
|
-
`
|
|
2803
|
+
`_data -> '$.${field}' AS ${quoteIdentifier(projectedJsonAlias(field))}`
|
|
2104
2804
|
);
|
|
2105
2805
|
}
|
|
2106
2806
|
return selectParts.join(", ");
|
|
@@ -2287,6 +2987,13 @@ var JsonQueryTranslator = class {
|
|
|
2287
2987
|
init_CursorManager();
|
|
2288
2988
|
init_sql();
|
|
2289
2989
|
var import_ulid = require("ulid");
|
|
2990
|
+
var AtomicBatchAbort = class extends Error {
|
|
2991
|
+
constructor(failedIndex) {
|
|
2992
|
+
super(`atomic batch aborted at operation ${failedIndex}`);
|
|
2993
|
+
this.failedIndex = failedIndex;
|
|
2994
|
+
this.name = "AtomicBatchAbort";
|
|
2995
|
+
}
|
|
2996
|
+
};
|
|
2290
2997
|
function checkReservedFieldName(fieldName) {
|
|
2291
2998
|
if (fieldName === "id") return null;
|
|
2292
2999
|
if (fieldName === "type") {
|
|
@@ -2331,6 +3038,35 @@ function applyAutoStamps(data, autoStamps, isNew) {
|
|
|
2331
3038
|
}
|
|
2332
3039
|
}
|
|
2333
3040
|
}
|
|
3041
|
+
function collectAppliedStamps(data, autoStamps, isNew) {
|
|
3042
|
+
if (!autoStamps || !data) return void 0;
|
|
3043
|
+
const stamps = {};
|
|
3044
|
+
const take = (fields) => {
|
|
3045
|
+
if (!fields) return;
|
|
3046
|
+
for (const field of fields) {
|
|
3047
|
+
const value = data[field];
|
|
3048
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3049
|
+
stamps[field] = value;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
};
|
|
3053
|
+
if (isNew) take(autoStamps.create);
|
|
3054
|
+
take(autoStamps.update);
|
|
3055
|
+
return Object.keys(stamps).length > 0 ? stamps : void 0;
|
|
3056
|
+
}
|
|
3057
|
+
function collectAppliedValues(data, fieldNames) {
|
|
3058
|
+
if (!data || !fieldNames || fieldNames.length === 0) return void 0;
|
|
3059
|
+
const out = {};
|
|
3060
|
+
for (const name of fieldNames) {
|
|
3061
|
+
const value = data[name];
|
|
3062
|
+
if (value !== void 0) out[name] = value;
|
|
3063
|
+
}
|
|
3064
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
3065
|
+
}
|
|
3066
|
+
function mergeAppliedFields(stamps, applied) {
|
|
3067
|
+
if (!stamps && !applied) return void 0;
|
|
3068
|
+
return { ...stamps ?? {}, ...applied ?? {} };
|
|
3069
|
+
}
|
|
2334
3070
|
function createDatabaseDO(config = {}) {
|
|
2335
3071
|
const hooks = config.hooks;
|
|
2336
3072
|
return class DocumentDO {
|
|
@@ -2488,6 +3224,16 @@ function createDatabaseDO(config = {}) {
|
|
|
2488
3224
|
hasMore,
|
|
2489
3225
|
isFirstPage
|
|
2490
3226
|
);
|
|
3227
|
+
if (options?.projection && Object.keys(options.projection).length > 0) {
|
|
3228
|
+
const isExclusion = Object.values(options.projection).some(
|
|
3229
|
+
(v) => v === 0
|
|
3230
|
+
);
|
|
3231
|
+
if (isExclusion) {
|
|
3232
|
+
data = data.map(
|
|
3233
|
+
(rec) => this._applyProjection(rec, options.projection)
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
2491
3237
|
const response = {
|
|
2492
3238
|
data,
|
|
2493
3239
|
hasMore,
|
|
@@ -2590,6 +3336,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2590
3336
|
}
|
|
2591
3337
|
const isNew = !this._engine.recordExists(modelName, id);
|
|
2592
3338
|
applyAutoStamps(data, autoStamps, isNew);
|
|
3339
|
+
let hookAppliedNames;
|
|
2593
3340
|
if (hooks?.beforeSave) {
|
|
2594
3341
|
const ctx = {
|
|
2595
3342
|
modelName,
|
|
@@ -2605,6 +3352,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2605
3352
|
if (!result.allow) {
|
|
2606
3353
|
return this._errorResponse(result.reason || "Save denied", 403);
|
|
2607
3354
|
}
|
|
3355
|
+
hookAppliedNames = result.serverAppliedFieldNames;
|
|
2608
3356
|
}
|
|
2609
3357
|
const existingForUnique = this._engine.execSqlSync(
|
|
2610
3358
|
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
@@ -2634,6 +3382,11 @@ function createDatabaseDO(config = {}) {
|
|
|
2634
3382
|
}
|
|
2635
3383
|
this._engine.trackModelFields(modelName, data);
|
|
2636
3384
|
const response = { success: true, id };
|
|
3385
|
+
const appliedFields = mergeAppliedFields(
|
|
3386
|
+
collectAppliedStamps(data, autoStamps, isNew),
|
|
3387
|
+
collectAppliedValues(data, hookAppliedNames)
|
|
3388
|
+
);
|
|
3389
|
+
if (appliedFields) response.appliedFields = appliedFields;
|
|
2637
3390
|
return Response.json(response);
|
|
2638
3391
|
}
|
|
2639
3392
|
/** @internal — Partial update: merge provided fields into existing record */
|
|
@@ -2663,6 +3416,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2663
3416
|
);
|
|
2664
3417
|
}
|
|
2665
3418
|
applyAutoStamps(data, autoStamps, false);
|
|
3419
|
+
let hookAppliedNames;
|
|
2666
3420
|
if (hooks?.beforeSave) {
|
|
2667
3421
|
const ctx = {
|
|
2668
3422
|
modelName,
|
|
@@ -2678,6 +3432,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2678
3432
|
if (!result.allow) {
|
|
2679
3433
|
return this._errorResponse(result.reason || "Save denied", 403);
|
|
2680
3434
|
}
|
|
3435
|
+
hookAppliedNames = result.serverAppliedFieldNames;
|
|
2681
3436
|
}
|
|
2682
3437
|
const existing = this._engine.execSqlSync(
|
|
2683
3438
|
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
@@ -2701,6 +3456,11 @@ function createDatabaseDO(config = {}) {
|
|
|
2701
3456
|
}
|
|
2702
3457
|
this._engine.trackModelFields(modelName, data);
|
|
2703
3458
|
const response = { success: true, id };
|
|
3459
|
+
const appliedFields = mergeAppliedFields(
|
|
3460
|
+
collectAppliedStamps(data, autoStamps, false),
|
|
3461
|
+
collectAppliedValues(data, hookAppliedNames)
|
|
3462
|
+
);
|
|
3463
|
+
if (appliedFields) response.appliedFields = appliedFields;
|
|
2704
3464
|
return Response.json(response);
|
|
2705
3465
|
}
|
|
2706
3466
|
/** @internal */
|
|
@@ -2742,7 +3502,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2742
3502
|
/** @internal — Execute multiple save/patch/delete operations in a single transaction */
|
|
2743
3503
|
async _handleBatch(request, docId) {
|
|
2744
3504
|
const body = await request.json();
|
|
2745
|
-
const { operations } = body;
|
|
3505
|
+
const { operations, atomic = false } = body;
|
|
2746
3506
|
if (!operations || !Array.isArray(operations) || operations.length === 0) {
|
|
2747
3507
|
return this._errorResponse("operations array is required and must not be empty", 400);
|
|
2748
3508
|
}
|
|
@@ -2750,6 +3510,8 @@ function createDatabaseDO(config = {}) {
|
|
|
2750
3510
|
const resolvedBatchIds = /* @__PURE__ */ new Map();
|
|
2751
3511
|
const opErrors = /* @__PURE__ */ new Map();
|
|
2752
3512
|
const batchCreatedIds = /* @__PURE__ */ new Set();
|
|
3513
|
+
const batchIsNew = /* @__PURE__ */ new Map();
|
|
3514
|
+
const batchAppliedNames = /* @__PURE__ */ new Map();
|
|
2753
3515
|
for (let i = 0; i < operations.length; i++) {
|
|
2754
3516
|
const op = operations[i];
|
|
2755
3517
|
if (!op.modelName || !op.id && !(op.op === "save" && op.upsertOn) || !op.op) {
|
|
@@ -2823,6 +3585,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2823
3585
|
if (op.op === "save" && isNew && effectiveId) {
|
|
2824
3586
|
batchCreatedIds.add(`${op.modelName}/${effectiveId}`);
|
|
2825
3587
|
}
|
|
3588
|
+
batchIsNew.set(i, isNew);
|
|
2826
3589
|
applyAutoStamps(op.data, op.autoStamps, isNew);
|
|
2827
3590
|
if (hooks?.beforeSave) {
|
|
2828
3591
|
const ctx = {
|
|
@@ -2838,6 +3601,8 @@ function createDatabaseDO(config = {}) {
|
|
|
2838
3601
|
const result = await hooks.beforeSave(ctx);
|
|
2839
3602
|
if (!result.allow) {
|
|
2840
3603
|
hookDenials.set(i, result.reason || "Save denied");
|
|
3604
|
+
} else if (result.serverAppliedFieldNames) {
|
|
3605
|
+
batchAppliedNames.set(i, result.serverAppliedFieldNames);
|
|
2841
3606
|
}
|
|
2842
3607
|
}
|
|
2843
3608
|
} else if (op.op === "delete") {
|
|
@@ -2886,205 +3651,263 @@ function createDatabaseDO(config = {}) {
|
|
|
2886
3651
|
}
|
|
2887
3652
|
const results = [];
|
|
2888
3653
|
const fieldsToTrack = [];
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
|
|
2911
|
-
results.push({
|
|
2912
|
-
success: false,
|
|
2913
|
-
id: conditionId,
|
|
2914
|
-
error: `Condition not met for ${op.modelName}/${conditionId}`
|
|
2915
|
-
});
|
|
2916
|
-
continue;
|
|
2917
|
-
}
|
|
2918
|
-
if (op.op === "save") {
|
|
2919
|
-
let opId = resolvedBatchIds.get(i) || op.id;
|
|
2920
|
-
if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
|
|
2921
|
-
results.push({
|
|
2922
|
-
success: false,
|
|
2923
|
-
id: opId,
|
|
2924
|
-
error: `Record ${op.modelName}/${opId} already exists`
|
|
3654
|
+
const pushFailure = (index, failure) => {
|
|
3655
|
+
results.push({
|
|
3656
|
+
success: false,
|
|
3657
|
+
id: failure.id,
|
|
3658
|
+
error: failure.error,
|
|
3659
|
+
status: failure.status,
|
|
3660
|
+
code: failure.code
|
|
3661
|
+
});
|
|
3662
|
+
if (atomic) throw new AtomicBatchAbort(index);
|
|
3663
|
+
};
|
|
3664
|
+
try {
|
|
3665
|
+
this._engine.transactionSync(() => {
|
|
3666
|
+
for (let i = 0; i < operations.length; i++) {
|
|
3667
|
+
const op = operations[i];
|
|
3668
|
+
if (hookDenials.has(i)) {
|
|
3669
|
+
const effectiveId = resolvedBatchIds.get(i) || op.id || "";
|
|
3670
|
+
pushFailure(i, {
|
|
3671
|
+
id: effectiveId,
|
|
3672
|
+
error: hookDenials.get(i),
|
|
3673
|
+
status: 403,
|
|
3674
|
+
code: "HOOK_DENIED"
|
|
2925
3675
|
});
|
|
2926
3676
|
continue;
|
|
2927
3677
|
}
|
|
2928
|
-
if (
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3678
|
+
if (opErrors.has(i)) {
|
|
3679
|
+
pushFailure(i, {
|
|
3680
|
+
id: op.id || "",
|
|
3681
|
+
error: opErrors.get(i),
|
|
3682
|
+
status: 400,
|
|
3683
|
+
code: "VALIDATION_ERROR"
|
|
3684
|
+
});
|
|
3685
|
+
continue;
|
|
3686
|
+
}
|
|
3687
|
+
const conditionId = resolvedBatchIds.get(i) || op.id;
|
|
3688
|
+
if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
|
|
3689
|
+
pushFailure(i, {
|
|
3690
|
+
id: conditionId,
|
|
3691
|
+
error: `Condition not met for ${op.modelName}/${conditionId}`,
|
|
3692
|
+
status: 409,
|
|
3693
|
+
code: "CONDITION_NOT_MET"
|
|
3694
|
+
});
|
|
3695
|
+
continue;
|
|
3696
|
+
}
|
|
3697
|
+
if (op.op === "save") {
|
|
3698
|
+
let opId = resolvedBatchIds.get(i) || op.id;
|
|
3699
|
+
if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
|
|
3700
|
+
pushFailure(i, {
|
|
2946
3701
|
id: opId,
|
|
2947
|
-
error:
|
|
3702
|
+
error: `Record ${op.modelName}/${opId} already exists`,
|
|
3703
|
+
status: 409,
|
|
3704
|
+
code: "ALREADY_EXISTS"
|
|
2948
3705
|
});
|
|
2949
3706
|
continue;
|
|
2950
3707
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
const dataJson = JSON.stringify(jsonFields);
|
|
2956
|
-
this._engine.execSqlSync(
|
|
2957
|
-
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
2958
|
-
[opId, op.modelName, dataJson]
|
|
2959
|
-
);
|
|
2960
|
-
for (const [fieldName, values] of Object.entries(op.stringSets)) {
|
|
2961
|
-
this._engine.execSqlSync(
|
|
2962
|
-
"DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
|
|
2963
|
-
[op.modelName, opId, fieldName]
|
|
3708
|
+
if (op.data) {
|
|
3709
|
+
const existingForUnique = this._engine.execSqlSync(
|
|
3710
|
+
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
3711
|
+
[opId, op.modelName]
|
|
2964
3712
|
);
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
);
|
|
3713
|
+
let dataForUnique = op.data;
|
|
3714
|
+
if (existingForUnique.length > 0) {
|
|
3715
|
+
const existingData = JSON.parse(existingForUnique[0]._data);
|
|
3716
|
+
dataForUnique = { ...existingData, ...op.data };
|
|
2970
3717
|
}
|
|
2971
|
-
}
|
|
2972
|
-
} else {
|
|
2973
|
-
const { id: _id, ...fieldsForJson } = op.data;
|
|
2974
|
-
const dataJson = JSON.stringify(fieldsForJson);
|
|
2975
|
-
this._engine.execSqlSync(
|
|
2976
|
-
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
2977
|
-
[opId, op.modelName, dataJson]
|
|
2978
|
-
);
|
|
2979
|
-
}
|
|
2980
|
-
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
2981
|
-
results.push({ success: true, id: opId });
|
|
2982
|
-
} else if (op.op === "patch") {
|
|
2983
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
2984
|
-
results.push({
|
|
2985
|
-
success: false,
|
|
2986
|
-
id: op.id,
|
|
2987
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
2988
|
-
});
|
|
2989
|
-
continue;
|
|
2990
|
-
}
|
|
2991
|
-
if (op.data) {
|
|
2992
|
-
const existing = this._engine.execSqlSync(
|
|
2993
|
-
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
2994
|
-
[op.id, op.modelName]
|
|
2995
|
-
);
|
|
2996
|
-
if (existing.length > 0) {
|
|
2997
|
-
const existingData = JSON.parse(existing[0]._data);
|
|
2998
|
-
const mergedData = { ...existingData, ...op.data };
|
|
2999
3718
|
const violation = this._engine.checkUniqueConstraints(
|
|
3000
3719
|
op.modelName,
|
|
3001
|
-
|
|
3002
|
-
|
|
3720
|
+
opId,
|
|
3721
|
+
dataForUnique
|
|
3003
3722
|
);
|
|
3004
3723
|
if (violation) {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3724
|
+
pushFailure(i, {
|
|
3725
|
+
id: opId,
|
|
3726
|
+
error: violation,
|
|
3727
|
+
status: 409,
|
|
3728
|
+
code: "UNIQUE_VIOLATION"
|
|
3009
3729
|
});
|
|
3010
3730
|
continue;
|
|
3011
3731
|
}
|
|
3012
3732
|
}
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3733
|
+
if (op.stringSets && Object.keys(op.stringSets).length > 0) {
|
|
3734
|
+
const { id: _id, ...fieldsForJson } = op.data;
|
|
3735
|
+
const jsonFields = { ...fieldsForJson, ...op.stringSets };
|
|
3736
|
+
const dataJson = JSON.stringify(jsonFields);
|
|
3737
|
+
this._engine.execSqlSync(
|
|
3738
|
+
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
3739
|
+
[opId, op.modelName, dataJson]
|
|
3740
|
+
);
|
|
3741
|
+
for (const [fieldName, values] of Object.entries(op.stringSets)) {
|
|
3742
|
+
this._engine.execSqlSync(
|
|
3743
|
+
"DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
|
|
3744
|
+
[op.modelName, opId, fieldName]
|
|
3745
|
+
);
|
|
3746
|
+
for (const value of values) {
|
|
3747
|
+
this._engine.execSqlSync(
|
|
3748
|
+
"INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
|
|
3749
|
+
[opId, op.modelName, fieldName, value]
|
|
3750
|
+
);
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
} else {
|
|
3754
|
+
const { id: _id, ...fieldsForJson } = op.data;
|
|
3755
|
+
const dataJson = JSON.stringify(fieldsForJson);
|
|
3756
|
+
this._engine.execSqlSync(
|
|
3757
|
+
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
3758
|
+
[opId, op.modelName, dataJson]
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3027
3761
|
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
3762
|
+
const saveResult = { success: true, id: opId };
|
|
3763
|
+
const saveApplied = mergeAppliedFields(
|
|
3764
|
+
collectAppliedStamps(
|
|
3765
|
+
op.data,
|
|
3766
|
+
op.autoStamps,
|
|
3767
|
+
batchIsNew.get(i) ?? false
|
|
3768
|
+
),
|
|
3769
|
+
collectAppliedValues(op.data, batchAppliedNames.get(i))
|
|
3770
|
+
);
|
|
3771
|
+
if (saveApplied) saveResult.appliedFields = saveApplied;
|
|
3772
|
+
results.push(saveResult);
|
|
3773
|
+
} else if (op.op === "patch") {
|
|
3774
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3775
|
+
pushFailure(i, {
|
|
3776
|
+
id: op.id,
|
|
3777
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3778
|
+
status: 404,
|
|
3779
|
+
code: "NOT_FOUND"
|
|
3780
|
+
});
|
|
3781
|
+
continue;
|
|
3782
|
+
}
|
|
3783
|
+
if (op.data) {
|
|
3784
|
+
const existing = this._engine.execSqlSync(
|
|
3785
|
+
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
3786
|
+
[op.id, op.modelName]
|
|
3787
|
+
);
|
|
3788
|
+
if (existing.length > 0) {
|
|
3789
|
+
const existingData = JSON.parse(existing[0]._data);
|
|
3790
|
+
const mergedData = { ...existingData, ...op.data };
|
|
3791
|
+
const violation = this._engine.checkUniqueConstraints(
|
|
3792
|
+
op.modelName,
|
|
3793
|
+
op.id,
|
|
3794
|
+
mergedData
|
|
3795
|
+
);
|
|
3796
|
+
if (violation) {
|
|
3797
|
+
pushFailure(i, {
|
|
3798
|
+
id: op.id,
|
|
3799
|
+
error: violation,
|
|
3800
|
+
status: 409,
|
|
3801
|
+
code: "UNIQUE_VIOLATION"
|
|
3802
|
+
});
|
|
3803
|
+
continue;
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
const found = this._engine.patchRecordRaw(
|
|
3808
|
+
op.modelName,
|
|
3809
|
+
op.id,
|
|
3810
|
+
op.data,
|
|
3811
|
+
op.stringSets
|
|
3812
|
+
);
|
|
3813
|
+
if (!found) {
|
|
3814
|
+
pushFailure(i, {
|
|
3815
|
+
id: op.id,
|
|
3816
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3817
|
+
status: 404,
|
|
3818
|
+
code: "NOT_FOUND"
|
|
3819
|
+
});
|
|
3820
|
+
} else {
|
|
3821
|
+
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
3822
|
+
const patchResult = {
|
|
3823
|
+
success: true,
|
|
3824
|
+
id: op.id
|
|
3825
|
+
};
|
|
3826
|
+
const patchApplied = mergeAppliedFields(
|
|
3827
|
+
collectAppliedStamps(op.data, op.autoStamps, false),
|
|
3828
|
+
collectAppliedValues(op.data, batchAppliedNames.get(i))
|
|
3829
|
+
);
|
|
3830
|
+
if (patchApplied) patchResult.appliedFields = patchApplied;
|
|
3831
|
+
results.push(patchResult);
|
|
3832
|
+
}
|
|
3833
|
+
} else if (op.op === "delete") {
|
|
3834
|
+
this._engine.execSqlSync(
|
|
3835
|
+
"DELETE FROM records WHERE _id = ? AND _type = ?",
|
|
3836
|
+
[op.id, op.modelName]
|
|
3837
|
+
);
|
|
3838
|
+
this._engine.execSqlSync(
|
|
3839
|
+
"DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
|
|
3840
|
+
[op.id, op.modelName]
|
|
3841
|
+
);
|
|
3842
|
+
results.push({ success: true, id: op.id });
|
|
3843
|
+
} else if (op.op === "increment") {
|
|
3844
|
+
const newValues = this._engine.incrementFieldsRaw(
|
|
3845
|
+
op.modelName,
|
|
3846
|
+
op.id,
|
|
3847
|
+
op.fields
|
|
3848
|
+
);
|
|
3849
|
+
if (!newValues) {
|
|
3850
|
+
pushFailure(i, {
|
|
3851
|
+
id: op.id,
|
|
3852
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3853
|
+
status: 404,
|
|
3854
|
+
code: "NOT_FOUND"
|
|
3855
|
+
});
|
|
3856
|
+
} else {
|
|
3857
|
+
results.push({ success: true, id: op.id, values: newValues });
|
|
3858
|
+
}
|
|
3859
|
+
} else if (op.op === "addToSet") {
|
|
3860
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3861
|
+
pushFailure(i, {
|
|
3862
|
+
id: op.id,
|
|
3863
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3864
|
+
status: 404,
|
|
3865
|
+
code: "NOT_FOUND"
|
|
3866
|
+
});
|
|
3867
|
+
continue;
|
|
3868
|
+
}
|
|
3869
|
+
this._engine.addToStringSetsRaw(
|
|
3870
|
+
op.modelName,
|
|
3871
|
+
op.id,
|
|
3872
|
+
op.stringSets
|
|
3873
|
+
);
|
|
3874
|
+
results.push({ success: true, id: op.id });
|
|
3875
|
+
} else if (op.op === "removeFromSet") {
|
|
3876
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3877
|
+
pushFailure(i, {
|
|
3878
|
+
id: op.id,
|
|
3879
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3880
|
+
status: 404,
|
|
3881
|
+
code: "NOT_FOUND"
|
|
3882
|
+
});
|
|
3883
|
+
continue;
|
|
3884
|
+
}
|
|
3885
|
+
this._engine.removeFromStringSetsRaw(
|
|
3886
|
+
op.modelName,
|
|
3887
|
+
op.id,
|
|
3888
|
+
op.stringSets
|
|
3889
|
+
);
|
|
3028
3890
|
results.push({ success: true, id: op.id });
|
|
3029
3891
|
}
|
|
3030
|
-
} else if (op.op === "delete") {
|
|
3031
|
-
this._engine.execSqlSync(
|
|
3032
|
-
"DELETE FROM records WHERE _id = ? AND _type = ?",
|
|
3033
|
-
[op.id, op.modelName]
|
|
3034
|
-
);
|
|
3035
|
-
this._engine.execSqlSync(
|
|
3036
|
-
"DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
|
|
3037
|
-
[op.id, op.modelName]
|
|
3038
|
-
);
|
|
3039
|
-
results.push({ success: true, id: op.id });
|
|
3040
|
-
} else if (op.op === "increment") {
|
|
3041
|
-
const newValues = this._engine.incrementFieldsRaw(
|
|
3042
|
-
op.modelName,
|
|
3043
|
-
op.id,
|
|
3044
|
-
op.fields
|
|
3045
|
-
);
|
|
3046
|
-
if (!newValues) {
|
|
3047
|
-
results.push({
|
|
3048
|
-
success: false,
|
|
3049
|
-
id: op.id,
|
|
3050
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
3051
|
-
});
|
|
3052
|
-
} else {
|
|
3053
|
-
results.push({ success: true, id: op.id, values: newValues });
|
|
3054
|
-
}
|
|
3055
|
-
} else if (op.op === "addToSet") {
|
|
3056
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3057
|
-
results.push({
|
|
3058
|
-
success: false,
|
|
3059
|
-
id: op.id,
|
|
3060
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
3061
|
-
});
|
|
3062
|
-
continue;
|
|
3063
|
-
}
|
|
3064
|
-
this._engine.addToStringSetsRaw(
|
|
3065
|
-
op.modelName,
|
|
3066
|
-
op.id,
|
|
3067
|
-
op.stringSets
|
|
3068
|
-
);
|
|
3069
|
-
results.push({ success: true, id: op.id });
|
|
3070
|
-
} else if (op.op === "removeFromSet") {
|
|
3071
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3072
|
-
results.push({
|
|
3073
|
-
success: false,
|
|
3074
|
-
id: op.id,
|
|
3075
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
3076
|
-
});
|
|
3077
|
-
continue;
|
|
3078
|
-
}
|
|
3079
|
-
this._engine.removeFromStringSetsRaw(
|
|
3080
|
-
op.modelName,
|
|
3081
|
-
op.id,
|
|
3082
|
-
op.stringSets
|
|
3083
|
-
);
|
|
3084
|
-
results.push({ success: true, id: op.id });
|
|
3085
3892
|
}
|
|
3893
|
+
});
|
|
3894
|
+
} catch (err) {
|
|
3895
|
+
if (err instanceof AtomicBatchAbort) {
|
|
3896
|
+
const failed = results[err.failedIndex];
|
|
3897
|
+
const failedOp = operations[err.failedIndex];
|
|
3898
|
+
console.warn(
|
|
3899
|
+
`[js-bao batch] atomic rollback: operation ${err.failedIndex} (${failedOp?.op}) failed \u2014 ${failed?.error ?? "unknown error"}`
|
|
3900
|
+
);
|
|
3901
|
+
const errorBody = {
|
|
3902
|
+
error: `Operation ${err.failedIndex} (${failedOp?.op}) failed: ${failed?.error ?? "unknown error"}`,
|
|
3903
|
+
failedIndex: err.failedIndex,
|
|
3904
|
+
op: failedOp?.op,
|
|
3905
|
+
code: failed?.code
|
|
3906
|
+
};
|
|
3907
|
+
return Response.json(errorBody, { status: failed?.status ?? 409 });
|
|
3086
3908
|
}
|
|
3087
|
-
|
|
3909
|
+
throw err;
|
|
3910
|
+
}
|
|
3088
3911
|
for (const entry of fieldsToTrack) {
|
|
3089
3912
|
this._engine.trackModelFields(entry.modelName, entry.data);
|
|
3090
3913
|
}
|
|
@@ -3454,6 +4277,18 @@ function createDatabaseDO(config = {}) {
|
|
|
3454
4277
|
}
|
|
3455
4278
|
return facetResult;
|
|
3456
4279
|
}
|
|
4280
|
+
if (aggOptions.groupBy.length === 0) {
|
|
4281
|
+
const row = results[0];
|
|
4282
|
+
const flat = {};
|
|
4283
|
+
for (const op of aggOptions.operations) {
|
|
4284
|
+
if (op.type === "count") {
|
|
4285
|
+
flat.count = row.count;
|
|
4286
|
+
} else {
|
|
4287
|
+
flat[`${op.type}_${op.field}`] = row[`${op.type}_${op.field}`];
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
return flat;
|
|
4291
|
+
}
|
|
3457
4292
|
const nestedResult = {};
|
|
3458
4293
|
const aliasLookup = /* @__PURE__ */ new Map();
|
|
3459
4294
|
for (const entry of aliasMap) {
|
|
@@ -3781,16 +4616,7 @@ function createDatabaseDO(config = {}) {
|
|
|
3781
4616
|
}
|
|
3782
4617
|
/** @internal */
|
|
3783
4618
|
_parseRow(row) {
|
|
3784
|
-
|
|
3785
|
-
let parsed = {};
|
|
3786
|
-
if (_data) {
|
|
3787
|
-
try {
|
|
3788
|
-
parsed = JSON.parse(_data);
|
|
3789
|
-
} catch (e) {
|
|
3790
|
-
console.warn("Failed to parse _data:", e);
|
|
3791
|
-
}
|
|
3792
|
-
}
|
|
3793
|
-
return { id: _id, type: _type, ...parsed, ...rest };
|
|
4619
|
+
return parseRecordRow(row);
|
|
3794
4620
|
}
|
|
3795
4621
|
// ─── Include (Related Data Loading) ──────────────────────────────
|
|
3796
4622
|
/** @internal — Validate an IncludeSpec, returning an error message or null */
|
|
@@ -3920,8 +4746,9 @@ function createDatabaseDO(config = {}) {
|
|
|
3920
4746
|
spec.filter,
|
|
3921
4747
|
{ projection: spec.projection }
|
|
3922
4748
|
);
|
|
4749
|
+
const relatedProjected = spec.projection ? related.map((r) => this._applyProjection(r, spec.projection)) : related;
|
|
3923
4750
|
const lookupMap = /* @__PURE__ */ new Map();
|
|
3924
|
-
for (const r of
|
|
4751
|
+
for (const r of relatedProjected) {
|
|
3925
4752
|
lookupMap.set(r.id, r);
|
|
3926
4753
|
}
|
|
3927
4754
|
for (const rec of records) {
|
|
@@ -3954,8 +4781,9 @@ function createDatabaseDO(config = {}) {
|
|
|
3954
4781
|
spec.filter,
|
|
3955
4782
|
{ projection: spec.projection }
|
|
3956
4783
|
);
|
|
4784
|
+
const targetsProjected = spec.projection ? targets.map((t) => this._applyProjection(t, spec.projection)) : targets;
|
|
3957
4785
|
const targetLookup = /* @__PURE__ */ new Map();
|
|
3958
|
-
for (const t of
|
|
4786
|
+
for (const t of targetsProjected) {
|
|
3959
4787
|
targetLookup.set(t.id, t);
|
|
3960
4788
|
}
|
|
3961
4789
|
for (const rec of records) {
|
|
@@ -4099,6 +4927,14 @@ function createDatabaseDO(config = {}) {
|
|
|
4099
4927
|
return allResults;
|
|
4100
4928
|
}
|
|
4101
4929
|
/** @internal — Apply projection to a single record */
|
|
4930
|
+
// Apply a projection to a single record. Assumes the projection has
|
|
4931
|
+
// already been validated upstream (JsonQueryTranslator.validateProjection /
|
|
4932
|
+
// buildSelectClause reject mixed inclusion+exclusion): `isInclusion` treats
|
|
4933
|
+
// "any value === 1" as inclusion mode, so an unvalidated mixed projection
|
|
4934
|
+
// would silently ignore the excludes. `id` and `type` are system-identity
|
|
4935
|
+
// fields and are always kept in BOTH modes (a record without its `id` is
|
|
4936
|
+
// unusable downstream). `_meta` is NOT forced — an explicit `{ _meta: 0 }`
|
|
4937
|
+
// is honored.
|
|
4102
4938
|
_applyProjection(record, projection) {
|
|
4103
4939
|
const fields = Object.entries(projection);
|
|
4104
4940
|
if (fields.length === 0) return record;
|
|
@@ -4117,7 +4953,7 @@ function createDatabaseDO(config = {}) {
|
|
|
4117
4953
|
} else {
|
|
4118
4954
|
const result = { ...record };
|
|
4119
4955
|
for (const [f, v] of fields) {
|
|
4120
|
-
if (v === 0) delete result[f];
|
|
4956
|
+
if (v === 0 && f !== "id" && f !== "type") delete result[f];
|
|
4121
4957
|
}
|
|
4122
4958
|
return result;
|
|
4123
4959
|
}
|
|
@@ -4208,6 +5044,12 @@ function discoverSchema(yDoc) {
|
|
|
4208
5044
|
}
|
|
4209
5045
|
return { models };
|
|
4210
5046
|
}
|
|
5047
|
+
function discoverDeclaredModel(yDoc, modelName) {
|
|
5048
|
+
const key = `_meta_${modelName}`;
|
|
5049
|
+
if (!yDoc.share.has(key)) return void 0;
|
|
5050
|
+
const map = materializeMap(yDoc, key);
|
|
5051
|
+
return map ? readModelMeta(map) : void 0;
|
|
5052
|
+
}
|
|
4211
5053
|
function discoverModelNames(yDoc) {
|
|
4212
5054
|
const names = [];
|
|
4213
5055
|
for (const key of yDoc.share.keys()) {
|
|
@@ -4395,6 +5237,7 @@ var import_smol_toml = require("smol-toml");
|
|
|
4395
5237
|
// src/models/schema.ts
|
|
4396
5238
|
init_BaseModel();
|
|
4397
5239
|
init_ModelRegistry();
|
|
5240
|
+
init_uniqueIndex();
|
|
4398
5241
|
function defineModelSchema(input) {
|
|
4399
5242
|
const { name, fields } = input;
|
|
4400
5243
|
const options = {
|
|
@@ -4437,7 +5280,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
|
|
|
4437
5280
|
for (const [fieldName, options] of fields.entries()) {
|
|
4438
5281
|
if (options.unique) {
|
|
4439
5282
|
resolved.push({
|
|
4440
|
-
name:
|
|
5283
|
+
name: fieldUniqueConstraintName(modelName, fieldName),
|
|
4441
5284
|
fields: [fieldName]
|
|
4442
5285
|
});
|
|
4443
5286
|
}
|
|
@@ -4685,3 +5528,248 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
|
4685
5528
|
// src/cloudflare-do.ts
|
|
4686
5529
|
init_metaSync();
|
|
4687
5530
|
init_sql();
|
|
5531
|
+
init_largeDocuments();
|
|
5532
|
+
|
|
5533
|
+
// src/utils/overlaySql.ts
|
|
5534
|
+
init_sql();
|
|
5535
|
+
var DEFAULT_OVERLAY_TABLES = {
|
|
5536
|
+
records: "records",
|
|
5537
|
+
stringSetIndex: "stringset_index"
|
|
5538
|
+
};
|
|
5539
|
+
var statementCache = /* @__PURE__ */ new Map();
|
|
5540
|
+
function statementsFor(tables) {
|
|
5541
|
+
const cacheKey = `${tables.records}\0${tables.stringSetIndex}`;
|
|
5542
|
+
const cached = statementCache.get(cacheKey);
|
|
5543
|
+
if (cached) return cached;
|
|
5544
|
+
assertValidIdentifier(tables.records, "overlay records table");
|
|
5545
|
+
assertValidIdentifier(tables.stringSetIndex, "overlay stringset table");
|
|
5546
|
+
const records = tables.records;
|
|
5547
|
+
const members = tables.stringSetIndex;
|
|
5548
|
+
const statements = {
|
|
5549
|
+
deleteRecord: `DELETE FROM ${records} WHERE _type = ? AND _id = ?`,
|
|
5550
|
+
deleteAllMembers: `DELETE FROM ${members} WHERE _type = ? AND _record_id = ?`,
|
|
5551
|
+
deleteMember: `DELETE FROM ${members} WHERE _type = ? AND _record_id = ? AND field = ? AND value = ?`,
|
|
5552
|
+
insertMember: `INSERT OR IGNORE INTO ${members} (_record_id, _type, field, value) VALUES (?, ?, ?, ?)`,
|
|
5553
|
+
selectMembers: `SELECT value FROM ${members} WHERE _type = ? AND _record_id = ? AND field = ? ORDER BY rowid`,
|
|
5554
|
+
mergeUpsert: `
|
|
5555
|
+
INSERT INTO ${records} (_id, _type, _data)
|
|
5556
|
+
VALUES (?, ?, json_patch('{}', ?))
|
|
5557
|
+
ON CONFLICT(_type, _id) DO UPDATE SET
|
|
5558
|
+
_data = json_patch(${records}._data, ?)`.trim(),
|
|
5559
|
+
replaceUpsert: `
|
|
5560
|
+
INSERT INTO ${records} (_id, _type, _data)
|
|
5561
|
+
VALUES (?, ?, json_patch('{}', ?))
|
|
5562
|
+
ON CONFLICT(_type, _id) DO UPDATE SET
|
|
5563
|
+
_data = json_patch('{}', ?)`.trim()
|
|
5564
|
+
};
|
|
5565
|
+
statementCache.set(cacheKey, statements);
|
|
5566
|
+
return statements;
|
|
5567
|
+
}
|
|
5568
|
+
function projectOverlayEntry(sql, modelName, entry, tables = DEFAULT_OVERLAY_TABLES) {
|
|
5569
|
+
const id = entry.id;
|
|
5570
|
+
const {
|
|
5571
|
+
deleteRecord: DELETE_RECORD,
|
|
5572
|
+
deleteAllMembers: DELETE_ALL_MEMBERS,
|
|
5573
|
+
deleteMember: DELETE_MEMBER,
|
|
5574
|
+
insertMember: INSERT_MEMBER,
|
|
5575
|
+
selectMembers: SELECT_MEMBERS,
|
|
5576
|
+
mergeUpsert: MERGE_UPSERT,
|
|
5577
|
+
replaceUpsert: REPLACE_UPSERT
|
|
5578
|
+
} = statementsFor(tables);
|
|
5579
|
+
if (entry.deleted) {
|
|
5580
|
+
sql.exec(DELETE_RECORD, modelName, id);
|
|
5581
|
+
sql.exec(DELETE_ALL_MEMBERS, modelName, id);
|
|
5582
|
+
return;
|
|
5583
|
+
}
|
|
5584
|
+
const touchedFields = Object.keys(entry.fields);
|
|
5585
|
+
const touchedSets = Object.keys(entry.stringSets);
|
|
5586
|
+
if (!entry.replace && touchedFields.length === 0 && touchedSets.length === 0) {
|
|
5587
|
+
return;
|
|
5588
|
+
}
|
|
5589
|
+
if (entry.replace) {
|
|
5590
|
+
sql.exec(DELETE_ALL_MEMBERS, modelName, id);
|
|
5591
|
+
}
|
|
5592
|
+
const patch = { ...entry.fields, id };
|
|
5593
|
+
for (const [field, members] of Object.entries(entry.stringSets)) {
|
|
5594
|
+
for (const [member, present] of Object.entries(members)) {
|
|
5595
|
+
if (present) sql.exec(INSERT_MEMBER, id, modelName, field, member);
|
|
5596
|
+
else sql.exec(DELETE_MEMBER, modelName, id, field, member);
|
|
5597
|
+
}
|
|
5598
|
+
const rows = Array.from(sql.exec(SELECT_MEMBERS, modelName, id, field));
|
|
5599
|
+
patch[field] = rows.map((row) => row.value);
|
|
5600
|
+
}
|
|
5601
|
+
const patchJson = JSON.stringify(patch);
|
|
5602
|
+
sql.exec(
|
|
5603
|
+
entry.replace ? REPLACE_UPSERT : MERGE_UPSERT,
|
|
5604
|
+
id,
|
|
5605
|
+
modelName,
|
|
5606
|
+
patchJson,
|
|
5607
|
+
patchJson
|
|
5608
|
+
);
|
|
5609
|
+
}
|
|
5610
|
+
|
|
5611
|
+
// src/utils/snapshotChunks.ts
|
|
5612
|
+
var SnapshotChunkIntegrityError = class extends Error {
|
|
5613
|
+
constructor(key, reason) {
|
|
5614
|
+
super(`Snapshot chunk ${key} does not match its manifest entry: ${reason}`);
|
|
5615
|
+
this.key = key;
|
|
5616
|
+
this.name = "SnapshotChunkIntegrityError";
|
|
5617
|
+
}
|
|
5618
|
+
};
|
|
5619
|
+
var SnapshotRowFormatError = class extends Error {
|
|
5620
|
+
constructor(model, id) {
|
|
5621
|
+
super(
|
|
5622
|
+
`Record ${model}/${id} cannot be written to a snapshot chunk: its id or data contains a newline or tab, which the line format reserves`
|
|
5623
|
+
);
|
|
5624
|
+
this.name = "SnapshotRowFormatError";
|
|
5625
|
+
}
|
|
5626
|
+
};
|
|
5627
|
+
function snapshotStringSetFields(manifest, model) {
|
|
5628
|
+
const declared = manifest.schema?.[model]?.stringSetFields;
|
|
5629
|
+
return new Set(Array.isArray(declared) ? declared.map(String) : []);
|
|
5630
|
+
}
|
|
5631
|
+
function snapshotDeclaredSchema(manifest, model) {
|
|
5632
|
+
const declared = manifest.schema?.[model] ?? {};
|
|
5633
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
5634
|
+
const declaration = declared.declaration && typeof declared.declaration === "object" ? {
|
|
5635
|
+
fields: declared.declaration.fields ?? {},
|
|
5636
|
+
constraints: declared.declaration.constraints ?? {}
|
|
5637
|
+
} : void 0;
|
|
5638
|
+
return {
|
|
5639
|
+
...declaration ? { declaration } : {},
|
|
5640
|
+
stringSetFields: list(declared.stringSetFields).map(String),
|
|
5641
|
+
uniqueConstraints: list(declared.uniqueConstraints).filter(
|
|
5642
|
+
(constraint) => constraint?.name && Array.isArray(constraint.fields) && constraint.fields.length > 0
|
|
5643
|
+
).map((constraint) => ({
|
|
5644
|
+
name: String(constraint.name),
|
|
5645
|
+
fields: constraint.fields.map(String)
|
|
5646
|
+
})),
|
|
5647
|
+
indexedFields: list(declared.indexedFields).filter((index) => index?.field).map((index) => ({
|
|
5648
|
+
field: String(index.field),
|
|
5649
|
+
type: String(index.type || "string"),
|
|
5650
|
+
unique: !!index.unique
|
|
5651
|
+
}))
|
|
5652
|
+
};
|
|
5653
|
+
}
|
|
5654
|
+
function snapshotRowAsOverlayEntry(id, data, stringSetFields) {
|
|
5655
|
+
const fields = {};
|
|
5656
|
+
const stringSets = {};
|
|
5657
|
+
for (const [field, value] of Object.entries(data)) {
|
|
5658
|
+
if (field === "id") continue;
|
|
5659
|
+
if (stringSetFields.has(field) && Array.isArray(value)) {
|
|
5660
|
+
const members = {};
|
|
5661
|
+
for (const member of value) members[String(member)] = true;
|
|
5662
|
+
stringSets[field] = members;
|
|
5663
|
+
continue;
|
|
5664
|
+
}
|
|
5665
|
+
fields[field] = value;
|
|
5666
|
+
}
|
|
5667
|
+
return { id, fields, stringSets, replace: true, deleted: false };
|
|
5668
|
+
}
|
|
5669
|
+
var encoder = new TextEncoder();
|
|
5670
|
+
var decoder = new TextDecoder();
|
|
5671
|
+
function assertLineSafe(row) {
|
|
5672
|
+
if (/[\n\r\t]/.test(row._id) || /[\n\r\t]/.test(row._data)) {
|
|
5673
|
+
throw new SnapshotRowFormatError(row._type, row._id);
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
function snapshotChunkLines(rows) {
|
|
5677
|
+
let body = "";
|
|
5678
|
+
for (const row of rows) {
|
|
5679
|
+
assertLineSafe(row);
|
|
5680
|
+
body += `${row._id} ${row._data}
|
|
5681
|
+
`;
|
|
5682
|
+
}
|
|
5683
|
+
return body;
|
|
5684
|
+
}
|
|
5685
|
+
async function gzip(bytes) {
|
|
5686
|
+
return await collect(
|
|
5687
|
+
new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"))
|
|
5688
|
+
);
|
|
5689
|
+
}
|
|
5690
|
+
async function gunzip(bytes) {
|
|
5691
|
+
return await collect(
|
|
5692
|
+
new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"))
|
|
5693
|
+
);
|
|
5694
|
+
}
|
|
5695
|
+
async function collect(stream) {
|
|
5696
|
+
const parts = [];
|
|
5697
|
+
let total = 0;
|
|
5698
|
+
const reader = stream.getReader();
|
|
5699
|
+
for (; ; ) {
|
|
5700
|
+
const { done, value } = await reader.read();
|
|
5701
|
+
if (done) break;
|
|
5702
|
+
parts.push(value);
|
|
5703
|
+
total += value.byteLength;
|
|
5704
|
+
}
|
|
5705
|
+
const out = new Uint8Array(total);
|
|
5706
|
+
let at = 0;
|
|
5707
|
+
for (const part of parts) {
|
|
5708
|
+
out.set(part, at);
|
|
5709
|
+
at += part.byteLength;
|
|
5710
|
+
}
|
|
5711
|
+
return out;
|
|
5712
|
+
}
|
|
5713
|
+
async function sha256Hex(bytes) {
|
|
5714
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
5715
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5716
|
+
}
|
|
5717
|
+
async function encodeSnapshotChunk(input) {
|
|
5718
|
+
const lines = encoder.encode(snapshotChunkLines(input.rows));
|
|
5719
|
+
const body = await gzip(lines);
|
|
5720
|
+
return {
|
|
5721
|
+
body,
|
|
5722
|
+
entry: {
|
|
5723
|
+
key: input.key,
|
|
5724
|
+
model: input.model,
|
|
5725
|
+
bytes: body.byteLength,
|
|
5726
|
+
rawBytes: lines.byteLength,
|
|
5727
|
+
rows: input.rows.length,
|
|
5728
|
+
firstId: input.rows.length > 0 ? input.rows[0]._id : "",
|
|
5729
|
+
lastId: input.rows.length > 0 ? input.rows[input.rows.length - 1]._id : "",
|
|
5730
|
+
sha256: await sha256Hex(body)
|
|
5731
|
+
}
|
|
5732
|
+
};
|
|
5733
|
+
}
|
|
5734
|
+
async function readSnapshotChunk(body, descriptor) {
|
|
5735
|
+
const entry = "sha256" in descriptor ? descriptor : null;
|
|
5736
|
+
if (entry) {
|
|
5737
|
+
if (body.byteLength !== entry.bytes) {
|
|
5738
|
+
throw new SnapshotChunkIntegrityError(
|
|
5739
|
+
entry.key,
|
|
5740
|
+
`expected ${entry.bytes} bytes, got ${body.byteLength}`
|
|
5741
|
+
);
|
|
5742
|
+
}
|
|
5743
|
+
const digest = await sha256Hex(body);
|
|
5744
|
+
if (digest !== entry.sha256) {
|
|
5745
|
+
throw new SnapshotChunkIntegrityError(
|
|
5746
|
+
entry.key,
|
|
5747
|
+
`expected sha256 ${entry.sha256}, got ${digest}`
|
|
5748
|
+
);
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
const text = decoder.decode(await gunzip(body));
|
|
5752
|
+
const rows = [];
|
|
5753
|
+
for (const line of text.split("\n")) {
|
|
5754
|
+
if (line.length === 0) continue;
|
|
5755
|
+
const tab = line.indexOf(" ");
|
|
5756
|
+
if (tab < 0) {
|
|
5757
|
+
throw new SnapshotChunkIntegrityError(
|
|
5758
|
+
entry?.key ?? descriptor.model,
|
|
5759
|
+
"a line carries no id/data separator"
|
|
5760
|
+
);
|
|
5761
|
+
}
|
|
5762
|
+
rows.push({
|
|
5763
|
+
_type: descriptor.model,
|
|
5764
|
+
_id: line.slice(0, tab),
|
|
5765
|
+
_data: line.slice(tab + 1)
|
|
5766
|
+
});
|
|
5767
|
+
}
|
|
5768
|
+
if (entry && rows.length !== entry.rows) {
|
|
5769
|
+
throw new SnapshotChunkIntegrityError(
|
|
5770
|
+
entry.key,
|
|
5771
|
+
`expected ${entry.rows} rows, got ${rows.length}`
|
|
5772
|
+
);
|
|
5773
|
+
}
|
|
5774
|
+
return rows;
|
|
5775
|
+
}
|