@haverstack/record-adapter-do-sqlite 0.18.0 → 0.21.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 +139 -117
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -17,14 +17,23 @@ var RECORD_SCHEMA_SQL = `
|
|
|
17
17
|
updated_by TEXT,
|
|
18
18
|
updated_via TEXT,
|
|
19
19
|
deleted_at INTEGER,
|
|
20
|
-
unlisted_at INTEGER
|
|
21
|
-
permissions TEXT CHECK (permissions IS NULL OR json_valid(permissions))
|
|
20
|
+
unlisted_at INTEGER
|
|
22
21
|
) STRICT;
|
|
23
22
|
|
|
23
|
+
-- Every edge a record carries, authority and data alike: a permission
|
|
24
|
+
-- entry has element identity and a delta the same shape as a tag's, so
|
|
25
|
+
-- it earns the same table and the same durability tier. The permissions
|
|
26
|
+
-- and associations fields are projections over it, partitioned by
|
|
27
|
+
-- kind. See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
28
|
+
--
|
|
24
29
|
-- A relationship's target is (related_scope, related_id) plus one
|
|
25
30
|
-- qualifier: related_stack for a record in another stack, related_ns for
|
|
26
31
|
-- a foreign namespace. All four are in the primary key, so two targets
|
|
27
|
-
-- differing only by namespace are two associations.
|
|
32
|
+
-- differing only by namespace are two associations. A permission's
|
|
33
|
+
-- grantee reuses them \u2014 related_scope 'entity' or 'group', related_id the
|
|
34
|
+
-- DID or group id \u2014 plus related_role, which only a group grantee sets
|
|
35
|
+
-- and which is in the key because member and admin name two different
|
|
36
|
+
-- sets of people.
|
|
28
37
|
--
|
|
29
38
|
-- attachment_record_id stays out of the primary key: it annotates a
|
|
30
39
|
-- reference rather than naming one, so re-pointing it updates the row an
|
|
@@ -36,22 +45,21 @@ var RECORD_SCHEMA_SQL = `
|
|
|
36
45
|
-- for the filter directions it cannot answer.
|
|
37
46
|
CREATE TABLE IF NOT EXISTS associations (
|
|
38
47
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
39
|
-
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship')),
|
|
48
|
+
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship', 'permission', 'anyone')),
|
|
40
49
|
label TEXT NOT NULL,
|
|
41
50
|
file_id TEXT NOT NULL DEFAULT '',
|
|
42
|
-
related_scope TEXT NOT NULL DEFAULT '' CHECK (related_scope IN ('', 'record', 'entity', 'external')),
|
|
51
|
+
related_scope TEXT NOT NULL DEFAULT '' CHECK (related_scope IN ('', 'record', 'entity', 'external', 'group')),
|
|
43
52
|
related_id TEXT NOT NULL DEFAULT '',
|
|
44
53
|
related_ns TEXT NOT NULL DEFAULT '',
|
|
45
54
|
related_stack TEXT NOT NULL DEFAULT '',
|
|
55
|
+
related_role TEXT NOT NULL DEFAULT '' CHECK (related_role IN ('', 'member', 'admin')),
|
|
46
56
|
attachment_record_id TEXT NOT NULL DEFAULT '',
|
|
47
|
-
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack)
|
|
57
|
+
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack, related_role)
|
|
48
58
|
) STRICT;
|
|
49
59
|
|
|
50
|
-
--
|
|
51
|
-
--
|
|
52
|
-
--
|
|
53
|
-
-- No associations column: associate()/dissociate() don't bump version,
|
|
54
|
-
-- so no version ever snapshots the association set. See
|
|
60
|
+
-- Content and the type it is read under. No parent_id, unlisted_at or
|
|
61
|
+
-- associations column: none of those aspects bumps version, so no
|
|
62
|
+
-- version ever snapshots one. See
|
|
55
63
|
-- docs/spec/versioning.md \xA7 Version history.
|
|
56
64
|
CREATE TABLE IF NOT EXISTS versions (
|
|
57
65
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
@@ -62,8 +70,6 @@ var RECORD_SCHEMA_SQL = `
|
|
|
62
70
|
entity_id TEXT,
|
|
63
71
|
updated_by TEXT,
|
|
64
72
|
updated_via TEXT,
|
|
65
|
-
parent_id TEXT,
|
|
66
|
-
permissions TEXT CHECK (permissions IS NULL OR json_valid(permissions)),
|
|
67
73
|
PRIMARY KEY (record_id, version)
|
|
68
74
|
) STRICT;
|
|
69
75
|
|
|
@@ -71,17 +77,15 @@ var RECORD_SCHEMA_SQL = `
|
|
|
71
77
|
-- them, and the association deltas nothing else retains. Envelope-level
|
|
72
78
|
-- by design \u2014 content lives on a versions row, and copying it here
|
|
73
79
|
-- would make the journal the larger of the two stores.
|
|
74
|
-
--
|
|
75
|
-
-- seq is dense from 1 per record and is the only ordering: at is wall
|
|
76
|
-
-- clock, and version stands still across an association change, so
|
|
77
|
-
-- neither orders the log alone. It is allocated inside the appending
|
|
78
|
-
-- write, which is why this table needs none of the collision healing
|
|
79
|
-
-- versions does. See docs/spec/versioning.md \xA7 The change journal.
|
|
80
|
+
-- See docs/spec/journal.md.
|
|
80
81
|
CREATE TABLE IF NOT EXISTS journal (
|
|
81
82
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
82
83
|
seq INTEGER NOT NULL,
|
|
83
84
|
at INTEGER NOT NULL,
|
|
84
|
-
|
|
85
|
+
-- No 'purged': a purge destroys the log rather than appending to it,
|
|
86
|
+
-- so the constraint names exactly the kinds an entry can carry.
|
|
87
|
+
-- See docs/spec/journal.md \xA7 A hard delete destroys the journal.
|
|
88
|
+
kind TEXT NOT NULL CHECK (kind IN ('created', 'changed', 'deleted')),
|
|
85
89
|
ops TEXT NOT NULL CHECK (json_valid(ops)),
|
|
86
90
|
version INTEGER NOT NULL,
|
|
87
91
|
type_id TEXT NOT NULL,
|
|
@@ -92,9 +96,9 @@ var RECORD_SCHEMA_SQL = `
|
|
|
92
96
|
-- the same input/state split parentId follows everywhere.
|
|
93
97
|
previous_parent_id TEXT,
|
|
94
98
|
actor TEXT CHECK (actor IS NULL OR json_valid(actor)),
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
-- One tagged edit per association the write moved, each carrying the
|
|
100
|
+
-- state it displaced. See docs/spec/journal.md \xA7 The entry.
|
|
101
|
+
associations TEXT CHECK (associations IS NULL OR json_valid(associations)),
|
|
98
102
|
PRIMARY KEY (record_id, seq)
|
|
99
103
|
) STRICT;
|
|
100
104
|
|
|
@@ -564,6 +568,10 @@ var buildQueryPlan = (query) => {
|
|
|
564
568
|
// ../sqlite-shared/dist/mappers.js
|
|
565
569
|
var toMs = (d) => d.getTime();
|
|
566
570
|
var fromMs = (ms) => new Date(ms);
|
|
571
|
+
var partitionAssociations = (associations) => ({
|
|
572
|
+
associations: associations.filter((a) => a.kind !== "permission" && a.kind !== "anyone"),
|
|
573
|
+
permissions: associations.filter((a) => a.kind === "permission" || a.kind === "anyone")
|
|
574
|
+
});
|
|
567
575
|
var rowToRecord = (row, associations) => {
|
|
568
576
|
const record = {
|
|
569
577
|
id: row.id,
|
|
@@ -589,16 +597,27 @@ var rowToRecord = (row, associations) => {
|
|
|
589
597
|
record.deletedAt = fromMs(row.deleted_at);
|
|
590
598
|
if (row.unlisted_at != null)
|
|
591
599
|
record.unlistedAt = fromMs(row.unlisted_at);
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
600
|
+
const partitioned = partitionAssociations(associations);
|
|
601
|
+
if (partitioned.associations.length)
|
|
602
|
+
record.associations = partitioned.associations;
|
|
603
|
+
if (partitioned.permissions.length)
|
|
604
|
+
record.permissions = partitioned.permissions;
|
|
596
605
|
return record;
|
|
597
606
|
};
|
|
598
607
|
var rowToAssociation = (row) => {
|
|
599
608
|
if (row.kind === "tag") {
|
|
600
609
|
return { kind: "tag", label: row.label };
|
|
601
610
|
}
|
|
611
|
+
if (row.kind === "anyone") {
|
|
612
|
+
return { kind: "anyone", label: "read" };
|
|
613
|
+
}
|
|
614
|
+
if (row.kind === "permission") {
|
|
615
|
+
return {
|
|
616
|
+
kind: "permission",
|
|
617
|
+
label: row.label,
|
|
618
|
+
grantee: rowToGrantee(row)
|
|
619
|
+
};
|
|
620
|
+
}
|
|
602
621
|
if (row.kind === "attachment") {
|
|
603
622
|
const attachmentRecordId = row.attachment_record_id;
|
|
604
623
|
return {
|
|
@@ -619,16 +638,25 @@ var rowToAssociation = (row) => {
|
|
|
619
638
|
};
|
|
620
639
|
var associationKeyColumns = (a) => {
|
|
621
640
|
if (a.kind === "attachment")
|
|
622
|
-
return [a.fileId, "", "", "", ""];
|
|
641
|
+
return [a.fileId, "", "", "", "", ""];
|
|
642
|
+
if (a.kind === "permission") {
|
|
643
|
+
const g = a.grantee;
|
|
644
|
+
return g.scope === "entity" ? ["", "entity", g.entityId, "", "", ""] : ["", "group", g.groupId, "", "", g.role];
|
|
645
|
+
}
|
|
623
646
|
if (a.kind !== "relationship")
|
|
624
|
-
return ["", "", "", "", ""];
|
|
647
|
+
return ["", "", "", "", "", ""];
|
|
625
648
|
const t = a.target;
|
|
626
649
|
if (t.scope === "entity")
|
|
627
|
-
return ["", "entity", t.entityId, "", ""];
|
|
650
|
+
return ["", "entity", t.entityId, "", "", ""];
|
|
628
651
|
if (t.scope === "external")
|
|
629
|
-
return ["", "external", t.id, t.ns, ""];
|
|
630
|
-
return ["", "record", t.recordId, "", t.stackUrl ?? ""];
|
|
652
|
+
return ["", "external", t.id, t.ns, "", ""];
|
|
653
|
+
return ["", "record", t.recordId, "", t.stackUrl ?? "", ""];
|
|
631
654
|
};
|
|
655
|
+
var rowToGrantee = (row) => row.related_scope === "group" ? {
|
|
656
|
+
scope: "group",
|
|
657
|
+
groupId: row.related_id,
|
|
658
|
+
role: row.related_role
|
|
659
|
+
} : { scope: "entity", entityId: row.related_id };
|
|
632
660
|
var rowToTarget = (row) => {
|
|
633
661
|
const id = row.related_id;
|
|
634
662
|
if (row.related_scope === "entity")
|
|
@@ -666,10 +694,6 @@ var rowToVersion = (row) => {
|
|
|
666
694
|
v.updatedBy = row.updated_by;
|
|
667
695
|
if (row.updated_via != null)
|
|
668
696
|
v.updatedVia = row.updated_via;
|
|
669
|
-
if (row.parent_id != null)
|
|
670
|
-
v.parentId = row.parent_id;
|
|
671
|
-
if (row.permissions != null)
|
|
672
|
-
v.permissions = JSON.parse(row.permissions);
|
|
673
697
|
return v;
|
|
674
698
|
};
|
|
675
699
|
var rowToJournalEntry = (row) => {
|
|
@@ -688,15 +712,8 @@ var rowToJournalEntry = (row) => {
|
|
|
688
712
|
}
|
|
689
713
|
if (row.actor != null)
|
|
690
714
|
e.actor = JSON.parse(row.actor);
|
|
691
|
-
if (row.
|
|
692
|
-
e.
|
|
693
|
-
}
|
|
694
|
-
if (row.associations_removed != null) {
|
|
695
|
-
e.associationsRemoved = JSON.parse(row.associations_removed);
|
|
696
|
-
}
|
|
697
|
-
if (row.associations_replaced != null) {
|
|
698
|
-
e.associationsReplaced = JSON.parse(row.associations_replaced);
|
|
699
|
-
}
|
|
715
|
+
if (row.associations != null)
|
|
716
|
+
e.associations = JSON.parse(row.associations);
|
|
700
717
|
return e;
|
|
701
718
|
};
|
|
702
719
|
|
|
@@ -735,9 +752,7 @@ var VERSION_COLUMNS = [
|
|
|
735
752
|
"updated_at",
|
|
736
753
|
"entity_id",
|
|
737
754
|
"updated_by",
|
|
738
|
-
"updated_via"
|
|
739
|
-
"parent_id",
|
|
740
|
-
"permissions"
|
|
755
|
+
"updated_via"
|
|
741
756
|
];
|
|
742
757
|
var versionRowValues = (version) => [
|
|
743
758
|
version.typeId,
|
|
@@ -745,9 +760,7 @@ var versionRowValues = (version) => [
|
|
|
745
760
|
toMs(version.updatedAt),
|
|
746
761
|
version.entityId ?? null,
|
|
747
762
|
version.updatedBy ?? null,
|
|
748
|
-
version.updatedVia ?? null
|
|
749
|
-
version.parentId ?? null,
|
|
750
|
-
version.permissions ? JSON.stringify(version.permissions) : null
|
|
763
|
+
version.updatedVia ?? null
|
|
751
764
|
];
|
|
752
765
|
var indexedFieldKinds = (schema) => {
|
|
753
766
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -758,7 +771,7 @@ var indexedFieldKinds = (schema) => {
|
|
|
758
771
|
}
|
|
759
772
|
return kinds;
|
|
760
773
|
};
|
|
761
|
-
var SharedSqlRecordLogic = class {
|
|
774
|
+
var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
762
775
|
deps;
|
|
763
776
|
/**
|
|
764
777
|
* Per-typeId cache of the fields content_index covers and their declared
|
|
@@ -864,8 +877,8 @@ var SharedSqlRecordLogic = class {
|
|
|
864
877
|
this.exec.run(`INSERT INTO records
|
|
865
878
|
(id, type_id, created_at, updated_at, content, version,
|
|
866
879
|
parent_id, entity_id, app_id, principal_id, updated_by, updated_via,
|
|
867
|
-
deleted_at, unlisted_at
|
|
868
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
880
|
+
deleted_at, unlisted_at)
|
|
881
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
869
882
|
record.id,
|
|
870
883
|
record.typeId,
|
|
871
884
|
toMs(record.createdAt),
|
|
@@ -879,8 +892,7 @@ var SharedSqlRecordLogic = class {
|
|
|
879
892
|
record.updatedBy ?? null,
|
|
880
893
|
record.updatedVia ?? null,
|
|
881
894
|
record.deletedAt ? toMs(record.deletedAt) : null,
|
|
882
|
-
record.unlistedAt ? toMs(record.unlistedAt) : null
|
|
883
|
-
record.permissions ? JSON.stringify(record.permissions) : null
|
|
895
|
+
record.unlistedAt ? toMs(record.unlistedAt) : null
|
|
884
896
|
]);
|
|
885
897
|
} catch (err) {
|
|
886
898
|
if (isUniqueConstraintViolation(err)) {
|
|
@@ -888,9 +900,9 @@ var SharedSqlRecordLogic = class {
|
|
|
888
900
|
}
|
|
889
901
|
throw err;
|
|
890
902
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
903
|
+
const edges = [...record.associations ?? [], ...record.permissions ?? []];
|
|
904
|
+
if (edges.length)
|
|
905
|
+
this.insertAssociations(record.id, edges);
|
|
894
906
|
fts5Strategy.insert(this.exec, record.id, JSON.stringify(record.content));
|
|
895
907
|
this.syncContentIndex(record.id, record.typeId, record.content);
|
|
896
908
|
this.appendJournal(record.id, opts.journal);
|
|
@@ -928,13 +940,13 @@ var SharedSqlRecordLogic = class {
|
|
|
928
940
|
* one call is one version whatever it moved.
|
|
929
941
|
*
|
|
930
942
|
* `opts.bumpsVersion: false` is a change set that touches only
|
|
931
|
-
* `associations` — Stack computes this from
|
|
932
|
-
* moves. Such a write
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
943
|
+
* `associations`, `parentId` and/or `unlisted` — Stack computes this from
|
|
944
|
+
* which ops the set actually moves. Such a write still lands its columns,
|
|
945
|
+
* but leaves `version`, `updatedAt` and the actor stamps exactly as they
|
|
946
|
+
* stood and takes no snapshot. `expectedVersion`, if given, is re-checked
|
|
947
|
+
* synchronously inside the transaction — the CAS guard the bumping path
|
|
948
|
+
* gets from its UPDATE's WHERE clause, restated as a read because the
|
|
949
|
+
* UPDATE here carries no version predicate. See
|
|
938
950
|
* docs/spec/versioning.md § Version history.
|
|
939
951
|
*
|
|
940
952
|
* The expectedVersion check for a bumping write is standalone rather than
|
|
@@ -948,38 +960,35 @@ var SharedSqlRecordLogic = class {
|
|
|
948
960
|
if (!existing)
|
|
949
961
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
950
962
|
this.checkExpectedVersion(existing, opts.expectedVersion);
|
|
963
|
+
const now = toMs(/* @__PURE__ */ new Date());
|
|
964
|
+
const sets = [];
|
|
965
|
+
const values = [];
|
|
966
|
+
if (changes.parentId !== void 0) {
|
|
967
|
+
sets.push("parent_id = ?");
|
|
968
|
+
values.push(changes.parentId);
|
|
969
|
+
}
|
|
970
|
+
if (changes.unlisted !== void 0) {
|
|
971
|
+
sets.push("unlisted_at = ?");
|
|
972
|
+
values.push(changes.unlisted ? now : null);
|
|
973
|
+
}
|
|
951
974
|
if (opts.bumpsVersion === false) {
|
|
952
975
|
this.exec.transaction(() => {
|
|
953
976
|
const current = this.readRecord(id);
|
|
954
977
|
if (!current)
|
|
955
978
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
956
979
|
this.checkExpectedVersion(current, opts.expectedVersion);
|
|
957
|
-
if (
|
|
958
|
-
this.
|
|
980
|
+
if (sets.length > 0) {
|
|
981
|
+
this.exec.run(`UPDATE records SET ${sets.join(", ")} WHERE id = ?`, [...values, id]);
|
|
959
982
|
}
|
|
983
|
+
this.replaceAssociationHalves(id, changes);
|
|
960
984
|
this.appendJournal(id, opts.journal);
|
|
961
985
|
});
|
|
962
986
|
return this.reread(id, "mutateRecord");
|
|
963
987
|
}
|
|
964
988
|
const merged = changes.contentPatch ? applyMergePatch(existing.content, changes.contentPatch) : void 0;
|
|
965
|
-
const sets = [];
|
|
966
|
-
const values = [];
|
|
967
989
|
if (merged !== void 0) {
|
|
968
|
-
sets.
|
|
969
|
-
values.
|
|
970
|
-
}
|
|
971
|
-
if (changes.parentId !== void 0) {
|
|
972
|
-
sets.push("parent_id = ?");
|
|
973
|
-
values.push(changes.parentId);
|
|
974
|
-
}
|
|
975
|
-
if (changes.permissions !== void 0) {
|
|
976
|
-
sets.push("permissions = ?");
|
|
977
|
-
values.push(changes.permissions.length ? JSON.stringify(changes.permissions) : null);
|
|
978
|
-
}
|
|
979
|
-
const now = toMs(/* @__PURE__ */ new Date());
|
|
980
|
-
if (changes.unlisted !== void 0) {
|
|
981
|
-
sets.push("unlisted_at = ?");
|
|
982
|
-
values.push(changes.unlisted ? now : null);
|
|
990
|
+
sets.unshift("content = ?");
|
|
991
|
+
values.unshift(JSON.stringify(merged));
|
|
983
992
|
}
|
|
984
993
|
this.exec.transaction(() => {
|
|
985
994
|
if (opts.snapshot)
|
|
@@ -987,9 +996,7 @@ var SharedSqlRecordLogic = class {
|
|
|
987
996
|
if (merged !== void 0)
|
|
988
997
|
fts5Strategy.remove(this.exec, id);
|
|
989
998
|
this.versionedUpdate(id, sets, values, opts, now);
|
|
990
|
-
|
|
991
|
-
this.replaceAssociations(id, changes.associations);
|
|
992
|
-
}
|
|
999
|
+
this.replaceAssociationHalves(id, changes);
|
|
993
1000
|
if (merged !== void 0) {
|
|
994
1001
|
fts5Strategy.insert(this.exec, id, JSON.stringify(merged));
|
|
995
1002
|
this.syncContentIndex(id, existing.typeId, merged);
|
|
@@ -1048,15 +1055,15 @@ var SharedSqlRecordLogic = class {
|
|
|
1048
1055
|
async restoreVersion(id, version, opts = {}) {
|
|
1049
1056
|
const existing = await this.getRecord(id);
|
|
1050
1057
|
if (!existing)
|
|
1051
|
-
throw new
|
|
1058
|
+
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1052
1059
|
this.checkExpectedVersion(existing, opts.expectedVersion);
|
|
1053
1060
|
const target = await this.getVersion(id, version);
|
|
1054
1061
|
if (!target)
|
|
1055
|
-
throw new
|
|
1062
|
+
throw new StackNotFoundError(`Version not found: "${id}"@${version}`);
|
|
1056
1063
|
this.exec.transaction(() => {
|
|
1057
1064
|
if (opts.snapshot)
|
|
1058
1065
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
1059
|
-
this.rewriteContent(id, target.typeId, target.content, opts
|
|
1066
|
+
this.rewriteContent(id, target.typeId, target.content, opts);
|
|
1060
1067
|
this.appendJournal(id, opts.journal);
|
|
1061
1068
|
});
|
|
1062
1069
|
return this.reread(id, "restoreVersion");
|
|
@@ -1064,7 +1071,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1064
1071
|
async commitMigration(id, toTypeId, content, opts = {}) {
|
|
1065
1072
|
const existing = await this.getRecord(id);
|
|
1066
1073
|
if (!existing)
|
|
1067
|
-
throw new
|
|
1074
|
+
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1068
1075
|
this.checkExpectedVersion(existing, opts.expectedVersion);
|
|
1069
1076
|
this.exec.transaction(() => {
|
|
1070
1077
|
if (opts.snapshot)
|
|
@@ -1076,8 +1083,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1076
1083
|
}
|
|
1077
1084
|
/**
|
|
1078
1085
|
* Replace a record's type and content wholesale — what restoreVersion and
|
|
1079
|
-
* commitMigration each do, differing only in where the content came from
|
|
1080
|
-
* and whether the container moves with it.
|
|
1086
|
+
* commitMigration each do, differing only in where the content came from.
|
|
1081
1087
|
*
|
|
1082
1088
|
* The order is the constraint: fts5Strategy.remove() has to read the old
|
|
1083
1089
|
* content off the records row, so it runs before the UPDATE, and the
|
|
@@ -1085,10 +1091,10 @@ var SharedSqlRecordLogic = class {
|
|
|
1085
1091
|
* exec.transaction() — a failure between the two halves would otherwise
|
|
1086
1092
|
* leave the record unindexed.
|
|
1087
1093
|
*/
|
|
1088
|
-
rewriteContent(id, typeId, content, opts
|
|
1094
|
+
rewriteContent(id, typeId, content, opts) {
|
|
1089
1095
|
const json = JSON.stringify(content);
|
|
1090
1096
|
fts5Strategy.remove(this.exec, id);
|
|
1091
|
-
this.versionedUpdate(id, ["type_id = ?", "content = ?"
|
|
1097
|
+
this.versionedUpdate(id, ["type_id = ?", "content = ?"], [typeId, json], opts);
|
|
1092
1098
|
fts5Strategy.insert(this.exec, id, json);
|
|
1093
1099
|
this.syncContentIndex(id, typeId, content);
|
|
1094
1100
|
}
|
|
@@ -1223,7 +1229,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1223
1229
|
* `seq` is allocated here, from the log's own max, rather than computed
|
|
1224
1230
|
* by the caller. That is what spares this table the collision healing
|
|
1225
1231
|
* versions needs: nothing outside this statement ever holds a seq it
|
|
1226
|
-
* expects to be free. See docs/spec/
|
|
1232
|
+
* expects to be free. See docs/spec/journal.md § Ordering.
|
|
1227
1233
|
*/
|
|
1228
1234
|
appendJournal(recordId, entry) {
|
|
1229
1235
|
if (!entry)
|
|
@@ -1233,12 +1239,11 @@ var SharedSqlRecordLogic = class {
|
|
|
1233
1239
|
throw new StackNotFoundError(`Record not found: "${recordId}"`);
|
|
1234
1240
|
this.exec.run(`INSERT INTO journal
|
|
1235
1241
|
(record_id, seq, at, kind, ops, version, type_id, parent_id,
|
|
1236
|
-
previous_parent_id, actor,
|
|
1237
|
-
associations_replaced)
|
|
1242
|
+
previous_parent_id, actor, associations)
|
|
1238
1243
|
VALUES (
|
|
1239
1244
|
?,
|
|
1240
1245
|
(SELECT COALESCE(MAX(seq), 0) + 1 FROM journal WHERE record_id = ?),
|
|
1241
|
-
?, ?, ?, ?, ?, ?, ?, ?,
|
|
1246
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1242
1247
|
recordId,
|
|
1243
1248
|
recordId,
|
|
1244
1249
|
toMs(/* @__PURE__ */ new Date()),
|
|
@@ -1250,12 +1255,13 @@ var SharedSqlRecordLogic = class {
|
|
|
1250
1255
|
// '' is the root; NULL is an entry that names no origin at all.
|
|
1251
1256
|
entry.previousParentId === void 0 ? null : entry.previousParentId ?? "",
|
|
1252
1257
|
entry.actor ? JSON.stringify(entry.actor) : null,
|
|
1253
|
-
entry.
|
|
1254
|
-
entry.associationsRemoved?.length ? JSON.stringify(entry.associationsRemoved) : null,
|
|
1255
|
-
entry.associationsReplaced?.length ? JSON.stringify(entry.associationsReplaced) : null
|
|
1258
|
+
entry.associations?.length ? JSON.stringify(entry.associations) : null
|
|
1256
1259
|
]);
|
|
1257
1260
|
}
|
|
1258
1261
|
async getJournal(id, query = {}) {
|
|
1262
|
+
if (!this.exec.get("SELECT 1 FROM records WHERE id = ?", [id])) {
|
|
1263
|
+
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1264
|
+
}
|
|
1259
1265
|
const conditions = ["record_id = ?"];
|
|
1260
1266
|
const values = [id];
|
|
1261
1267
|
if (query.sinceSeq !== void 0) {
|
|
@@ -1296,11 +1302,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1296
1302
|
// Associations
|
|
1297
1303
|
// -------------------------------------------------------
|
|
1298
1304
|
/**
|
|
1299
|
-
* Never bumps `version`/`updatedAt` and never snapshots —
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
1302
|
-
* associations table changes. See docs/spec/versioning.md § Version
|
|
1303
|
-
* history.
|
|
1305
|
+
* Never bumps `version`/`updatedAt` and never snapshots — see
|
|
1306
|
+
* StackRecordAdapter.associate(). The records row is untouched entirely;
|
|
1307
|
+
* only the associations table changes.
|
|
1304
1308
|
*/
|
|
1305
1309
|
async associate(recordId, association, opts = {}) {
|
|
1306
1310
|
this.exec.transaction(() => {
|
|
@@ -1324,19 +1328,37 @@ var SharedSqlRecordLogic = class {
|
|
|
1324
1328
|
AND related_scope = ?
|
|
1325
1329
|
AND related_id = ?
|
|
1326
1330
|
AND related_ns = ?
|
|
1327
|
-
AND related_stack =
|
|
1331
|
+
AND related_stack = ?
|
|
1332
|
+
AND related_role = ?`, [recordId, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1328
1333
|
this.appendJournal(recordId, opts.journal);
|
|
1329
1334
|
});
|
|
1330
1335
|
return this.reread(recordId, "dissociate");
|
|
1331
1336
|
}
|
|
1337
|
+
/** The kinds each change-set key replaces, and nothing else. */
|
|
1338
|
+
static DATA_KINDS = ["tag", "attachment", "relationship"];
|
|
1339
|
+
static AUTHORITY_KINDS = ["permission", "anyone"];
|
|
1340
|
+
/**
|
|
1341
|
+
* Apply a change set's two association keys, each replacing only within
|
|
1342
|
+
* its own half of the partition. That partition is the whole reason a
|
|
1343
|
+
* tag write cannot drop an ACL: the `associations` key deletes data
|
|
1344
|
+
* kinds alone, so the authority rows beside them are never in scope.
|
|
1345
|
+
* Caller-transactional, like every write below.
|
|
1346
|
+
* See docs/spec/access-control.md § Record-level permissions.
|
|
1347
|
+
*/
|
|
1348
|
+
replaceAssociationHalves(recordId, changes) {
|
|
1349
|
+
if (changes.associations !== void 0) {
|
|
1350
|
+
this.replaceAssociations(recordId, changes.associations, _SharedSqlRecordLogic.DATA_KINDS);
|
|
1351
|
+
}
|
|
1352
|
+
if (changes.permissions !== void 0) {
|
|
1353
|
+
this.replaceAssociations(recordId, changes.permissions, _SharedSqlRecordLogic.AUTHORITY_KINDS);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1332
1356
|
/**
|
|
1333
|
-
* Swap a record's association set
|
|
1334
|
-
*
|
|
1335
|
-
* version snapshot's both carry. Caller-transactional, like every write
|
|
1336
|
-
* below.
|
|
1357
|
+
* Swap one half of a record's association set wholesale — the "replace,
|
|
1358
|
+
* don't merge" semantics a change set's association list carries.
|
|
1337
1359
|
*/
|
|
1338
|
-
replaceAssociations(recordId, associations) {
|
|
1339
|
-
this.exec.run(
|
|
1360
|
+
replaceAssociations(recordId, associations, kinds) {
|
|
1361
|
+
this.exec.run(`DELETE FROM associations WHERE record_id = ? AND kind IN (${kinds.map(() => "?").join(", ")})`, [recordId, ...kinds]);
|
|
1340
1362
|
if (associations.length)
|
|
1341
1363
|
this.insertAssociations(recordId, associations);
|
|
1342
1364
|
}
|
|
@@ -1355,8 +1377,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1355
1377
|
try {
|
|
1356
1378
|
this.exec.run(`INSERT INTO associations
|
|
1357
1379
|
(record_id, kind, label, file_id, related_scope, related_id, related_ns,
|
|
1358
|
-
related_stack, attachment_record_id)
|
|
1359
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1380
|
+
related_stack, related_role, attachment_record_id)
|
|
1381
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1360
1382
|
ON CONFLICT DO UPDATE SET attachment_record_id = excluded.attachment_record_id`, [
|
|
1361
1383
|
recordId,
|
|
1362
1384
|
assoc.kind,
|