@haverstack/record-adapter-sqlite 0.25.0 → 0.28.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 +3 -2
package/dist/index.js
CHANGED
|
@@ -24,14 +24,23 @@ var RECORD_SCHEMA_SQL = `
|
|
|
24
24
|
updated_by TEXT,
|
|
25
25
|
updated_via TEXT,
|
|
26
26
|
deleted_at INTEGER,
|
|
27
|
-
unlisted_at INTEGER
|
|
28
|
-
permissions TEXT CHECK (permissions IS NULL OR json_valid(permissions))
|
|
27
|
+
unlisted_at INTEGER
|
|
29
28
|
) STRICT;
|
|
30
29
|
|
|
30
|
+
-- Every edge a record carries, authority and data alike: a permission
|
|
31
|
+
-- entry has element identity and a delta the same shape as a tag's, so
|
|
32
|
+
-- it earns the same table and the same durability tier. The permissions
|
|
33
|
+
-- and associations fields are projections over it, partitioned by
|
|
34
|
+
-- kind. See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
35
|
+
--
|
|
31
36
|
-- A relationship's target is (related_scope, related_id) plus one
|
|
32
37
|
-- qualifier: related_stack for a record in another stack, related_ns for
|
|
33
38
|
-- a foreign namespace. All four are in the primary key, so two targets
|
|
34
|
-
-- differing only by namespace are two associations.
|
|
39
|
+
-- differing only by namespace are two associations. A permission's
|
|
40
|
+
-- grantee reuses them \u2014 related_scope 'entity' or 'group', related_id the
|
|
41
|
+
-- DID or group id \u2014 plus related_role, which only a group grantee sets
|
|
42
|
+
-- and which is in the key because member and admin name two different
|
|
43
|
+
-- sets of people.
|
|
35
44
|
--
|
|
36
45
|
-- attachment_record_id stays out of the primary key: it annotates a
|
|
37
46
|
-- reference rather than naming one, so re-pointing it updates the row an
|
|
@@ -43,22 +52,21 @@ var RECORD_SCHEMA_SQL = `
|
|
|
43
52
|
-- for the filter directions it cannot answer.
|
|
44
53
|
CREATE TABLE IF NOT EXISTS associations (
|
|
45
54
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
46
|
-
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship')),
|
|
55
|
+
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship', 'permission', 'anyone')),
|
|
47
56
|
label TEXT NOT NULL,
|
|
48
57
|
file_id TEXT NOT NULL DEFAULT '',
|
|
49
|
-
related_scope TEXT NOT NULL DEFAULT '' CHECK (related_scope IN ('', 'record', 'entity', 'external')),
|
|
58
|
+
related_scope TEXT NOT NULL DEFAULT '' CHECK (related_scope IN ('', 'record', 'entity', 'external', 'group')),
|
|
50
59
|
related_id TEXT NOT NULL DEFAULT '',
|
|
51
60
|
related_ns TEXT NOT NULL DEFAULT '',
|
|
52
61
|
related_stack TEXT NOT NULL DEFAULT '',
|
|
62
|
+
related_role TEXT NOT NULL DEFAULT '' CHECK (related_role IN ('', 'member', 'admin')),
|
|
53
63
|
attachment_record_id TEXT NOT NULL DEFAULT '',
|
|
54
|
-
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack)
|
|
64
|
+
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack, related_role)
|
|
55
65
|
) STRICT;
|
|
56
66
|
|
|
57
|
-
--
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
-- No associations column: associate()/dissociate() don't bump version,
|
|
61
|
-
-- so no version ever snapshots the association set. See
|
|
67
|
+
-- Content and the type it is read under. No parent_id, unlisted_at or
|
|
68
|
+
-- associations column: none of those aspects bumps version, so no
|
|
69
|
+
-- version ever snapshots one. See
|
|
62
70
|
-- docs/spec/versioning.md \xA7 Version history.
|
|
63
71
|
CREATE TABLE IF NOT EXISTS versions (
|
|
64
72
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
@@ -69,8 +77,6 @@ var RECORD_SCHEMA_SQL = `
|
|
|
69
77
|
entity_id TEXT,
|
|
70
78
|
updated_by TEXT,
|
|
71
79
|
updated_via TEXT,
|
|
72
|
-
parent_id TEXT,
|
|
73
|
-
permissions TEXT CHECK (permissions IS NULL OR json_valid(permissions)),
|
|
74
80
|
PRIMARY KEY (record_id, version)
|
|
75
81
|
) STRICT;
|
|
76
82
|
|
|
@@ -78,17 +84,15 @@ var RECORD_SCHEMA_SQL = `
|
|
|
78
84
|
-- them, and the association deltas nothing else retains. Envelope-level
|
|
79
85
|
-- by design \u2014 content lives on a versions row, and copying it here
|
|
80
86
|
-- would make the journal the larger of the two stores.
|
|
81
|
-
--
|
|
82
|
-
-- seq is dense from 1 per record and is the only ordering: at is wall
|
|
83
|
-
-- clock, and version stands still across an association change, so
|
|
84
|
-
-- neither orders the log alone. It is allocated inside the appending
|
|
85
|
-
-- write, which is why this table needs none of the collision healing
|
|
86
|
-
-- versions does. See docs/spec/versioning.md \xA7 The change journal.
|
|
87
|
+
-- See docs/spec/journal.md.
|
|
87
88
|
CREATE TABLE IF NOT EXISTS journal (
|
|
88
89
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
89
90
|
seq INTEGER NOT NULL,
|
|
90
91
|
at INTEGER NOT NULL,
|
|
91
|
-
|
|
92
|
+
-- No 'purged': a purge destroys the log rather than appending to it,
|
|
93
|
+
-- so the constraint names exactly the kinds an entry can carry.
|
|
94
|
+
-- See docs/spec/journal.md \xA7 A hard delete destroys the journal.
|
|
95
|
+
kind TEXT NOT NULL CHECK (kind IN ('created', 'changed', 'deleted')),
|
|
92
96
|
ops TEXT NOT NULL CHECK (json_valid(ops)),
|
|
93
97
|
version INTEGER NOT NULL,
|
|
94
98
|
type_id TEXT NOT NULL,
|
|
@@ -99,9 +103,9 @@ var RECORD_SCHEMA_SQL = `
|
|
|
99
103
|
-- the same input/state split parentId follows everywhere.
|
|
100
104
|
previous_parent_id TEXT,
|
|
101
105
|
actor TEXT CHECK (actor IS NULL OR json_valid(actor)),
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
-- One tagged edit per association the write moved, each carrying the
|
|
107
|
+
-- state it displaced. See docs/spec/journal.md \xA7 The entry.
|
|
108
|
+
associations TEXT CHECK (associations IS NULL OR json_valid(associations)),
|
|
105
109
|
PRIMARY KEY (record_id, seq)
|
|
106
110
|
) STRICT;
|
|
107
111
|
|
|
@@ -584,6 +588,10 @@ var buildQueryPlan = (query) => {
|
|
|
584
588
|
// ../sqlite-shared/dist/mappers.js
|
|
585
589
|
var toMs = (d) => d.getTime();
|
|
586
590
|
var fromMs = (ms) => new Date(ms);
|
|
591
|
+
var partitionAssociations = (associations) => ({
|
|
592
|
+
associations: associations.filter((a) => a.kind !== "permission" && a.kind !== "anyone"),
|
|
593
|
+
permissions: associations.filter((a) => a.kind === "permission" || a.kind === "anyone")
|
|
594
|
+
});
|
|
587
595
|
var rowToRecord = (row, associations) => {
|
|
588
596
|
const record = {
|
|
589
597
|
id: row.id,
|
|
@@ -609,16 +617,27 @@ var rowToRecord = (row, associations) => {
|
|
|
609
617
|
record.deletedAt = fromMs(row.deleted_at);
|
|
610
618
|
if (row.unlisted_at != null)
|
|
611
619
|
record.unlistedAt = fromMs(row.unlisted_at);
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
620
|
+
const partitioned = partitionAssociations(associations);
|
|
621
|
+
if (partitioned.associations.length)
|
|
622
|
+
record.associations = partitioned.associations;
|
|
623
|
+
if (partitioned.permissions.length)
|
|
624
|
+
record.permissions = partitioned.permissions;
|
|
616
625
|
return record;
|
|
617
626
|
};
|
|
618
627
|
var rowToAssociation = (row) => {
|
|
619
628
|
if (row.kind === "tag") {
|
|
620
629
|
return { kind: "tag", label: row.label };
|
|
621
630
|
}
|
|
631
|
+
if (row.kind === "anyone") {
|
|
632
|
+
return { kind: "anyone", label: "read" };
|
|
633
|
+
}
|
|
634
|
+
if (row.kind === "permission") {
|
|
635
|
+
return {
|
|
636
|
+
kind: "permission",
|
|
637
|
+
label: row.label,
|
|
638
|
+
grantee: rowToGrantee(row)
|
|
639
|
+
};
|
|
640
|
+
}
|
|
622
641
|
if (row.kind === "attachment") {
|
|
623
642
|
const attachmentRecordId = row.attachment_record_id;
|
|
624
643
|
return {
|
|
@@ -639,16 +658,25 @@ var rowToAssociation = (row) => {
|
|
|
639
658
|
};
|
|
640
659
|
var associationKeyColumns = (a) => {
|
|
641
660
|
if (a.kind === "attachment")
|
|
642
|
-
return [a.fileId, "", "", "", ""];
|
|
661
|
+
return [a.fileId, "", "", "", "", ""];
|
|
662
|
+
if (a.kind === "permission") {
|
|
663
|
+
const g = a.grantee;
|
|
664
|
+
return g.scope === "entity" ? ["", "entity", g.entityId, "", "", ""] : ["", "group", g.groupId, "", "", g.role];
|
|
665
|
+
}
|
|
643
666
|
if (a.kind !== "relationship")
|
|
644
|
-
return ["", "", "", "", ""];
|
|
667
|
+
return ["", "", "", "", "", ""];
|
|
645
668
|
const t = a.target;
|
|
646
669
|
if (t.scope === "entity")
|
|
647
|
-
return ["", "entity", t.entityId, "", ""];
|
|
670
|
+
return ["", "entity", t.entityId, "", "", ""];
|
|
648
671
|
if (t.scope === "external")
|
|
649
|
-
return ["", "external", t.id, t.ns, ""];
|
|
650
|
-
return ["", "record", t.recordId, "", t.stackUrl ?? ""];
|
|
672
|
+
return ["", "external", t.id, t.ns, "", ""];
|
|
673
|
+
return ["", "record", t.recordId, "", t.stackUrl ?? "", ""];
|
|
651
674
|
};
|
|
675
|
+
var rowToGrantee = (row) => row.related_scope === "group" ? {
|
|
676
|
+
scope: "group",
|
|
677
|
+
groupId: row.related_id,
|
|
678
|
+
role: row.related_role
|
|
679
|
+
} : { scope: "entity", entityId: row.related_id };
|
|
652
680
|
var rowToTarget = (row) => {
|
|
653
681
|
const id = row.related_id;
|
|
654
682
|
if (row.related_scope === "entity")
|
|
@@ -686,10 +714,6 @@ var rowToVersion = (row) => {
|
|
|
686
714
|
v.updatedBy = row.updated_by;
|
|
687
715
|
if (row.updated_via != null)
|
|
688
716
|
v.updatedVia = row.updated_via;
|
|
689
|
-
if (row.parent_id != null)
|
|
690
|
-
v.parentId = row.parent_id;
|
|
691
|
-
if (row.permissions != null)
|
|
692
|
-
v.permissions = JSON.parse(row.permissions);
|
|
693
717
|
return v;
|
|
694
718
|
};
|
|
695
719
|
var rowToJournalEntry = (row) => {
|
|
@@ -708,15 +732,8 @@ var rowToJournalEntry = (row) => {
|
|
|
708
732
|
}
|
|
709
733
|
if (row.actor != null)
|
|
710
734
|
e.actor = JSON.parse(row.actor);
|
|
711
|
-
if (row.
|
|
712
|
-
e.
|
|
713
|
-
}
|
|
714
|
-
if (row.associations_removed != null) {
|
|
715
|
-
e.associationsRemoved = JSON.parse(row.associations_removed);
|
|
716
|
-
}
|
|
717
|
-
if (row.associations_replaced != null) {
|
|
718
|
-
e.associationsReplaced = JSON.parse(row.associations_replaced);
|
|
719
|
-
}
|
|
735
|
+
if (row.associations != null)
|
|
736
|
+
e.associations = JSON.parse(row.associations);
|
|
720
737
|
return e;
|
|
721
738
|
};
|
|
722
739
|
|
|
@@ -761,9 +778,7 @@ var VERSION_COLUMNS = [
|
|
|
761
778
|
"updated_at",
|
|
762
779
|
"entity_id",
|
|
763
780
|
"updated_by",
|
|
764
|
-
"updated_via"
|
|
765
|
-
"parent_id",
|
|
766
|
-
"permissions"
|
|
781
|
+
"updated_via"
|
|
767
782
|
];
|
|
768
783
|
var versionRowValues = (version) => [
|
|
769
784
|
version.typeId,
|
|
@@ -771,9 +786,7 @@ var versionRowValues = (version) => [
|
|
|
771
786
|
toMs(version.updatedAt),
|
|
772
787
|
version.entityId ?? null,
|
|
773
788
|
version.updatedBy ?? null,
|
|
774
|
-
version.updatedVia ?? null
|
|
775
|
-
version.parentId ?? null,
|
|
776
|
-
version.permissions ? JSON.stringify(version.permissions) : null
|
|
789
|
+
version.updatedVia ?? null
|
|
777
790
|
];
|
|
778
791
|
var indexedFieldKinds = (schema) => {
|
|
779
792
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -784,7 +797,7 @@ var indexedFieldKinds = (schema) => {
|
|
|
784
797
|
}
|
|
785
798
|
return kinds;
|
|
786
799
|
};
|
|
787
|
-
var SharedSqlRecordLogic = class {
|
|
800
|
+
var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
788
801
|
deps;
|
|
789
802
|
/**
|
|
790
803
|
* Per-typeId cache of the fields content_index covers and their declared
|
|
@@ -890,8 +903,8 @@ var SharedSqlRecordLogic = class {
|
|
|
890
903
|
this.exec.run(`INSERT INTO records
|
|
891
904
|
(id, type_id, created_at, updated_at, content, version,
|
|
892
905
|
parent_id, entity_id, app_id, principal_id, updated_by, updated_via,
|
|
893
|
-
deleted_at, unlisted_at
|
|
894
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
906
|
+
deleted_at, unlisted_at)
|
|
907
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
895
908
|
record.id,
|
|
896
909
|
record.typeId,
|
|
897
910
|
toMs(record.createdAt),
|
|
@@ -905,8 +918,7 @@ var SharedSqlRecordLogic = class {
|
|
|
905
918
|
record.updatedBy ?? null,
|
|
906
919
|
record.updatedVia ?? null,
|
|
907
920
|
record.deletedAt ? toMs(record.deletedAt) : null,
|
|
908
|
-
record.unlistedAt ? toMs(record.unlistedAt) : null
|
|
909
|
-
record.permissions ? JSON.stringify(record.permissions) : null
|
|
921
|
+
record.unlistedAt ? toMs(record.unlistedAt) : null
|
|
910
922
|
]);
|
|
911
923
|
} catch (err) {
|
|
912
924
|
if (isUniqueConstraintViolation(err)) {
|
|
@@ -914,9 +926,9 @@ var SharedSqlRecordLogic = class {
|
|
|
914
926
|
}
|
|
915
927
|
throw err;
|
|
916
928
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
929
|
+
const edges = [...record.associations ?? [], ...record.permissions ?? []];
|
|
930
|
+
if (edges.length)
|
|
931
|
+
this.insertAssociations(record.id, edges);
|
|
920
932
|
fts5Strategy.insert(this.exec, record.id, JSON.stringify(record.content));
|
|
921
933
|
this.syncContentIndex(record.id, record.typeId, record.content);
|
|
922
934
|
this.appendJournal(record.id, opts.journal);
|
|
@@ -954,13 +966,13 @@ var SharedSqlRecordLogic = class {
|
|
|
954
966
|
* one call is one version whatever it moved.
|
|
955
967
|
*
|
|
956
968
|
* `opts.bumpsVersion: false` is a change set that touches only
|
|
957
|
-
* `associations` — Stack computes this from
|
|
958
|
-
* moves. Such a write
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
969
|
+
* `associations`, `parentId` and/or `unlisted` — Stack computes this from
|
|
970
|
+
* which ops the set actually moves. Such a write still lands its columns,
|
|
971
|
+
* but leaves `version`, `updatedAt` and the actor stamps exactly as they
|
|
972
|
+
* stood and takes no snapshot. `expectedVersion`, if given, is re-checked
|
|
973
|
+
* synchronously inside the transaction — the CAS guard the bumping path
|
|
974
|
+
* gets from its UPDATE's WHERE clause, restated as a read because the
|
|
975
|
+
* UPDATE here carries no version predicate. See
|
|
964
976
|
* docs/spec/versioning.md § Version history.
|
|
965
977
|
*
|
|
966
978
|
* The expectedVersion check for a bumping write is standalone rather than
|
|
@@ -974,38 +986,35 @@ var SharedSqlRecordLogic = class {
|
|
|
974
986
|
if (!existing)
|
|
975
987
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
976
988
|
this.checkExpectedVersion(existing, opts.expectedVersion);
|
|
989
|
+
const now = toMs(/* @__PURE__ */ new Date());
|
|
990
|
+
const sets = [];
|
|
991
|
+
const values = [];
|
|
992
|
+
if (changes.parentId !== void 0) {
|
|
993
|
+
sets.push("parent_id = ?");
|
|
994
|
+
values.push(changes.parentId);
|
|
995
|
+
}
|
|
996
|
+
if (changes.unlisted !== void 0) {
|
|
997
|
+
sets.push("unlisted_at = ?");
|
|
998
|
+
values.push(changes.unlisted ? now : null);
|
|
999
|
+
}
|
|
977
1000
|
if (opts.bumpsVersion === false) {
|
|
978
1001
|
this.exec.transaction(() => {
|
|
979
1002
|
const current = this.readRecord(id);
|
|
980
1003
|
if (!current)
|
|
981
1004
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
982
1005
|
this.checkExpectedVersion(current, opts.expectedVersion);
|
|
983
|
-
if (
|
|
984
|
-
this.
|
|
1006
|
+
if (sets.length > 0) {
|
|
1007
|
+
this.exec.run(`UPDATE records SET ${sets.join(", ")} WHERE id = ?`, [...values, id]);
|
|
985
1008
|
}
|
|
1009
|
+
this.replaceAssociationHalves(id, changes);
|
|
986
1010
|
this.appendJournal(id, opts.journal);
|
|
987
1011
|
});
|
|
988
1012
|
return this.reread(id, "mutateRecord");
|
|
989
1013
|
}
|
|
990
1014
|
const merged = changes.contentPatch ? applyMergePatch(existing.content, changes.contentPatch) : void 0;
|
|
991
|
-
const sets = [];
|
|
992
|
-
const values = [];
|
|
993
1015
|
if (merged !== void 0) {
|
|
994
|
-
sets.
|
|
995
|
-
values.
|
|
996
|
-
}
|
|
997
|
-
if (changes.parentId !== void 0) {
|
|
998
|
-
sets.push("parent_id = ?");
|
|
999
|
-
values.push(changes.parentId);
|
|
1000
|
-
}
|
|
1001
|
-
if (changes.permissions !== void 0) {
|
|
1002
|
-
sets.push("permissions = ?");
|
|
1003
|
-
values.push(changes.permissions.length ? JSON.stringify(changes.permissions) : null);
|
|
1004
|
-
}
|
|
1005
|
-
const now = toMs(/* @__PURE__ */ new Date());
|
|
1006
|
-
if (changes.unlisted !== void 0) {
|
|
1007
|
-
sets.push("unlisted_at = ?");
|
|
1008
|
-
values.push(changes.unlisted ? now : null);
|
|
1016
|
+
sets.unshift("content = ?");
|
|
1017
|
+
values.unshift(JSON.stringify(merged));
|
|
1009
1018
|
}
|
|
1010
1019
|
this.exec.transaction(() => {
|
|
1011
1020
|
if (opts.snapshot)
|
|
@@ -1013,9 +1022,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1013
1022
|
if (merged !== void 0)
|
|
1014
1023
|
fts5Strategy.remove(this.exec, id);
|
|
1015
1024
|
this.versionedUpdate(id, sets, values, opts, now);
|
|
1016
|
-
|
|
1017
|
-
this.replaceAssociations(id, changes.associations);
|
|
1018
|
-
}
|
|
1025
|
+
this.replaceAssociationHalves(id, changes);
|
|
1019
1026
|
if (merged !== void 0) {
|
|
1020
1027
|
fts5Strategy.insert(this.exec, id, JSON.stringify(merged));
|
|
1021
1028
|
this.syncContentIndex(id, existing.typeId, merged);
|
|
@@ -1074,15 +1081,15 @@ var SharedSqlRecordLogic = class {
|
|
|
1074
1081
|
async restoreVersion(id, version, opts = {}) {
|
|
1075
1082
|
const existing = await this.getRecord(id);
|
|
1076
1083
|
if (!existing)
|
|
1077
|
-
throw new
|
|
1084
|
+
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1078
1085
|
this.checkExpectedVersion(existing, opts.expectedVersion);
|
|
1079
1086
|
const target = await this.getVersion(id, version);
|
|
1080
1087
|
if (!target)
|
|
1081
|
-
throw new
|
|
1088
|
+
throw new StackNotFoundError(`Version not found: "${id}"@${version}`);
|
|
1082
1089
|
this.exec.transaction(() => {
|
|
1083
1090
|
if (opts.snapshot)
|
|
1084
1091
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
1085
|
-
this.rewriteContent(id, target.typeId, target.content, opts
|
|
1092
|
+
this.rewriteContent(id, target.typeId, target.content, opts);
|
|
1086
1093
|
this.appendJournal(id, opts.journal);
|
|
1087
1094
|
});
|
|
1088
1095
|
return this.reread(id, "restoreVersion");
|
|
@@ -1090,7 +1097,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1090
1097
|
async commitMigration(id, toTypeId, content, opts = {}) {
|
|
1091
1098
|
const existing = await this.getRecord(id);
|
|
1092
1099
|
if (!existing)
|
|
1093
|
-
throw new
|
|
1100
|
+
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1094
1101
|
this.checkExpectedVersion(existing, opts.expectedVersion);
|
|
1095
1102
|
this.exec.transaction(() => {
|
|
1096
1103
|
if (opts.snapshot)
|
|
@@ -1102,8 +1109,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1102
1109
|
}
|
|
1103
1110
|
/**
|
|
1104
1111
|
* Replace a record's type and content wholesale — what restoreVersion and
|
|
1105
|
-
* commitMigration each do, differing only in where the content came from
|
|
1106
|
-
* and whether the container moves with it.
|
|
1112
|
+
* commitMigration each do, differing only in where the content came from.
|
|
1107
1113
|
*
|
|
1108
1114
|
* The order is the constraint: fts5Strategy.remove() has to read the old
|
|
1109
1115
|
* content off the records row, so it runs before the UPDATE, and the
|
|
@@ -1111,10 +1117,10 @@ var SharedSqlRecordLogic = class {
|
|
|
1111
1117
|
* exec.transaction() — a failure between the two halves would otherwise
|
|
1112
1118
|
* leave the record unindexed.
|
|
1113
1119
|
*/
|
|
1114
|
-
rewriteContent(id, typeId, content, opts
|
|
1120
|
+
rewriteContent(id, typeId, content, opts) {
|
|
1115
1121
|
const json = JSON.stringify(content);
|
|
1116
1122
|
fts5Strategy.remove(this.exec, id);
|
|
1117
|
-
this.versionedUpdate(id, ["type_id = ?", "content = ?"
|
|
1123
|
+
this.versionedUpdate(id, ["type_id = ?", "content = ?"], [typeId, json], opts);
|
|
1118
1124
|
fts5Strategy.insert(this.exec, id, json);
|
|
1119
1125
|
this.syncContentIndex(id, typeId, content);
|
|
1120
1126
|
}
|
|
@@ -1249,7 +1255,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1249
1255
|
* `seq` is allocated here, from the log's own max, rather than computed
|
|
1250
1256
|
* by the caller. That is what spares this table the collision healing
|
|
1251
1257
|
* versions needs: nothing outside this statement ever holds a seq it
|
|
1252
|
-
* expects to be free. See docs/spec/
|
|
1258
|
+
* expects to be free. See docs/spec/journal.md § Ordering.
|
|
1253
1259
|
*/
|
|
1254
1260
|
appendJournal(recordId, entry) {
|
|
1255
1261
|
if (!entry)
|
|
@@ -1259,12 +1265,11 @@ var SharedSqlRecordLogic = class {
|
|
|
1259
1265
|
throw new StackNotFoundError(`Record not found: "${recordId}"`);
|
|
1260
1266
|
this.exec.run(`INSERT INTO journal
|
|
1261
1267
|
(record_id, seq, at, kind, ops, version, type_id, parent_id,
|
|
1262
|
-
previous_parent_id, actor,
|
|
1263
|
-
associations_replaced)
|
|
1268
|
+
previous_parent_id, actor, associations)
|
|
1264
1269
|
VALUES (
|
|
1265
1270
|
?,
|
|
1266
1271
|
(SELECT COALESCE(MAX(seq), 0) + 1 FROM journal WHERE record_id = ?),
|
|
1267
|
-
?, ?, ?, ?, ?, ?, ?, ?,
|
|
1272
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1268
1273
|
recordId,
|
|
1269
1274
|
recordId,
|
|
1270
1275
|
toMs(/* @__PURE__ */ new Date()),
|
|
@@ -1276,12 +1281,13 @@ var SharedSqlRecordLogic = class {
|
|
|
1276
1281
|
// '' is the root; NULL is an entry that names no origin at all.
|
|
1277
1282
|
entry.previousParentId === void 0 ? null : entry.previousParentId ?? "",
|
|
1278
1283
|
entry.actor ? JSON.stringify(entry.actor) : null,
|
|
1279
|
-
entry.
|
|
1280
|
-
entry.associationsRemoved?.length ? JSON.stringify(entry.associationsRemoved) : null,
|
|
1281
|
-
entry.associationsReplaced?.length ? JSON.stringify(entry.associationsReplaced) : null
|
|
1284
|
+
entry.associations?.length ? JSON.stringify(entry.associations) : null
|
|
1282
1285
|
]);
|
|
1283
1286
|
}
|
|
1284
1287
|
async getJournal(id, query = {}) {
|
|
1288
|
+
if (!this.exec.get("SELECT 1 FROM records WHERE id = ?", [id])) {
|
|
1289
|
+
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1290
|
+
}
|
|
1285
1291
|
const conditions = ["record_id = ?"];
|
|
1286
1292
|
const values = [id];
|
|
1287
1293
|
if (query.sinceSeq !== void 0) {
|
|
@@ -1322,11 +1328,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1322
1328
|
// Associations
|
|
1323
1329
|
// -------------------------------------------------------
|
|
1324
1330
|
/**
|
|
1325
|
-
* Never bumps `version`/`updatedAt` and never snapshots —
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
* associations table changes. See docs/spec/versioning.md § Version
|
|
1329
|
-
* history.
|
|
1331
|
+
* Never bumps `version`/`updatedAt` and never snapshots — see
|
|
1332
|
+
* StackRecordAdapter.associate(). The records row is untouched entirely;
|
|
1333
|
+
* only the associations table changes.
|
|
1330
1334
|
*/
|
|
1331
1335
|
async associate(recordId, association, opts = {}) {
|
|
1332
1336
|
this.exec.transaction(() => {
|
|
@@ -1350,19 +1354,37 @@ var SharedSqlRecordLogic = class {
|
|
|
1350
1354
|
AND related_scope = ?
|
|
1351
1355
|
AND related_id = ?
|
|
1352
1356
|
AND related_ns = ?
|
|
1353
|
-
AND related_stack =
|
|
1357
|
+
AND related_stack = ?
|
|
1358
|
+
AND related_role = ?`, [recordId, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1354
1359
|
this.appendJournal(recordId, opts.journal);
|
|
1355
1360
|
});
|
|
1356
1361
|
return this.reread(recordId, "dissociate");
|
|
1357
1362
|
}
|
|
1363
|
+
/** The kinds each change-set key replaces, and nothing else. */
|
|
1364
|
+
static DATA_KINDS = ["tag", "attachment", "relationship"];
|
|
1365
|
+
static AUTHORITY_KINDS = ["permission", "anyone"];
|
|
1366
|
+
/**
|
|
1367
|
+
* Apply a change set's two association keys, each replacing only within
|
|
1368
|
+
* its own half of the partition. That partition is the whole reason a
|
|
1369
|
+
* tag write cannot drop an ACL: the `associations` key deletes data
|
|
1370
|
+
* kinds alone, so the authority rows beside them are never in scope.
|
|
1371
|
+
* Caller-transactional, like every write below.
|
|
1372
|
+
* See docs/spec/access-control.md § Record-level permissions.
|
|
1373
|
+
*/
|
|
1374
|
+
replaceAssociationHalves(recordId, changes) {
|
|
1375
|
+
if (changes.associations !== void 0) {
|
|
1376
|
+
this.replaceAssociations(recordId, changes.associations, _SharedSqlRecordLogic.DATA_KINDS);
|
|
1377
|
+
}
|
|
1378
|
+
if (changes.permissions !== void 0) {
|
|
1379
|
+
this.replaceAssociations(recordId, changes.permissions, _SharedSqlRecordLogic.AUTHORITY_KINDS);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1358
1382
|
/**
|
|
1359
|
-
* Swap a record's association set
|
|
1360
|
-
*
|
|
1361
|
-
* version snapshot's both carry. Caller-transactional, like every write
|
|
1362
|
-
* below.
|
|
1383
|
+
* Swap one half of a record's association set wholesale — the "replace,
|
|
1384
|
+
* don't merge" semantics a change set's association list carries.
|
|
1363
1385
|
*/
|
|
1364
|
-
replaceAssociations(recordId, associations) {
|
|
1365
|
-
this.exec.run(
|
|
1386
|
+
replaceAssociations(recordId, associations, kinds) {
|
|
1387
|
+
this.exec.run(`DELETE FROM associations WHERE record_id = ? AND kind IN (${kinds.map(() => "?").join(", ")})`, [recordId, ...kinds]);
|
|
1366
1388
|
if (associations.length)
|
|
1367
1389
|
this.insertAssociations(recordId, associations);
|
|
1368
1390
|
}
|
|
@@ -1381,8 +1403,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1381
1403
|
try {
|
|
1382
1404
|
this.exec.run(`INSERT INTO associations
|
|
1383
1405
|
(record_id, kind, label, file_id, related_scope, related_id, related_ns,
|
|
1384
|
-
related_stack, attachment_record_id)
|
|
1385
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1406
|
+
related_stack, related_role, attachment_record_id)
|
|
1407
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1386
1408
|
ON CONFLICT DO UPDATE SET attachment_record_id = excluded.attachment_record_id`, [
|
|
1387
1409
|
recordId,
|
|
1388
1410
|
assoc.kind,
|