@haverstack/record-adapter-do-sqlite 0.14.0 → 0.15.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/dist/index.js +176 -197
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StackNotFoundError, StackConflictError, applyMergePatch, StackVersionConflictError, StackQueryError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
2
2
|
import { contentSortEntry, parseContentFilterKey, contentSortKey } from '@haverstack/core/adapter';
|
|
3
3
|
|
|
4
4
|
// ../sqlite-shared/dist/schema.js
|
|
@@ -132,7 +132,7 @@ var FTS5_SCHEMA_SQL = `
|
|
|
132
132
|
);
|
|
133
133
|
`;
|
|
134
134
|
var PRAGMA_FOREIGN_KEYS_ON = `PRAGMA foreign_keys = ON;`;
|
|
135
|
-
var SORT_FIELDS =
|
|
135
|
+
var SORT_FIELDS = NATIVE_SORT_FIELDS;
|
|
136
136
|
var toBase64 = (text) => {
|
|
137
137
|
const bytes = new TextEncoder().encode(text);
|
|
138
138
|
let binary = "";
|
|
@@ -348,17 +348,30 @@ var recordConditions = (query) => {
|
|
|
348
348
|
const conditions = ["r.id != '_config'"];
|
|
349
349
|
const params = [];
|
|
350
350
|
const f = query.filter ?? {};
|
|
351
|
+
const inFilter = (column, value) => {
|
|
352
|
+
if (value === void 0)
|
|
353
|
+
return;
|
|
354
|
+
const values = Array.isArray(value) ? value : [value];
|
|
355
|
+
conditions.push(`r.${column} IN (${values.map(() => "?").join(",")})`);
|
|
356
|
+
params.push(...values);
|
|
357
|
+
};
|
|
358
|
+
const rangeFilter = (column, range) => {
|
|
359
|
+
if (range?.after) {
|
|
360
|
+
conditions.push(`r.${column} > ?`);
|
|
361
|
+
params.push(range.after.getTime());
|
|
362
|
+
}
|
|
363
|
+
if (range?.before) {
|
|
364
|
+
conditions.push(`r.${column} < ?`);
|
|
365
|
+
params.push(range.before.getTime());
|
|
366
|
+
}
|
|
367
|
+
};
|
|
351
368
|
if (!f.includeDeleted) {
|
|
352
369
|
conditions.push("r.deleted_at IS NULL");
|
|
353
370
|
}
|
|
354
371
|
if (!f.includeUnlisted) {
|
|
355
372
|
conditions.push("r.unlisted_at IS NULL");
|
|
356
373
|
}
|
|
357
|
-
|
|
358
|
-
const ids = Array.isArray(f.typeId) ? f.typeId : [f.typeId];
|
|
359
|
-
conditions.push(`r.type_id IN (${ids.map(() => "?").join(",")})`);
|
|
360
|
-
params.push(...ids);
|
|
361
|
-
}
|
|
374
|
+
inFilter("type_id", f.typeId);
|
|
362
375
|
if (f.parentId !== void 0) {
|
|
363
376
|
if (f.parentId === null) {
|
|
364
377
|
conditions.push("r.parent_id IS NULL");
|
|
@@ -367,49 +380,24 @@ var recordConditions = (query) => {
|
|
|
367
380
|
params.push(f.parentId);
|
|
368
381
|
}
|
|
369
382
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const ids = Array.isArray(f.entityId) ? f.entityId : [f.entityId];
|
|
377
|
-
conditions.push(`r.entity_id IN (${ids.map(() => "?").join(",")})`);
|
|
378
|
-
params.push(...ids);
|
|
379
|
-
}
|
|
380
|
-
if (f.principalId !== void 0) {
|
|
381
|
-
const ids = Array.isArray(f.principalId) ? f.principalId : [f.principalId];
|
|
382
|
-
conditions.push(`r.principal_id IN (${ids.map(() => "?").join(",")})`);
|
|
383
|
-
params.push(...ids);
|
|
384
|
-
}
|
|
385
|
-
if (f.createdAt?.after) {
|
|
386
|
-
conditions.push("r.created_at > ?");
|
|
387
|
-
params.push(f.createdAt.after.getTime());
|
|
388
|
-
}
|
|
389
|
-
if (f.createdAt?.before) {
|
|
390
|
-
conditions.push("r.created_at < ?");
|
|
391
|
-
params.push(f.createdAt.before.getTime());
|
|
392
|
-
}
|
|
393
|
-
if (f.updatedAt?.after) {
|
|
394
|
-
conditions.push("r.updated_at > ?");
|
|
395
|
-
params.push(f.updatedAt.after.getTime());
|
|
396
|
-
}
|
|
397
|
-
if (f.updatedAt?.before) {
|
|
398
|
-
conditions.push("r.updated_at < ?");
|
|
399
|
-
params.push(f.updatedAt.before.getTime());
|
|
400
|
-
}
|
|
383
|
+
inFilter("app_id", f.appId);
|
|
384
|
+
inFilter("entity_id", f.entityId);
|
|
385
|
+
inFilter("principal_id", f.principalId);
|
|
386
|
+
rangeFilter("created_at", f.createdAt);
|
|
387
|
+
rangeFilter("updated_at", f.updatedAt);
|
|
388
|
+
const hasAssociation = (kind, clauses) => `r.id IN (SELECT a.record_id FROM associations a WHERE a.kind = '${kind}'` + clauses.map((c) => ` AND ${c}`).join("") + `)`;
|
|
401
389
|
if (f.tags?.length) {
|
|
402
390
|
for (const tag of f.tags) {
|
|
403
|
-
conditions.push(
|
|
391
|
+
conditions.push(hasAssociation("tag", ["a.label = ?"]));
|
|
404
392
|
params.push(tag);
|
|
405
393
|
}
|
|
406
394
|
}
|
|
407
395
|
if (f.hasAttachment) {
|
|
408
|
-
conditions.push(
|
|
396
|
+
conditions.push(hasAssociation("attachment", ["a.label = ?"]));
|
|
409
397
|
params.push(f.hasAttachment);
|
|
410
398
|
}
|
|
411
399
|
if (f.attachmentFileId) {
|
|
412
|
-
conditions.push(`(
|
|
400
|
+
conditions.push(`(${hasAssociation("attachment", ["a.file_id = ?"])}
|
|
413
401
|
OR r.id IN (SELECT ci.record_id FROM content_index ci WHERE ci.file_id = ?))`);
|
|
414
402
|
params.push(f.attachmentFileId, f.attachmentFileId);
|
|
415
403
|
}
|
|
@@ -438,7 +426,7 @@ var recordConditions = (query) => {
|
|
|
438
426
|
}
|
|
439
427
|
}
|
|
440
428
|
}
|
|
441
|
-
conditions.push(
|
|
429
|
+
conditions.push(hasAssociation("relationship", clauses));
|
|
442
430
|
}
|
|
443
431
|
if (f.content) {
|
|
444
432
|
let aliasSeq = 0;
|
|
@@ -664,6 +652,29 @@ var asStackQueryError = (query, run) => {
|
|
|
664
652
|
throw new StackQueryError(`Search text could not be parsed: ${err instanceof Error ? err.message : String(err)}`);
|
|
665
653
|
}
|
|
666
654
|
};
|
|
655
|
+
var versionConflict = (id, expected, actual) => new StackVersionConflictError(`Record "${id}" is at version ${actual}, expected ${expected}`, id, expected, actual);
|
|
656
|
+
var VERSION_COLUMNS = [
|
|
657
|
+
"type_id",
|
|
658
|
+
"content",
|
|
659
|
+
"updated_at",
|
|
660
|
+
"entity_id",
|
|
661
|
+
"updated_by",
|
|
662
|
+
"updated_via",
|
|
663
|
+
"parent_id",
|
|
664
|
+
"associations",
|
|
665
|
+
"permissions"
|
|
666
|
+
];
|
|
667
|
+
var versionRowValues = (version) => [
|
|
668
|
+
version.typeId,
|
|
669
|
+
JSON.stringify(version.content),
|
|
670
|
+
toMs(version.updatedAt),
|
|
671
|
+
version.entityId ?? null,
|
|
672
|
+
version.updatedBy ?? null,
|
|
673
|
+
version.updatedVia ?? null,
|
|
674
|
+
version.parentId ?? null,
|
|
675
|
+
version.associations ? JSON.stringify(version.associations) : null,
|
|
676
|
+
version.permissions ? JSON.stringify(version.permissions) : null
|
|
677
|
+
];
|
|
667
678
|
var indexedFieldKinds = (schema) => {
|
|
668
679
|
const kinds = /* @__PURE__ */ new Map();
|
|
669
680
|
for (const [field, def] of Object.entries(schema)) {
|
|
@@ -689,26 +700,22 @@ var SharedSqlRecordLogic = class {
|
|
|
689
700
|
return this.deps.exec;
|
|
690
701
|
}
|
|
691
702
|
/**
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
* Precondition check for mutations that can't fold expectedVersion into
|
|
701
|
-
* their primary UPDATE's WHERE clause: mutateRecord/restoreVersion need
|
|
702
|
-
* fts.remove() to run *before* the records-table content changes, so
|
|
703
|
-
* the check happens first, standalone.
|
|
703
|
+
* Precondition check for a mutation that has already read the record —
|
|
704
|
+
* either because it can't fold the check into its own statement
|
|
705
|
+
* (hardDeleteRecord: children reference the row, so the DELETE can't
|
|
706
|
+
* carry it), or because it has to settle the precondition *before*
|
|
707
|
+
* fts5Strategy.remove() runs, which needs the records row still holding
|
|
708
|
+
* the old content. In the latter case versionedUpdate() re-checks it
|
|
709
|
+
* inside the statement, and that is the enforcement; this is what lets
|
|
710
|
+
* the work before it be skipped.
|
|
704
711
|
*/
|
|
705
712
|
checkExpectedVersion(record, expectedVersion) {
|
|
706
713
|
if (expectedVersion === void 0 || record.version === expectedVersion)
|
|
707
714
|
return;
|
|
708
|
-
throw
|
|
715
|
+
throw versionConflict(record.id, expectedVersion, record.version);
|
|
709
716
|
}
|
|
710
717
|
/**
|
|
711
|
-
* Called after a
|
|
718
|
+
* Called after a version-gated statement affected zero rows.
|
|
712
719
|
* Distinguishes "record doesn't exist" (StackNotFoundError) from "it
|
|
713
720
|
* exists but isn't at expectedVersion" (StackVersionConflictError, with
|
|
714
721
|
* the actual current version for the caller to act on).
|
|
@@ -719,7 +726,48 @@ var SharedSqlRecordLogic = class {
|
|
|
719
726
|
]);
|
|
720
727
|
if (!row)
|
|
721
728
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
722
|
-
throw
|
|
729
|
+
throw versionConflict(id, expectedVersion, row.version);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* The one shape every mutating UPDATE in this class has: whatever columns
|
|
733
|
+
* the caller is changing, then the version bump and the actor/timestamp
|
|
734
|
+
* stamp that every mutation owes, gated on the opt-in `expectedVersion`
|
|
735
|
+
* precondition and failing loudly when nothing matched.
|
|
736
|
+
*
|
|
737
|
+
* The guard rides in the WHERE clause even where the caller has already
|
|
738
|
+
* checked the version against a record it read: that read is outside this
|
|
739
|
+
* statement, so a writer slipping in between the two is caught here
|
|
740
|
+
* rather than silently overwritten.
|
|
741
|
+
*/
|
|
742
|
+
versionedUpdate(id, sets, values, opts, now = toMs(/* @__PURE__ */ new Date())) {
|
|
743
|
+
const guarded = opts.expectedVersion !== void 0;
|
|
744
|
+
const changed = this.exec.run(`UPDATE records SET ${[...sets, "version = version + 1", "updated_at = ?", "updated_by = ?", "updated_via = ?"].join(", ")} WHERE id = ?${guarded ? " AND version = ?" : ""}`, [
|
|
745
|
+
...values,
|
|
746
|
+
now,
|
|
747
|
+
opts.updatedBy ?? null,
|
|
748
|
+
opts.updatedVia ?? null,
|
|
749
|
+
id,
|
|
750
|
+
...guarded ? [opts.expectedVersion] : []
|
|
751
|
+
]);
|
|
752
|
+
if (changed === 0)
|
|
753
|
+
this.throwVersionConflict(id, opts.expectedVersion);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Read a record back after its own mutation committed, for the caller to
|
|
757
|
+
* return. Its absence is not a "not found" for the caller to handle — the
|
|
758
|
+
* mutation just succeeded against it — so it fails as the invariant
|
|
759
|
+
* breach it would be.
|
|
760
|
+
*
|
|
761
|
+
* Synchronous, via readRecord() rather than getRecord(), so the read
|
|
762
|
+
* cannot be separated from the commit by anything: an async caller
|
|
763
|
+
* returning this still hands back a record read in the same tick its
|
|
764
|
+
* transaction committed. See readRecord() for why that matters.
|
|
765
|
+
*/
|
|
766
|
+
reread(id, verb) {
|
|
767
|
+
const updated = this.readRecord(id);
|
|
768
|
+
if (!updated)
|
|
769
|
+
throw new Error(`Record not found after ${verb}: "${id}"`);
|
|
770
|
+
return updated;
|
|
723
771
|
}
|
|
724
772
|
// -------------------------------------------------------
|
|
725
773
|
// Records
|
|
@@ -776,11 +824,11 @@ var SharedSqlRecordLogic = class {
|
|
|
776
824
|
* reason (see SqlExecutor.transaction).
|
|
777
825
|
*
|
|
778
826
|
* The same synchrony is what lets the post-commit reads below report the
|
|
779
|
-
* version their own mutation produced:
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
* that window, and a concurrent mutation could be
|
|
783
|
-
* one's verb.
|
|
827
|
+
* version their own mutation produced: reread() calls this directly, so
|
|
828
|
+
* nothing interleaves between the commit and the read even though the
|
|
829
|
+
* method returning it is async. A backend that made these reads genuinely
|
|
830
|
+
* asynchronous would open that window, and a concurrent mutation could be
|
|
831
|
+
* reported under this one's verb.
|
|
784
832
|
*/
|
|
785
833
|
readRecord(id) {
|
|
786
834
|
const row = this.exec.get("SELECT * FROM records WHERE id = ?", [id]);
|
|
@@ -831,49 +879,28 @@ var SharedSqlRecordLogic = class {
|
|
|
831
879
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
832
880
|
if (merged !== void 0)
|
|
833
881
|
fts5Strategy.remove(this.exec, id);
|
|
834
|
-
|
|
835
|
-
const changed = this.exec.run(`UPDATE records SET ${sets.concat(["version = version + 1", "updated_at = ?", "updated_by = ?", "updated_via = ?"]).join(", ")} WHERE id = ?${clause}`, [...values, now, opts.updatedBy ?? null, opts.updatedVia ?? null, id, ...verParams]);
|
|
836
|
-
if (changed === 0)
|
|
837
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
882
|
+
this.versionedUpdate(id, sets, values, opts, now);
|
|
838
883
|
if (changes.associations !== void 0) {
|
|
839
|
-
this.
|
|
840
|
-
if (changes.associations.length)
|
|
841
|
-
this.insertAssociations(id, changes.associations);
|
|
884
|
+
this.replaceAssociations(id, changes.associations);
|
|
842
885
|
}
|
|
843
886
|
if (merged !== void 0) {
|
|
844
887
|
fts5Strategy.insert(this.exec, id, JSON.stringify(merged));
|
|
845
888
|
this.syncContentIndex(id, existing.typeId, merged);
|
|
846
889
|
}
|
|
847
890
|
});
|
|
848
|
-
|
|
849
|
-
if (!updated)
|
|
850
|
-
throw new Error(`Record not found after mutateRecord: "${id}"`);
|
|
851
|
-
return updated;
|
|
891
|
+
return this.reread(id, "mutateRecord");
|
|
852
892
|
}
|
|
853
893
|
async deleteRecord(id, opts = {}) {
|
|
854
894
|
if (opts.hard) {
|
|
855
895
|
return this.exec.transaction(() => this.hardDeleteRecord(id, opts.expectedVersion));
|
|
856
|
-
} else {
|
|
857
|
-
this.exec.transaction(() => {
|
|
858
|
-
if (opts.snapshot)
|
|
859
|
-
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
860
|
-
const { clause, params: verParams } = this.versionGuard(opts.expectedVersion);
|
|
861
|
-
const changed = this.exec.run(`UPDATE records SET deleted_at = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?${clause}`, [
|
|
862
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
863
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
864
|
-
opts.updatedBy ?? null,
|
|
865
|
-
opts.updatedVia ?? null,
|
|
866
|
-
id,
|
|
867
|
-
...verParams
|
|
868
|
-
]);
|
|
869
|
-
if (changed === 0)
|
|
870
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
871
|
-
});
|
|
872
896
|
}
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
897
|
+
const now = toMs(/* @__PURE__ */ new Date());
|
|
898
|
+
this.exec.transaction(() => {
|
|
899
|
+
if (opts.snapshot)
|
|
900
|
+
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
901
|
+
this.versionedUpdate(id, ["deleted_at = ?"], [now], opts, now);
|
|
902
|
+
});
|
|
903
|
+
return this.reread(id, "deleteRecord");
|
|
877
904
|
}
|
|
878
905
|
/**
|
|
879
906
|
* Deletes a record's FTS entry, associations, versions, file-refs, sort
|
|
@@ -886,15 +913,12 @@ var SharedSqlRecordLogic = class {
|
|
|
886
913
|
*/
|
|
887
914
|
hardDeleteRecord(id, expectedVersion) {
|
|
888
915
|
const purged = this.readRecord(id);
|
|
889
|
-
if (
|
|
890
|
-
if (
|
|
916
|
+
if (!purged) {
|
|
917
|
+
if (expectedVersion !== void 0)
|
|
891
918
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
892
|
-
if (purged.version !== expectedVersion) {
|
|
893
|
-
throw new StackVersionConflictError(`Record "${id}" is at version ${purged.version}, expected ${expectedVersion}`, id, expectedVersion, purged.version);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
if (!purged)
|
|
897
919
|
return null;
|
|
920
|
+
}
|
|
921
|
+
this.checkExpectedVersion(purged, expectedVersion);
|
|
898
922
|
fts5Strategy.remove(this.exec, id);
|
|
899
923
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
900
924
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
@@ -906,15 +930,9 @@ var SharedSqlRecordLogic = class {
|
|
|
906
930
|
this.exec.transaction(() => {
|
|
907
931
|
if (opts.snapshot)
|
|
908
932
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
909
|
-
|
|
910
|
-
const changed = this.exec.run(`UPDATE records SET deleted_at = NULL, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?${clause}`, [toMs(/* @__PURE__ */ new Date()), opts.updatedBy ?? null, opts.updatedVia ?? null, id, ...verParams]);
|
|
911
|
-
if (changed === 0)
|
|
912
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
933
|
+
this.versionedUpdate(id, ["deleted_at = NULL"], [], opts);
|
|
913
934
|
});
|
|
914
|
-
|
|
915
|
-
if (!updated)
|
|
916
|
-
throw new Error(`Record not found after undelete: "${id}"`);
|
|
917
|
-
return updated;
|
|
935
|
+
return this.reread(id, "undelete");
|
|
918
936
|
}
|
|
919
937
|
async restoreVersion(id, version, opts = {}) {
|
|
920
938
|
const existing = await this.getRecord(id);
|
|
@@ -927,29 +945,12 @@ var SharedSqlRecordLogic = class {
|
|
|
927
945
|
this.exec.transaction(() => {
|
|
928
946
|
if (opts.snapshot)
|
|
929
947
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
930
|
-
|
|
931
|
-
this.exec.run(`UPDATE records SET type_id = ?, content = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ?, parent_id = ? WHERE id = ?`, [
|
|
932
|
-
target.typeId,
|
|
933
|
-
JSON.stringify(target.content),
|
|
934
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
935
|
-
opts.updatedBy ?? null,
|
|
936
|
-
opts.updatedVia ?? null,
|
|
937
|
-
target.parentId ?? null,
|
|
938
|
-
id
|
|
939
|
-
]);
|
|
948
|
+
this.rewriteContent(id, target.typeId, target.content, opts, ["parent_id = ?"], [target.parentId ?? null]);
|
|
940
949
|
const applied = opts.restoreAssociations === false ? void 0 : target.associations;
|
|
941
|
-
if (applied !== void 0)
|
|
942
|
-
this.
|
|
943
|
-
if (applied.length)
|
|
944
|
-
this.insertAssociations(id, applied);
|
|
945
|
-
}
|
|
946
|
-
fts5Strategy.insert(this.exec, id, JSON.stringify(target.content));
|
|
947
|
-
this.syncContentIndex(id, target.typeId, target.content);
|
|
950
|
+
if (applied !== void 0)
|
|
951
|
+
this.replaceAssociations(id, applied);
|
|
948
952
|
});
|
|
949
|
-
|
|
950
|
-
if (!updated)
|
|
951
|
-
throw new Error(`Record not found after restoreVersion: "${id}"`);
|
|
952
|
-
return updated;
|
|
953
|
+
return this.reread(id, "restoreVersion");
|
|
953
954
|
}
|
|
954
955
|
async commitMigration(id, toTypeId, content, opts = {}) {
|
|
955
956
|
const existing = await this.getRecord(id);
|
|
@@ -959,22 +960,27 @@ var SharedSqlRecordLogic = class {
|
|
|
959
960
|
this.exec.transaction(() => {
|
|
960
961
|
if (opts.snapshot)
|
|
961
962
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
962
|
-
|
|
963
|
-
this.exec.run(`UPDATE records SET type_id = ?, content = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?`, [
|
|
964
|
-
toTypeId,
|
|
965
|
-
JSON.stringify(content),
|
|
966
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
967
|
-
opts.updatedBy ?? null,
|
|
968
|
-
opts.updatedVia ?? null,
|
|
969
|
-
id
|
|
970
|
-
]);
|
|
971
|
-
fts5Strategy.insert(this.exec, id, JSON.stringify(content));
|
|
972
|
-
this.syncContentIndex(id, toTypeId, content);
|
|
963
|
+
this.rewriteContent(id, toTypeId, content, opts);
|
|
973
964
|
});
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
965
|
+
return this.reread(id, "commitMigration");
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Replace a record's type and content wholesale — what restoreVersion and
|
|
969
|
+
* commitMigration each do, differing only in where the content came from
|
|
970
|
+
* and whether the container moves with it.
|
|
971
|
+
*
|
|
972
|
+
* The order is the constraint: fts5Strategy.remove() has to read the old
|
|
973
|
+
* content off the records row, so it runs before the UPDATE, and the
|
|
974
|
+
* re-index runs after. Every caller must already sit inside
|
|
975
|
+
* exec.transaction() — a failure between the two halves would otherwise
|
|
976
|
+
* leave the record unindexed.
|
|
977
|
+
*/
|
|
978
|
+
rewriteContent(id, typeId, content, opts, extraSets = [], extraValues = []) {
|
|
979
|
+
const json = JSON.stringify(content);
|
|
980
|
+
fts5Strategy.remove(this.exec, id);
|
|
981
|
+
this.versionedUpdate(id, ["type_id = ?", "content = ?", ...extraSets], [typeId, json, ...extraValues], opts);
|
|
982
|
+
fts5Strategy.insert(this.exec, id, json);
|
|
983
|
+
this.syncContentIndex(id, typeId, content);
|
|
978
984
|
}
|
|
979
985
|
async queryRecords(query) {
|
|
980
986
|
const limit = query.limit ?? 50;
|
|
@@ -1035,8 +1041,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1035
1041
|
return rows.map(rowToVersion);
|
|
1036
1042
|
}
|
|
1037
1043
|
async getVersion(id, version) {
|
|
1038
|
-
const
|
|
1039
|
-
return
|
|
1044
|
+
const row = this.exec.get("SELECT * FROM versions WHERE record_id = ? AND version = ?", [id, version]);
|
|
1045
|
+
return row ? rowToVersion(row) : null;
|
|
1040
1046
|
}
|
|
1041
1047
|
/**
|
|
1042
1048
|
* A (record_id, version) collision is rejected loudly via the UNIQUE
|
|
@@ -1047,21 +1053,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1047
1053
|
insertVersionRow(id, version) {
|
|
1048
1054
|
try {
|
|
1049
1055
|
this.exec.run(`INSERT INTO versions
|
|
1050
|
-
(record_id, version,
|
|
1051
|
-
|
|
1052
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1053
|
-
id,
|
|
1054
|
-
version.version,
|
|
1055
|
-
version.typeId,
|
|
1056
|
-
JSON.stringify(version.content),
|
|
1057
|
-
toMs(version.updatedAt),
|
|
1058
|
-
version.entityId ?? null,
|
|
1059
|
-
version.updatedBy ?? null,
|
|
1060
|
-
version.updatedVia ?? null,
|
|
1061
|
-
version.parentId ?? null,
|
|
1062
|
-
version.associations ? JSON.stringify(version.associations) : null,
|
|
1063
|
-
version.permissions ? JSON.stringify(version.permissions) : null
|
|
1064
|
-
]);
|
|
1056
|
+
(record_id, version, ${VERSION_COLUMNS.join(", ")})
|
|
1057
|
+
VALUES (?, ?, ${VERSION_COLUMNS.map(() => "?").join(", ")})`, [id, version.version, ...versionRowValues(version)]);
|
|
1065
1058
|
} catch (err) {
|
|
1066
1059
|
if (isUniqueConstraintViolation(err)) {
|
|
1067
1060
|
throw new StackConflictError(`Version ${version.version} already exists for record "${id}" \u2014 a concurrent writer raced past this version. Use ifVersion to detect this before it happens.`);
|
|
@@ -1070,28 +1063,15 @@ var SharedSqlRecordLogic = class {
|
|
|
1070
1063
|
}
|
|
1071
1064
|
}
|
|
1072
1065
|
/**
|
|
1073
|
-
* Replaces every column insertVersionRow writes
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1066
|
+
* Replaces every column insertVersionRow writes — structurally, off the
|
|
1067
|
+
* same VERSION_COLUMNS list, because a column left out here would keep
|
|
1068
|
+
* the replaced row's value and surface later as a restore putting back
|
|
1069
|
+
* something the snapshot never said.
|
|
1077
1070
|
*/
|
|
1078
1071
|
overwriteVersionRow(id, version) {
|
|
1079
1072
|
this.exec.run(`UPDATE versions
|
|
1080
|
-
SET
|
|
1081
|
-
|
|
1082
|
-
WHERE record_id = ? AND version = ?`, [
|
|
1083
|
-
version.typeId,
|
|
1084
|
-
JSON.stringify(version.content),
|
|
1085
|
-
toMs(version.updatedAt),
|
|
1086
|
-
version.entityId ?? null,
|
|
1087
|
-
version.updatedBy ?? null,
|
|
1088
|
-
version.updatedVia ?? null,
|
|
1089
|
-
version.parentId ?? null,
|
|
1090
|
-
version.associations ? JSON.stringify(version.associations) : null,
|
|
1091
|
-
version.permissions ? JSON.stringify(version.permissions) : null,
|
|
1092
|
-
id,
|
|
1093
|
-
version.version
|
|
1094
|
-
]);
|
|
1073
|
+
SET ${VERSION_COLUMNS.map((c) => `${c} = ?`).join(", ")}
|
|
1074
|
+
WHERE record_id = ? AND version = ?`, [...versionRowValues(version), id, version.version]);
|
|
1095
1075
|
}
|
|
1096
1076
|
/**
|
|
1097
1077
|
* Standalone snapshot write for tooling and tests — loud on any
|
|
@@ -1142,8 +1122,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1142
1122
|
this.indexedFieldsByType.set(type.id, indexedFieldKinds(schema));
|
|
1143
1123
|
}
|
|
1144
1124
|
async getType(id) {
|
|
1145
|
-
const
|
|
1146
|
-
return
|
|
1125
|
+
const row = this.exec.get("SELECT * FROM types WHERE id = ?", [id]);
|
|
1126
|
+
return row ? rowToType(row) : null;
|
|
1147
1127
|
}
|
|
1148
1128
|
async listTypes() {
|
|
1149
1129
|
const rows = this.exec.all("SELECT * FROM types ORDER BY base_id, version");
|
|
@@ -1156,19 +1136,16 @@ var SharedSqlRecordLogic = class {
|
|
|
1156
1136
|
this.exec.transaction(() => {
|
|
1157
1137
|
if (opts.snapshot)
|
|
1158
1138
|
this.snapshotBeforeMutation(recordId, opts.snapshot);
|
|
1159
|
-
this.
|
|
1139
|
+
this.versionedUpdate(recordId, [], [], opts);
|
|
1160
1140
|
this.insertAssociations(recordId, [association]);
|
|
1161
1141
|
});
|
|
1162
|
-
|
|
1163
|
-
if (!updated)
|
|
1164
|
-
throw new Error(`Record not found after associate: "${recordId}"`);
|
|
1165
|
-
return updated;
|
|
1142
|
+
return this.reread(recordId, "associate");
|
|
1166
1143
|
}
|
|
1167
1144
|
async dissociate(recordId, association, opts = {}) {
|
|
1168
1145
|
this.exec.transaction(() => {
|
|
1169
1146
|
if (opts.snapshot)
|
|
1170
1147
|
this.snapshotBeforeMutation(recordId, opts.snapshot);
|
|
1171
|
-
this.
|
|
1148
|
+
this.versionedUpdate(recordId, [], [], opts);
|
|
1172
1149
|
this.exec.run(`DELETE FROM associations
|
|
1173
1150
|
WHERE record_id = ?
|
|
1174
1151
|
AND kind = ?
|
|
@@ -1179,16 +1156,18 @@ var SharedSqlRecordLogic = class {
|
|
|
1179
1156
|
AND related_ns = ?
|
|
1180
1157
|
AND related_stack = ?`, [recordId, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1181
1158
|
});
|
|
1182
|
-
|
|
1183
|
-
if (!updated)
|
|
1184
|
-
throw new Error(`Record not found after dissociate: "${recordId}"`);
|
|
1185
|
-
return updated;
|
|
1159
|
+
return this.reread(recordId, "dissociate");
|
|
1186
1160
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1161
|
+
/**
|
|
1162
|
+
* Swap a record's association set for `associations` wholesale — the
|
|
1163
|
+
* "replace, don't merge" semantics a change set's association list and a
|
|
1164
|
+
* version snapshot's both carry. Caller-transactional, like every write
|
|
1165
|
+
* below.
|
|
1166
|
+
*/
|
|
1167
|
+
replaceAssociations(recordId, associations) {
|
|
1168
|
+
this.exec.run("DELETE FROM associations WHERE record_id = ?", [recordId]);
|
|
1169
|
+
if (associations.length)
|
|
1170
|
+
this.insertAssociations(recordId, associations);
|
|
1192
1171
|
}
|
|
1193
1172
|
/**
|
|
1194
1173
|
* FK enforcement (PRAGMA_FOREIGN_KEYS_ON) means inserting an
|