@haverstack/record-adapter-do-sqlite 0.13.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.d.ts +4 -14
- package/dist/index.js +221 -278
- 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: patchContent/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]);
|
|
@@ -789,55 +837,70 @@ var SharedSqlRecordLogic = class {
|
|
|
789
837
|
const associations = this.getAssociationsForRecord(id);
|
|
790
838
|
return rowToRecord(row, associations);
|
|
791
839
|
}
|
|
792
|
-
|
|
840
|
+
/**
|
|
841
|
+
* Apply a change set in one transaction: one UPDATE over the record row
|
|
842
|
+
* with only the columns the change set names, the association set
|
|
843
|
+
* replaced when it carries one, and the snapshot written alongside — so
|
|
844
|
+
* one call is one version whatever it moved.
|
|
845
|
+
*
|
|
846
|
+
* The expectedVersion check is standalone rather than folded into the
|
|
847
|
+
* UPDATE alone, because a content change needs fts.remove() to run
|
|
848
|
+
* *before* the records-table content changes. The guard is still in the
|
|
849
|
+
* UPDATE's WHERE, so a writer that slipped in between the read and the
|
|
850
|
+
* write is caught there rather than overwriting.
|
|
851
|
+
*/
|
|
852
|
+
async mutateRecord(id, changes, opts = {}) {
|
|
793
853
|
const existing = await this.getRecord(id);
|
|
794
854
|
if (!existing)
|
|
795
|
-
throw new
|
|
855
|
+
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
796
856
|
this.checkExpectedVersion(existing, opts.expectedVersion);
|
|
797
|
-
const merged = applyMergePatch(existing.content,
|
|
857
|
+
const merged = changes.contentPatch ? applyMergePatch(existing.content, changes.contentPatch) : void 0;
|
|
858
|
+
const sets = [];
|
|
859
|
+
const values = [];
|
|
860
|
+
if (merged !== void 0) {
|
|
861
|
+
sets.push("content = ?");
|
|
862
|
+
values.push(JSON.stringify(merged));
|
|
863
|
+
}
|
|
864
|
+
if (changes.parentId !== void 0) {
|
|
865
|
+
sets.push("parent_id = ?");
|
|
866
|
+
values.push(changes.parentId);
|
|
867
|
+
}
|
|
868
|
+
if (changes.permissions !== void 0) {
|
|
869
|
+
sets.push("permissions = ?");
|
|
870
|
+
values.push(changes.permissions.length ? JSON.stringify(changes.permissions) : null);
|
|
871
|
+
}
|
|
872
|
+
const now = toMs(/* @__PURE__ */ new Date());
|
|
873
|
+
if (changes.unlisted !== void 0) {
|
|
874
|
+
sets.push("unlisted_at = ?");
|
|
875
|
+
values.push(changes.unlisted ? now : null);
|
|
876
|
+
}
|
|
798
877
|
this.exec.transaction(() => {
|
|
799
878
|
if (opts.snapshot)
|
|
800
879
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
880
|
+
if (merged !== void 0)
|
|
881
|
+
fts5Strategy.remove(this.exec, id);
|
|
882
|
+
this.versionedUpdate(id, sets, values, opts, now);
|
|
883
|
+
if (changes.associations !== void 0) {
|
|
884
|
+
this.replaceAssociations(id, changes.associations);
|
|
885
|
+
}
|
|
886
|
+
if (merged !== void 0) {
|
|
887
|
+
fts5Strategy.insert(this.exec, id, JSON.stringify(merged));
|
|
888
|
+
this.syncContentIndex(id, existing.typeId, merged);
|
|
889
|
+
}
|
|
811
890
|
});
|
|
812
|
-
|
|
813
|
-
if (!updated)
|
|
814
|
-
throw new Error(`Record not found after patchContent: "${id}"`);
|
|
815
|
-
return updated;
|
|
891
|
+
return this.reread(id, "mutateRecord");
|
|
816
892
|
}
|
|
817
893
|
async deleteRecord(id, opts = {}) {
|
|
818
894
|
if (opts.hard) {
|
|
819
895
|
return this.exec.transaction(() => this.hardDeleteRecord(id, opts.expectedVersion));
|
|
820
|
-
} else {
|
|
821
|
-
this.exec.transaction(() => {
|
|
822
|
-
if (opts.snapshot)
|
|
823
|
-
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
824
|
-
const { clause, params: verParams } = this.versionGuard(opts.expectedVersion);
|
|
825
|
-
const changed = this.exec.run(`UPDATE records SET deleted_at = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?${clause}`, [
|
|
826
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
827
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
828
|
-
opts.updatedBy ?? null,
|
|
829
|
-
opts.updatedVia ?? null,
|
|
830
|
-
id,
|
|
831
|
-
...verParams
|
|
832
|
-
]);
|
|
833
|
-
if (changed === 0)
|
|
834
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
835
|
-
});
|
|
836
896
|
}
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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");
|
|
841
904
|
}
|
|
842
905
|
/**
|
|
843
906
|
* Deletes a record's FTS entry, associations, versions, file-refs, sort
|
|
@@ -850,15 +913,12 @@ var SharedSqlRecordLogic = class {
|
|
|
850
913
|
*/
|
|
851
914
|
hardDeleteRecord(id, expectedVersion) {
|
|
852
915
|
const purged = this.readRecord(id);
|
|
853
|
-
if (
|
|
854
|
-
if (
|
|
916
|
+
if (!purged) {
|
|
917
|
+
if (expectedVersion !== void 0)
|
|
855
918
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
856
|
-
if (purged.version !== expectedVersion) {
|
|
857
|
-
throw new StackVersionConflictError(`Record "${id}" is at version ${purged.version}, expected ${expectedVersion}`, id, expectedVersion, purged.version);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
if (!purged)
|
|
861
919
|
return null;
|
|
920
|
+
}
|
|
921
|
+
this.checkExpectedVersion(purged, expectedVersion);
|
|
862
922
|
fts5Strategy.remove(this.exec, id);
|
|
863
923
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
864
924
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
@@ -870,79 +930,9 @@ var SharedSqlRecordLogic = class {
|
|
|
870
930
|
this.exec.transaction(() => {
|
|
871
931
|
if (opts.snapshot)
|
|
872
932
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
873
|
-
|
|
874
|
-
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]);
|
|
875
|
-
if (changed === 0)
|
|
876
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
933
|
+
this.versionedUpdate(id, ["deleted_at = NULL"], [], opts);
|
|
877
934
|
});
|
|
878
|
-
|
|
879
|
-
if (!updated)
|
|
880
|
-
throw new Error(`Record not found after undelete: "${id}"`);
|
|
881
|
-
return updated;
|
|
882
|
-
}
|
|
883
|
-
async setPermissions(id, permissions, opts = {}) {
|
|
884
|
-
this.exec.transaction(() => {
|
|
885
|
-
if (opts.snapshot)
|
|
886
|
-
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
887
|
-
const { clause, params: verParams } = this.versionGuard(opts.expectedVersion);
|
|
888
|
-
const changed = this.exec.run(`UPDATE records SET permissions = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?${clause}`, [
|
|
889
|
-
permissions.length ? JSON.stringify(permissions) : null,
|
|
890
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
891
|
-
opts.updatedBy ?? null,
|
|
892
|
-
opts.updatedVia ?? null,
|
|
893
|
-
id,
|
|
894
|
-
...verParams
|
|
895
|
-
]);
|
|
896
|
-
if (changed === 0)
|
|
897
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
898
|
-
});
|
|
899
|
-
const updated = await this.getRecord(id);
|
|
900
|
-
if (!updated)
|
|
901
|
-
throw new Error(`Record not found after setPermissions: "${id}"`);
|
|
902
|
-
return updated;
|
|
903
|
-
}
|
|
904
|
-
async setUnlisted(id, unlisted, opts = {}) {
|
|
905
|
-
this.exec.transaction(() => {
|
|
906
|
-
if (opts.snapshot)
|
|
907
|
-
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
908
|
-
const { clause, params: verParams } = this.versionGuard(opts.expectedVersion);
|
|
909
|
-
const now = toMs(/* @__PURE__ */ new Date());
|
|
910
|
-
const changed = this.exec.run(`UPDATE records SET unlisted_at = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?${clause}`, [
|
|
911
|
-
unlisted ? now : null,
|
|
912
|
-
now,
|
|
913
|
-
opts.updatedBy ?? null,
|
|
914
|
-
opts.updatedVia ?? null,
|
|
915
|
-
id,
|
|
916
|
-
...verParams
|
|
917
|
-
]);
|
|
918
|
-
if (changed === 0)
|
|
919
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
920
|
-
});
|
|
921
|
-
const updated = await this.getRecord(id);
|
|
922
|
-
if (!updated)
|
|
923
|
-
throw new Error(`Record not found after setUnlisted: "${id}"`);
|
|
924
|
-
return updated;
|
|
925
|
-
}
|
|
926
|
-
async setParent(id, parentId, opts = {}) {
|
|
927
|
-
this.exec.transaction(() => {
|
|
928
|
-
if (opts.snapshot)
|
|
929
|
-
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
930
|
-
const { clause, params: verParams } = this.versionGuard(opts.expectedVersion);
|
|
931
|
-
const changed = this.exec.run(`UPDATE records SET parent_id = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?${clause}`, [
|
|
932
|
-
parentId,
|
|
933
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
934
|
-
opts.updatedBy ?? null,
|
|
935
|
-
opts.updatedVia ?? null,
|
|
936
|
-
id,
|
|
937
|
-
...verParams
|
|
938
|
-
]);
|
|
939
|
-
if (changed === 0)
|
|
940
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
941
|
-
});
|
|
942
|
-
const updated = await this.getRecord(id);
|
|
943
|
-
if (!updated)
|
|
944
|
-
throw new Error(`Record not found after setParent: "${id}"`);
|
|
945
|
-
return updated;
|
|
935
|
+
return this.reread(id, "undelete");
|
|
946
936
|
}
|
|
947
937
|
async restoreVersion(id, version, opts = {}) {
|
|
948
938
|
const existing = await this.getRecord(id);
|
|
@@ -955,28 +945,12 @@ var SharedSqlRecordLogic = class {
|
|
|
955
945
|
this.exec.transaction(() => {
|
|
956
946
|
if (opts.snapshot)
|
|
957
947
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
963
|
-
opts.updatedBy ?? null,
|
|
964
|
-
opts.updatedVia ?? null,
|
|
965
|
-
target.parentId ?? null,
|
|
966
|
-
id
|
|
967
|
-
]);
|
|
968
|
-
if (target.associations !== void 0) {
|
|
969
|
-
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
970
|
-
if (target.associations.length)
|
|
971
|
-
this.insertAssociations(id, target.associations);
|
|
972
|
-
}
|
|
973
|
-
fts5Strategy.insert(this.exec, id, JSON.stringify(target.content));
|
|
974
|
-
this.syncContentIndex(id, target.typeId, target.content);
|
|
948
|
+
this.rewriteContent(id, target.typeId, target.content, opts, ["parent_id = ?"], [target.parentId ?? null]);
|
|
949
|
+
const applied = opts.restoreAssociations === false ? void 0 : target.associations;
|
|
950
|
+
if (applied !== void 0)
|
|
951
|
+
this.replaceAssociations(id, applied);
|
|
975
952
|
});
|
|
976
|
-
|
|
977
|
-
if (!updated)
|
|
978
|
-
throw new Error(`Record not found after restoreVersion: "${id}"`);
|
|
979
|
-
return updated;
|
|
953
|
+
return this.reread(id, "restoreVersion");
|
|
980
954
|
}
|
|
981
955
|
async commitMigration(id, toTypeId, content, opts = {}) {
|
|
982
956
|
const existing = await this.getRecord(id);
|
|
@@ -986,22 +960,27 @@ var SharedSqlRecordLogic = class {
|
|
|
986
960
|
this.exec.transaction(() => {
|
|
987
961
|
if (opts.snapshot)
|
|
988
962
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
989
|
-
|
|
990
|
-
this.exec.run(`UPDATE records SET type_id = ?, content = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?`, [
|
|
991
|
-
toTypeId,
|
|
992
|
-
JSON.stringify(content),
|
|
993
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
994
|
-
opts.updatedBy ?? null,
|
|
995
|
-
opts.updatedVia ?? null,
|
|
996
|
-
id
|
|
997
|
-
]);
|
|
998
|
-
fts5Strategy.insert(this.exec, id, JSON.stringify(content));
|
|
999
|
-
this.syncContentIndex(id, toTypeId, content);
|
|
963
|
+
this.rewriteContent(id, toTypeId, content, opts);
|
|
1000
964
|
});
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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);
|
|
1005
984
|
}
|
|
1006
985
|
async queryRecords(query) {
|
|
1007
986
|
const limit = query.limit ?? 50;
|
|
@@ -1062,8 +1041,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1062
1041
|
return rows.map(rowToVersion);
|
|
1063
1042
|
}
|
|
1064
1043
|
async getVersion(id, version) {
|
|
1065
|
-
const
|
|
1066
|
-
return
|
|
1044
|
+
const row = this.exec.get("SELECT * FROM versions WHERE record_id = ? AND version = ?", [id, version]);
|
|
1045
|
+
return row ? rowToVersion(row) : null;
|
|
1067
1046
|
}
|
|
1068
1047
|
/**
|
|
1069
1048
|
* A (record_id, version) collision is rejected loudly via the UNIQUE
|
|
@@ -1074,21 +1053,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1074
1053
|
insertVersionRow(id, version) {
|
|
1075
1054
|
try {
|
|
1076
1055
|
this.exec.run(`INSERT INTO versions
|
|
1077
|
-
(record_id, version,
|
|
1078
|
-
|
|
1079
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1080
|
-
id,
|
|
1081
|
-
version.version,
|
|
1082
|
-
version.typeId,
|
|
1083
|
-
JSON.stringify(version.content),
|
|
1084
|
-
toMs(version.updatedAt),
|
|
1085
|
-
version.entityId ?? null,
|
|
1086
|
-
version.updatedBy ?? null,
|
|
1087
|
-
version.updatedVia ?? null,
|
|
1088
|
-
version.parentId ?? null,
|
|
1089
|
-
version.associations ? JSON.stringify(version.associations) : null,
|
|
1090
|
-
version.permissions ? JSON.stringify(version.permissions) : null
|
|
1091
|
-
]);
|
|
1056
|
+
(record_id, version, ${VERSION_COLUMNS.join(", ")})
|
|
1057
|
+
VALUES (?, ?, ${VERSION_COLUMNS.map(() => "?").join(", ")})`, [id, version.version, ...versionRowValues(version)]);
|
|
1092
1058
|
} catch (err) {
|
|
1093
1059
|
if (isUniqueConstraintViolation(err)) {
|
|
1094
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.`);
|
|
@@ -1097,28 +1063,15 @@ var SharedSqlRecordLogic = class {
|
|
|
1097
1063
|
}
|
|
1098
1064
|
}
|
|
1099
1065
|
/**
|
|
1100
|
-
* Replaces every column insertVersionRow writes
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
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.
|
|
1104
1070
|
*/
|
|
1105
1071
|
overwriteVersionRow(id, version) {
|
|
1106
1072
|
this.exec.run(`UPDATE versions
|
|
1107
|
-
SET
|
|
1108
|
-
|
|
1109
|
-
WHERE record_id = ? AND version = ?`, [
|
|
1110
|
-
version.typeId,
|
|
1111
|
-
JSON.stringify(version.content),
|
|
1112
|
-
toMs(version.updatedAt),
|
|
1113
|
-
version.entityId ?? null,
|
|
1114
|
-
version.updatedBy ?? null,
|
|
1115
|
-
version.updatedVia ?? null,
|
|
1116
|
-
version.parentId ?? null,
|
|
1117
|
-
version.associations ? JSON.stringify(version.associations) : null,
|
|
1118
|
-
version.permissions ? JSON.stringify(version.permissions) : null,
|
|
1119
|
-
id,
|
|
1120
|
-
version.version
|
|
1121
|
-
]);
|
|
1073
|
+
SET ${VERSION_COLUMNS.map((c) => `${c} = ?`).join(", ")}
|
|
1074
|
+
WHERE record_id = ? AND version = ?`, [...versionRowValues(version), id, version.version]);
|
|
1122
1075
|
}
|
|
1123
1076
|
/**
|
|
1124
1077
|
* Standalone snapshot write for tooling and tests — loud on any
|
|
@@ -1169,8 +1122,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1169
1122
|
this.indexedFieldsByType.set(type.id, indexedFieldKinds(schema));
|
|
1170
1123
|
}
|
|
1171
1124
|
async getType(id) {
|
|
1172
|
-
const
|
|
1173
|
-
return
|
|
1125
|
+
const row = this.exec.get("SELECT * FROM types WHERE id = ?", [id]);
|
|
1126
|
+
return row ? rowToType(row) : null;
|
|
1174
1127
|
}
|
|
1175
1128
|
async listTypes() {
|
|
1176
1129
|
const rows = this.exec.all("SELECT * FROM types ORDER BY base_id, version");
|
|
@@ -1183,19 +1136,16 @@ var SharedSqlRecordLogic = class {
|
|
|
1183
1136
|
this.exec.transaction(() => {
|
|
1184
1137
|
if (opts.snapshot)
|
|
1185
1138
|
this.snapshotBeforeMutation(recordId, opts.snapshot);
|
|
1186
|
-
this.
|
|
1139
|
+
this.versionedUpdate(recordId, [], [], opts);
|
|
1187
1140
|
this.insertAssociations(recordId, [association]);
|
|
1188
1141
|
});
|
|
1189
|
-
|
|
1190
|
-
if (!updated)
|
|
1191
|
-
throw new Error(`Record not found after associate: "${recordId}"`);
|
|
1192
|
-
return updated;
|
|
1142
|
+
return this.reread(recordId, "associate");
|
|
1193
1143
|
}
|
|
1194
1144
|
async dissociate(recordId, association, opts = {}) {
|
|
1195
1145
|
this.exec.transaction(() => {
|
|
1196
1146
|
if (opts.snapshot)
|
|
1197
1147
|
this.snapshotBeforeMutation(recordId, opts.snapshot);
|
|
1198
|
-
this.
|
|
1148
|
+
this.versionedUpdate(recordId, [], [], opts);
|
|
1199
1149
|
this.exec.run(`DELETE FROM associations
|
|
1200
1150
|
WHERE record_id = ?
|
|
1201
1151
|
AND kind = ?
|
|
@@ -1206,16 +1156,18 @@ var SharedSqlRecordLogic = class {
|
|
|
1206
1156
|
AND related_ns = ?
|
|
1207
1157
|
AND related_stack = ?`, [recordId, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1208
1158
|
});
|
|
1209
|
-
|
|
1210
|
-
if (!updated)
|
|
1211
|
-
throw new Error(`Record not found after dissociate: "${recordId}"`);
|
|
1212
|
-
return updated;
|
|
1159
|
+
return this.reread(recordId, "dissociate");
|
|
1213
1160
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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);
|
|
1219
1171
|
}
|
|
1220
1172
|
/**
|
|
1221
1173
|
* FK enforcement (PRAGMA_FOREIGN_KEYS_ON) means inserting an
|
|
@@ -1379,8 +1331,8 @@ var DoSQLiteRecordAdapter = class _DoSQLiteRecordAdapter {
|
|
|
1379
1331
|
getRecord(id) {
|
|
1380
1332
|
return this.record.getRecord(id);
|
|
1381
1333
|
}
|
|
1382
|
-
|
|
1383
|
-
return this.record.
|
|
1334
|
+
mutateRecord(id, changes, opts) {
|
|
1335
|
+
return this.record.mutateRecord(id, changes, opts);
|
|
1384
1336
|
}
|
|
1385
1337
|
deleteRecord(id, opts) {
|
|
1386
1338
|
return this.record.deleteRecord(id, opts);
|
|
@@ -1388,15 +1340,6 @@ var DoSQLiteRecordAdapter = class _DoSQLiteRecordAdapter {
|
|
|
1388
1340
|
undeleteRecord(id, opts) {
|
|
1389
1341
|
return this.record.undeleteRecord(id, opts);
|
|
1390
1342
|
}
|
|
1391
|
-
setPermissions(id, permissions, opts) {
|
|
1392
|
-
return this.record.setPermissions(id, permissions, opts);
|
|
1393
|
-
}
|
|
1394
|
-
setUnlisted(id, unlisted, opts) {
|
|
1395
|
-
return this.record.setUnlisted(id, unlisted, opts);
|
|
1396
|
-
}
|
|
1397
|
-
setParent(id, parentId, opts) {
|
|
1398
|
-
return this.record.setParent(id, parentId, opts);
|
|
1399
|
-
}
|
|
1400
1343
|
restoreVersion(id, version, opts) {
|
|
1401
1344
|
return this.record.restoreVersion(id, version, opts);
|
|
1402
1345
|
}
|