@haverstack/record-adapter-do-sqlite 0.28.0 → 0.30.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 +20 -16
- package/dist/index.js +211 -132
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { StackRecordAdapter, StoreWriters, JournalOptions, RecordHistory, MutateOptions, AspectWriteOptions, AdapterQuery, Reach, ElementWriteOptions, StoreSeed } from '@haverstack/core/adapter';
|
|
2
|
+
import { StackCapabilities, ConfigContent, StackRecord, TypeId, QueryResult, FileId, BaseId, VersionsQuery, RecordVersion, JournalQuery, RecordJournalEntry, StackType, AssociationEdit, DataAssociation, AuthorityAssociation } from '@haverstack/core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Normalizes a SQLite binding's call convention (array-bind-then-step,
|
|
@@ -31,12 +31,6 @@ interface SqlExecutor {
|
|
|
31
31
|
transaction<T>(fn: () => T): T;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/** timezone is optional passthrough app metadata — no default. */
|
|
35
|
-
type StackConfig = {
|
|
36
|
-
entityId: string;
|
|
37
|
-
timezone: string | undefined;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
34
|
/**
|
|
41
35
|
* The engine-independent half of a SQLite-backed StackRecordAdapter: the
|
|
42
36
|
* capability declaration and the delegation of all twenty-three
|
|
@@ -57,10 +51,15 @@ type StackConfig = {
|
|
|
57
51
|
declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
|
|
58
52
|
readonly capabilities: StackCapabilities;
|
|
59
53
|
readonly ownerEntityId: string;
|
|
60
|
-
|
|
54
|
+
/** Each engine holds its database for one process; see docs/spec/adapters.md § Concurrency & storage ownership. */
|
|
55
|
+
readonly writers: StoreWriters;
|
|
61
56
|
private readonly record;
|
|
62
|
-
|
|
57
|
+
/** Moved by every write method; see head(). */
|
|
58
|
+
private readonly written;
|
|
59
|
+
protected constructor(exec: SqlExecutor, config: ConfigContent);
|
|
60
|
+
head(): Promise<string>;
|
|
63
61
|
createRecord(record: StackRecord, opts?: JournalOptions): Promise<StackRecord>;
|
|
62
|
+
importRecord(record: StackRecord, history: RecordHistory): Promise<StackRecord>;
|
|
64
63
|
getRecord(id: string): Promise<StackRecord | null>;
|
|
65
64
|
patchContent(id: string, patch: Record<string, unknown | null>, opts?: MutateOptions): Promise<StackRecord>;
|
|
66
65
|
setParent(id: string, parentId: string | null, opts?: AspectWriteOptions): Promise<StackRecord>;
|
|
@@ -76,7 +75,6 @@ declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
|
|
|
76
75
|
deleteUnreferencedAttachmentRecords(fileId: FileId, metadataBaseId: BaseId): Promise<StackRecord[]>;
|
|
77
76
|
getVersions(id: string, query?: VersionsQuery): Promise<RecordVersion[]>;
|
|
78
77
|
getVersion(id: string, version: number): Promise<RecordVersion | null>;
|
|
79
|
-
saveVersion(id: string, version: RecordVersion): Promise<void>;
|
|
80
78
|
getJournal(id: string, query?: JournalQuery): Promise<RecordJournalEntry[]>;
|
|
81
79
|
saveType(type: StackType): Promise<void>;
|
|
82
80
|
getType(id: TypeId): Promise<StackType | null>;
|
|
@@ -149,17 +147,23 @@ type DoSQLiteRecordAdapterOpenOptions = {
|
|
|
149
147
|
* provider is called only when the DO's storage has none yet.
|
|
150
148
|
*/
|
|
151
149
|
ownerEntityId: string | (() => string | Promise<string>);
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
seed?: never;
|
|
151
|
+
} | {
|
|
152
|
+
/**
|
|
153
|
+
* The `_config` record and history new storage is created holding: a
|
|
154
|
+
* copy's source's. Storage that already holds a store refuses it.
|
|
155
|
+
*/
|
|
156
|
+
seed: StoreSeed;
|
|
157
|
+
ownerEntityId?: never;
|
|
154
158
|
};
|
|
155
159
|
declare class DoSQLiteRecordAdapter extends SharedSqlRecordAdapter {
|
|
156
160
|
private constructor();
|
|
157
161
|
/**
|
|
158
162
|
* Open the adapter for a DO instance, creating its storage on first use.
|
|
159
163
|
* A DO id either already has a config record (reattach) or it doesn't
|
|
160
|
-
* (first call — opts.ownerEntityId
|
|
161
|
-
* is no `create` option.
|
|
162
|
-
*
|
|
164
|
+
* (first call — opts.ownerEntityId becomes the config), so there
|
|
165
|
+
* is no `create` option. A new store's tables are written in the same
|
|
166
|
+
* transaction as its config record, so storage holds a whole store or none.
|
|
163
167
|
*
|
|
164
168
|
* DO SQLite manages its own durability and rejects PRAGMA journal_mode
|
|
165
169
|
* outright ("not authorized") — verified against the real runtime, not
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { trackWrites, OwnerMismatchError, newStoreSeed, softDeletedRefusal, sameAssociationSet, ElementSetMovedError, FormatMismatchError, contentSortEntry, constantSortEntry, contentSortKey, parseContentFilterKey } from '@haverstack/core/adapter';
|
|
1
2
|
import { StackConflictError, applyMergePatch, StackNotFoundError, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
2
|
-
import { OwnerMismatchError, softDeletedRefusal, sameAssociationSet, ElementSetMovedError, contentSortEntry, constantSortEntry, contentSortKey, parseContentFilterKey } from '@haverstack/core/adapter';
|
|
3
3
|
|
|
4
4
|
// ../sqlite-shared/dist/schema.js
|
|
5
|
+
var FORMAT_VERSION = 1;
|
|
5
6
|
var RECORD_SCHEMA_SQL = `
|
|
6
|
-
|
|
7
|
+
-- Which layout this store is, in its one row, so an export names the one
|
|
8
|
+
-- a reader holds. See docs/spec/portability.md \xA7 The portable format.
|
|
9
|
+
CREATE TABLE format (
|
|
10
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
11
|
+
version INTEGER NOT NULL
|
|
12
|
+
) STRICT;
|
|
13
|
+
|
|
14
|
+
CREATE TABLE records (
|
|
7
15
|
id TEXT PRIMARY KEY,
|
|
8
16
|
type_id TEXT NOT NULL,
|
|
9
17
|
created_at INTEGER NOT NULL,
|
|
@@ -23,7 +31,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
23
31
|
-- A record's data associations: tags, attachments and relationships.
|
|
24
32
|
-- Its permissions are in their own table below, so no predicate over this
|
|
25
33
|
-- one can match a grantee as a relationship target.
|
|
26
|
-
CREATE TABLE
|
|
34
|
+
CREATE TABLE associations (
|
|
27
35
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
28
36
|
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship')),
|
|
29
37
|
label TEXT NOT NULL,
|
|
@@ -49,7 +57,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
49
57
|
-- grantee; a group grantee's role is in the key, because member and
|
|
50
58
|
-- admin name two different sets of people.
|
|
51
59
|
-- See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
52
|
-
CREATE TABLE
|
|
60
|
+
CREATE TABLE permissions (
|
|
53
61
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
54
62
|
kind TEXT NOT NULL CHECK (kind IN ('permission', 'anyone')),
|
|
55
63
|
label TEXT NOT NULL CHECK (label IN ('read', 'write')),
|
|
@@ -63,7 +71,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
63
71
|
-- associations column: none of those aspects bumps version, so no
|
|
64
72
|
-- version ever snapshots one. See
|
|
65
73
|
-- docs/spec/versioning.md \xA7 Version history.
|
|
66
|
-
CREATE TABLE
|
|
74
|
+
CREATE TABLE versions (
|
|
67
75
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
68
76
|
version INTEGER NOT NULL,
|
|
69
77
|
type_id TEXT NOT NULL,
|
|
@@ -81,7 +89,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
81
89
|
-- by design \u2014 content lives on a versions row, and copying it here
|
|
82
90
|
-- would make the journal the larger of the two stores.
|
|
83
91
|
-- See docs/spec/journal.md.
|
|
84
|
-
CREATE TABLE
|
|
92
|
+
CREATE TABLE journal (
|
|
85
93
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
86
94
|
seq INTEGER NOT NULL,
|
|
87
95
|
at INTEGER NOT NULL,
|
|
@@ -105,7 +113,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
105
113
|
PRIMARY KEY (record_id, seq)
|
|
106
114
|
) STRICT;
|
|
107
115
|
|
|
108
|
-
CREATE TABLE
|
|
116
|
+
CREATE TABLE types (
|
|
109
117
|
id TEXT PRIMARY KEY,
|
|
110
118
|
base_id TEXT NOT NULL,
|
|
111
119
|
version INTEGER NOT NULL,
|
|
@@ -123,7 +131,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
123
131
|
-- value_rank is 0 for a numeric value and 1 for a text one \u2014 the same
|
|
124
132
|
-- split as "num_value IS NULL", but non-null, so one index can carry
|
|
125
133
|
-- the whole ordering. file_id is set only for a 'file-ref' field.
|
|
126
|
-
CREATE TABLE
|
|
134
|
+
CREATE TABLE content_index (
|
|
127
135
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
128
136
|
field TEXT NOT NULL,
|
|
129
137
|
value_rank INTEGER NOT NULL CHECK (value_rank IN (0, 1)),
|
|
@@ -139,39 +147,39 @@ var RECORD_SCHEMA_SQL = `
|
|
|
139
147
|
-- leaves the planner sorting every match before it can honor LIMIT.
|
|
140
148
|
-- deleted_at and unlisted_at get none: an index on a column every query
|
|
141
149
|
-- tests IS NULL only lures the planner off the one answering ORDER BY.
|
|
142
|
-
CREATE INDEX
|
|
143
|
-
CREATE INDEX
|
|
144
|
-
CREATE INDEX
|
|
145
|
-
CREATE INDEX
|
|
146
|
-
CREATE INDEX
|
|
147
|
-
CREATE INDEX
|
|
148
|
-
CREATE INDEX
|
|
149
|
-
CREATE INDEX
|
|
150
|
+
CREATE INDEX idx_records_type_id ON records(type_id, created_at, id);
|
|
151
|
+
CREATE INDEX idx_records_parent_id ON records(parent_id, created_at, id);
|
|
152
|
+
CREATE INDEX idx_records_entity_id ON records(entity_id, created_at, id);
|
|
153
|
+
CREATE INDEX idx_records_app_id ON records(app_id, created_at, id);
|
|
154
|
+
CREATE INDEX idx_records_principal_id ON records(principal_id, created_at, id);
|
|
155
|
+
CREATE INDEX idx_records_created_at ON records(created_at, id);
|
|
156
|
+
CREATE INDEX idx_records_updated_at ON records(updated_at, id);
|
|
157
|
+
CREATE INDEX idx_records_version ON records(version, id);
|
|
150
158
|
|
|
151
159
|
-- Each association index ends with record_id so the semi-joins in
|
|
152
160
|
-- query.ts read the matching record ids out of the index alone.
|
|
153
161
|
-- idx_assoc_related carries related_ns ahead of related_id because a
|
|
154
162
|
-- record- or entity-scoped target always stores '' there, which
|
|
155
163
|
-- buildQueryPlan states explicitly to keep the prefix contiguous.
|
|
156
|
-
CREATE INDEX
|
|
157
|
-
CREATE INDEX
|
|
158
|
-
CREATE INDEX
|
|
159
|
-
CREATE INDEX
|
|
164
|
+
CREATE INDEX idx_assoc_kind_label ON associations(kind, label, record_id);
|
|
165
|
+
CREATE INDEX idx_assoc_kind_file_id ON associations(kind, file_id, record_id);
|
|
166
|
+
CREATE INDEX idx_assoc_related ON associations(kind, related_scope, related_ns, related_id, record_id);
|
|
167
|
+
CREATE INDEX idx_types_base_id ON types(base_id, version);
|
|
160
168
|
|
|
161
169
|
-- The permissions key with record_id moved last, so a scoped query reads
|
|
162
170
|
-- the records carrying an element straight out of the index.
|
|
163
|
-
CREATE INDEX
|
|
171
|
+
CREATE INDEX idx_permissions_element
|
|
164
172
|
ON permissions(kind, label, grantee_kind, grantee_id, role, record_id);
|
|
165
173
|
|
|
166
174
|
-- The whole content ordering, in index order, for one field at a time:
|
|
167
175
|
-- a content-sorted page is an ordered walk of a slice of this index.
|
|
168
|
-
CREATE INDEX
|
|
176
|
+
CREATE INDEX idx_content_index_sort
|
|
169
177
|
ON content_index(field, value_rank, num_value, text_key, text_value, record_id);
|
|
170
|
-
CREATE INDEX
|
|
178
|
+
CREATE INDEX idx_content_index_file
|
|
171
179
|
ON content_index(file_id, record_id) WHERE file_id IS NOT NULL;
|
|
172
180
|
`;
|
|
173
181
|
var FTS5_SCHEMA_SQL = `
|
|
174
|
-
CREATE VIRTUAL TABLE
|
|
182
|
+
CREATE VIRTUAL TABLE records_fts USING fts5(
|
|
175
183
|
content,
|
|
176
184
|
content='records',
|
|
177
185
|
content_rowid='rowid',
|
|
@@ -179,10 +187,11 @@ var FTS5_SCHEMA_SQL = `
|
|
|
179
187
|
);
|
|
180
188
|
`;
|
|
181
189
|
var PRAGMA_FOREIGN_KEYS_ON = `PRAGMA foreign_keys = ON;`;
|
|
182
|
-
var
|
|
190
|
+
var PRAGMA_JOURNAL_MODE_WAL = `PRAGMA journal_mode = WAL;`;
|
|
191
|
+
var applyConnectionPragmas = (exec, opts) => {
|
|
183
192
|
exec.exec(PRAGMA_FOREIGN_KEYS_ON);
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
if (opts.wal)
|
|
194
|
+
exec.exec(PRAGMA_JOURNAL_MODE_WAL);
|
|
186
195
|
};
|
|
187
196
|
|
|
188
197
|
// ../sqlite-shared/dist/fts5.js
|
|
@@ -268,22 +277,6 @@ var restoreImplicitAnd = (query) => withPhrasesMasked(query, (masked) => masked.
|
|
|
268
277
|
// ../sqlite-shared/dist/executor.js
|
|
269
278
|
var isForeignKeyViolation = (err) => err instanceof Error && err.message.includes("FOREIGN KEY constraint failed");
|
|
270
279
|
var isUniqueConstraintViolation = (err) => err instanceof Error && err.message.includes("UNIQUE constraint failed");
|
|
271
|
-
|
|
272
|
-
// ../sqlite-shared/dist/config.js
|
|
273
|
-
var CONFIG_ROW_SQL = `SELECT content FROM records WHERE id = '_config'`;
|
|
274
|
-
var insertConfigRecord = (exec, entityId, timezone) => {
|
|
275
|
-
const now = Date.now();
|
|
276
|
-
exec.run(`INSERT INTO records (id, type_id, created_at, updated_at, content, version)
|
|
277
|
-
VALUES ('_config', '_config@1', ?, ?, ?, 1)`, [now, now, JSON.stringify({ entityId, timezone })]);
|
|
278
|
-
return { entityId, timezone };
|
|
279
|
-
};
|
|
280
|
-
var tryReadStackConfig = (exec) => {
|
|
281
|
-
const row = exec.get(CONFIG_ROW_SQL);
|
|
282
|
-
if (!row)
|
|
283
|
-
return null;
|
|
284
|
-
const content = JSON.parse(row.content);
|
|
285
|
-
return { entityId: content.entityId, timezone: content.timezone };
|
|
286
|
-
};
|
|
287
280
|
var toBase64 = (text) => {
|
|
288
281
|
const bytes = new TextEncoder().encode(text);
|
|
289
282
|
let binary = "";
|
|
@@ -953,6 +946,35 @@ var versionRowValues = (version) => [
|
|
|
953
946
|
version.updatedBy?.subjectId ?? null,
|
|
954
947
|
version.updatedBy?.principalId ?? null
|
|
955
948
|
];
|
|
949
|
+
var JOURNAL_COLUMNS = [
|
|
950
|
+
"record_id",
|
|
951
|
+
"seq",
|
|
952
|
+
"at",
|
|
953
|
+
"kind",
|
|
954
|
+
"ops",
|
|
955
|
+
"previous_parent_id",
|
|
956
|
+
"actor",
|
|
957
|
+
"associations",
|
|
958
|
+
"version",
|
|
959
|
+
"type_id",
|
|
960
|
+
"parent_id"
|
|
961
|
+
];
|
|
962
|
+
var INSERT_JOURNAL_SQL = `INSERT INTO journal (${JOURNAL_COLUMNS.join(", ")})
|
|
963
|
+
VALUES (${JOURNAL_COLUMNS.map(() => "?").join(", ")})`;
|
|
964
|
+
var journalValues = (recordId, seq, at, entry, row) => [
|
|
965
|
+
recordId,
|
|
966
|
+
seq,
|
|
967
|
+
toMs(at),
|
|
968
|
+
entry.kind,
|
|
969
|
+
JSON.stringify(entry.ops),
|
|
970
|
+
// '' is the root; NULL is an entry that names no origin at all.
|
|
971
|
+
entry.previousParentId === void 0 ? null : entry.previousParentId ?? "",
|
|
972
|
+
entry.actor ? JSON.stringify(entry.actor) : null,
|
|
973
|
+
entry.associations?.length ? JSON.stringify(entry.associations) : null,
|
|
974
|
+
row.version,
|
|
975
|
+
row.typeId,
|
|
976
|
+
row.parentId
|
|
977
|
+
];
|
|
956
978
|
var SharedSqlRecordLogic = class {
|
|
957
979
|
exec;
|
|
958
980
|
searchIndex;
|
|
@@ -1048,42 +1070,71 @@ var SharedSqlRecordLogic = class {
|
|
|
1048
1070
|
*/
|
|
1049
1071
|
async createRecord(record, opts = {}) {
|
|
1050
1072
|
this.exec.transaction(() => {
|
|
1051
|
-
|
|
1052
|
-
|
|
1073
|
+
this.insertRecord(record);
|
|
1074
|
+
this.appendJournal(record.id, opts.journal);
|
|
1075
|
+
});
|
|
1076
|
+
return record;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* createRecord() carrying the record's history as recorded: its version
|
|
1080
|
+
* rows and journal entries, `seq` and `at` included, in the same
|
|
1081
|
+
* transaction. Appends no entry of its own. See
|
|
1082
|
+
* docs/spec/portability.md § Writing history.
|
|
1083
|
+
*/
|
|
1084
|
+
async importRecord(record, history) {
|
|
1085
|
+
this.exec.transaction(() => this.writeImported(record, history));
|
|
1086
|
+
return record;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* importRecord()'s write, for a caller already inside a transaction:
|
|
1090
|
+
* writeNewStore() writes a store's `_config` seed through it, so the one
|
|
1091
|
+
* Record a store starts with lands as every other Record does.
|
|
1092
|
+
*/
|
|
1093
|
+
writeImported(record, history) {
|
|
1094
|
+
this.insertRecord(record);
|
|
1095
|
+
for (const version of history.versions)
|
|
1096
|
+
this.insertVersionRow(record.id, version);
|
|
1097
|
+
for (const entry of history.journal)
|
|
1098
|
+
this.insertJournalRow(record.id, entry);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* The record row and every table describing it, for a caller already
|
|
1102
|
+
* inside a transaction. A duplicate id maps to StackConflictError.
|
|
1103
|
+
*/
|
|
1104
|
+
insertRecord(record) {
|
|
1105
|
+
try {
|
|
1106
|
+
this.exec.run(`INSERT INTO records
|
|
1053
1107
|
(id, type_id, created_at, updated_at, content, version,
|
|
1054
1108
|
parent_id, entity_id, app_id, principal_id, updated_by, updated_via,
|
|
1055
1109
|
deleted_at, unlisted_at)
|
|
1056
1110
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1076
|
-
throw err;
|
|
1111
|
+
record.id,
|
|
1112
|
+
record.typeId,
|
|
1113
|
+
toMs(record.createdAt),
|
|
1114
|
+
toMs(record.updatedAt),
|
|
1115
|
+
JSON.stringify(record.content),
|
|
1116
|
+
record.version,
|
|
1117
|
+
record.parentId ?? null,
|
|
1118
|
+
record.createdBy?.subjectId ?? null,
|
|
1119
|
+
record.appId ?? null,
|
|
1120
|
+
record.createdBy?.principalId ?? null,
|
|
1121
|
+
record.updatedBy?.subjectId ?? null,
|
|
1122
|
+
record.updatedBy?.principalId ?? null,
|
|
1123
|
+
record.deletedAt ? toMs(record.deletedAt) : null,
|
|
1124
|
+
record.unlistedAt ? toMs(record.unlistedAt) : null
|
|
1125
|
+
]);
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
if (isUniqueConstraintViolation(err)) {
|
|
1128
|
+
throw new StackConflictError(`Record already exists: "${record.id}"`);
|
|
1077
1129
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
this.
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
return record;
|
|
1130
|
+
throw err;
|
|
1131
|
+
}
|
|
1132
|
+
if (record.associations?.length)
|
|
1133
|
+
this.insertAssociations(record.id, record.associations);
|
|
1134
|
+
if (record.permissions?.length)
|
|
1135
|
+
this.insertPermissions(record.id, record.permissions);
|
|
1136
|
+
this.searchIndex.insert(record.id, JSON.stringify(record.content));
|
|
1137
|
+
this.contentIndex.sync(record.id, record.typeId, record.content);
|
|
1087
1138
|
}
|
|
1088
1139
|
async getRecord(id) {
|
|
1089
1140
|
return this.readRecord(id);
|
|
@@ -1308,7 +1359,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1308
1359
|
VALUES (?, ?, ${VERSION_COLUMNS.map(() => "?").join(", ")})`, [id, version.version, ...versionRowValues(version)]);
|
|
1309
1360
|
} catch (err) {
|
|
1310
1361
|
if (isUniqueConstraintViolation(err)) {
|
|
1311
|
-
throw new StackConflictError(`Version ${version.version} already exists for record "${id}"
|
|
1362
|
+
throw new StackConflictError(`Version ${version.version} already exists for record "${id}"`);
|
|
1312
1363
|
}
|
|
1313
1364
|
throw err;
|
|
1314
1365
|
}
|
|
@@ -1324,14 +1375,6 @@ var SharedSqlRecordLogic = class {
|
|
|
1324
1375
|
SET ${VERSION_COLUMNS.map((c) => `${c} = ?`).join(", ")}
|
|
1325
1376
|
WHERE record_id = ? AND version = ?`, [...versionRowValues(version), id, version.version]);
|
|
1326
1377
|
}
|
|
1327
|
-
/**
|
|
1328
|
-
* Standalone snapshot write for tooling and tests — loud on any
|
|
1329
|
-
* collision, since nothing here is about to bump the version. Mutating
|
|
1330
|
-
* methods take a `snapshot` option instead; see snapshotBeforeMutation.
|
|
1331
|
-
*/
|
|
1332
|
-
async saveVersion(id, version) {
|
|
1333
|
-
this.insertVersionRow(id, version);
|
|
1334
|
-
}
|
|
1335
1378
|
/**
|
|
1336
1379
|
* The snapshot half of a mutating method's atomic snapshot-then-mutate
|
|
1337
1380
|
* transaction. A collision is a racing-writer conflict, except an orphan
|
|
@@ -1345,8 +1388,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1345
1388
|
} catch (err) {
|
|
1346
1389
|
if (!(err instanceof StackConflictError))
|
|
1347
1390
|
throw err;
|
|
1348
|
-
if (this.currentVersion(id) !== version.version)
|
|
1349
|
-
throw
|
|
1391
|
+
if (this.currentVersion(id) !== version.version) {
|
|
1392
|
+
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.`);
|
|
1393
|
+
}
|
|
1350
1394
|
this.overwriteVersionRow(id, version);
|
|
1351
1395
|
}
|
|
1352
1396
|
}
|
|
@@ -1355,9 +1399,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1355
1399
|
// -------------------------------------------------------
|
|
1356
1400
|
/**
|
|
1357
1401
|
* Append one entry, stamped from the row as it now stands, so callers run
|
|
1358
|
-
* this inside their write's transaction, after the write. `seq` is
|
|
1359
|
-
*
|
|
1360
|
-
* to be free. See docs/spec/journal.md § Ordering.
|
|
1402
|
+
* this inside their write's transaction, after the write. `seq` is read
|
|
1403
|
+
* from the log's maximum in that same transaction, so no caller ever holds
|
|
1404
|
+
* one it expects to be free. See docs/spec/journal.md § Ordering.
|
|
1361
1405
|
*/
|
|
1362
1406
|
appendJournal(recordId, entry) {
|
|
1363
1407
|
if (!entry)
|
|
@@ -1365,26 +1409,20 @@ var SharedSqlRecordLogic = class {
|
|
|
1365
1409
|
const row = this.exec.get("SELECT version, type_id, parent_id FROM records WHERE id = ?", [recordId]);
|
|
1366
1410
|
if (!row)
|
|
1367
1411
|
throw recordNotFound(recordId);
|
|
1368
|
-
this.exec.
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
entry.
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
row.parent_id,
|
|
1383
|
-
// '' is the root; NULL is an entry that names no origin at all.
|
|
1384
|
-
entry.previousParentId === void 0 ? null : entry.previousParentId ?? "",
|
|
1385
|
-
entry.actor ? JSON.stringify(entry.actor) : null,
|
|
1386
|
-
entry.associations?.length ? JSON.stringify(entry.associations) : null
|
|
1387
|
-
]);
|
|
1412
|
+
const { seq } = this.exec.get("SELECT COALESCE(MAX(seq), 0) + 1 AS seq FROM journal WHERE record_id = ?", [recordId]);
|
|
1413
|
+
this.exec.run(INSERT_JOURNAL_SQL, journalValues(recordId, seq, /* @__PURE__ */ new Date(), entry, {
|
|
1414
|
+
version: row.version,
|
|
1415
|
+
typeId: row.type_id,
|
|
1416
|
+
parentId: row.parent_id
|
|
1417
|
+
}));
|
|
1418
|
+
}
|
|
1419
|
+
/** A whole entry as recorded, `seq` and `at` included — importRecord()'s half of the journal. */
|
|
1420
|
+
insertJournalRow(recordId, entry) {
|
|
1421
|
+
this.exec.run(INSERT_JOURNAL_SQL, journalValues(recordId, entry.seq, entry.at, entry, {
|
|
1422
|
+
version: entry.version,
|
|
1423
|
+
typeId: entry.typeId,
|
|
1424
|
+
parentId: entry.parentId ?? null
|
|
1425
|
+
}));
|
|
1388
1426
|
}
|
|
1389
1427
|
async getJournal(id, query = {}) {
|
|
1390
1428
|
if (!this.exec.get("SELECT 1 FROM records WHERE id = ?", [id])) {
|
|
@@ -1549,7 +1587,51 @@ var ASSOCIATION_KEY = "record_id, kind, label, file_id, related_scope, related_i
|
|
|
1549
1587
|
var PERMISSION_KEY = "record_id, kind, label, grantee_kind, grantee_id, role";
|
|
1550
1588
|
var addsOf = (edits) => edits.flatMap((e) => e.op === "add" ? [e.association] : []);
|
|
1551
1589
|
|
|
1552
|
-
// ../sqlite-shared/dist/
|
|
1590
|
+
// ../sqlite-shared/dist/config.js
|
|
1591
|
+
var CONFIG_ROW_SQL = `SELECT content FROM records WHERE id = '_config'`;
|
|
1592
|
+
var missingStoreError = (where) => new Error(`Cannot open: no database found at ${where}. Pass create: 'ifMissing' or 'exclusive' to create one.`);
|
|
1593
|
+
var writeNewStore = (exec, seed) => {
|
|
1594
|
+
exec.transaction(() => {
|
|
1595
|
+
exec.exec(RECORD_SCHEMA_SQL);
|
|
1596
|
+
exec.exec(FTS5_SCHEMA_SQL);
|
|
1597
|
+
exec.run("INSERT INTO format (id, version) VALUES (1, ?)", [FORMAT_VERSION]);
|
|
1598
|
+
new SharedSqlRecordLogic({ exec }).writeImported(seed.config, seed.history);
|
|
1599
|
+
});
|
|
1600
|
+
return seed.config.content;
|
|
1601
|
+
};
|
|
1602
|
+
var readStoredConfig = (exec, where) => {
|
|
1603
|
+
const hasFormat = exec.get(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'format'`) !== void 0;
|
|
1604
|
+
if (!hasFormat)
|
|
1605
|
+
return null;
|
|
1606
|
+
const version = exec.get("SELECT version FROM format WHERE id = 1")?.version;
|
|
1607
|
+
if (version !== FORMAT_VERSION)
|
|
1608
|
+
throw new FormatMismatchError(FORMAT_VERSION, version, where);
|
|
1609
|
+
const { content } = exec.get(CONFIG_ROW_SQL);
|
|
1610
|
+
return JSON.parse(content);
|
|
1611
|
+
};
|
|
1612
|
+
var openStore = async (exec, opts) => {
|
|
1613
|
+
const stored = readStoredConfig(exec, opts.where);
|
|
1614
|
+
if (!stored && opts.mode === "never")
|
|
1615
|
+
throw missingStoreError(opts.where);
|
|
1616
|
+
if (stored && (opts.mode === "exclusive" || opts.seed)) {
|
|
1617
|
+
throw new Error(`Cannot create: database already exists at ${opts.where}. Pass create: 'never' or 'ifMissing' without a seed to open it.`);
|
|
1618
|
+
}
|
|
1619
|
+
applyConnectionPragmas(exec, { wal: opts.wal });
|
|
1620
|
+
if (stored) {
|
|
1621
|
+
const expected = typeof opts.ownerEntityId === "string" ? opts.ownerEntityId : void 0;
|
|
1622
|
+
if (expected !== void 0 && expected !== stored.ownerEntityId) {
|
|
1623
|
+
throw new OwnerMismatchError(expected, stored.ownerEntityId, opts.where);
|
|
1624
|
+
}
|
|
1625
|
+
return stored;
|
|
1626
|
+
}
|
|
1627
|
+
if (opts.seed)
|
|
1628
|
+
return writeNewStore(exec, opts.seed);
|
|
1629
|
+
const owner = typeof opts.ownerEntityId === "function" ? await opts.ownerEntityId() : opts.ownerEntityId;
|
|
1630
|
+
if (!owner) {
|
|
1631
|
+
throw new Error(`Cannot create: no ownerEntityId given for the new database at ${opts.where}.`);
|
|
1632
|
+
}
|
|
1633
|
+
return writeNewStore(exec, newStoreSeed({ ownerEntityId: owner }));
|
|
1634
|
+
};
|
|
1553
1635
|
var SQLITE_RECORD_CAPABILITIES = {
|
|
1554
1636
|
filter: {
|
|
1555
1637
|
content: "path",
|
|
@@ -1570,12 +1652,17 @@ var SQLITE_RECORD_CAPABILITIES = {
|
|
|
1570
1652
|
var SharedSqlRecordAdapter = class {
|
|
1571
1653
|
capabilities = SQLITE_RECORD_CAPABILITIES;
|
|
1572
1654
|
ownerEntityId;
|
|
1573
|
-
|
|
1655
|
+
/** Each engine holds its database for one process; see docs/spec/adapters.md § Concurrency & storage ownership. */
|
|
1656
|
+
writers = "owner";
|
|
1574
1657
|
record;
|
|
1658
|
+
/** Moved by every write method; see head(). */
|
|
1659
|
+
written = trackWrites(this);
|
|
1575
1660
|
constructor(exec, config) {
|
|
1576
1661
|
this.record = new SharedSqlRecordLogic({ exec });
|
|
1577
|
-
this.ownerEntityId = config.
|
|
1578
|
-
|
|
1662
|
+
this.ownerEntityId = config.ownerEntityId;
|
|
1663
|
+
}
|
|
1664
|
+
async head() {
|
|
1665
|
+
return this.written.read();
|
|
1579
1666
|
}
|
|
1580
1667
|
// -------------------------------------------------------
|
|
1581
1668
|
// Records
|
|
@@ -1583,6 +1670,9 @@ var SharedSqlRecordAdapter = class {
|
|
|
1583
1670
|
createRecord(record, opts) {
|
|
1584
1671
|
return this.record.createRecord(record, opts);
|
|
1585
1672
|
}
|
|
1673
|
+
importRecord(record, history) {
|
|
1674
|
+
return this.record.importRecord(record, history);
|
|
1675
|
+
}
|
|
1586
1676
|
getRecord(id) {
|
|
1587
1677
|
return this.record.getRecord(id);
|
|
1588
1678
|
}
|
|
@@ -1625,9 +1715,6 @@ var SharedSqlRecordAdapter = class {
|
|
|
1625
1715
|
getVersion(id, version) {
|
|
1626
1716
|
return this.record.getVersion(id, version);
|
|
1627
1717
|
}
|
|
1628
|
-
saveVersion(id, version) {
|
|
1629
|
-
return this.record.saveVersion(id, version);
|
|
1630
|
-
}
|
|
1631
1718
|
// -------------------------------------------------------
|
|
1632
1719
|
// Change journal
|
|
1633
1720
|
// -------------------------------------------------------
|
|
@@ -1699,9 +1786,9 @@ var DoSQLiteRecordAdapter = class _DoSQLiteRecordAdapter extends SharedSqlRecord
|
|
|
1699
1786
|
/**
|
|
1700
1787
|
* Open the adapter for a DO instance, creating its storage on first use.
|
|
1701
1788
|
* A DO id either already has a config record (reattach) or it doesn't
|
|
1702
|
-
* (first call — opts.ownerEntityId
|
|
1703
|
-
* is no `create` option.
|
|
1704
|
-
*
|
|
1789
|
+
* (first call — opts.ownerEntityId becomes the config), so there
|
|
1790
|
+
* is no `create` option. A new store's tables are written in the same
|
|
1791
|
+
* transaction as its config record, so storage holds a whole store or none.
|
|
1705
1792
|
*
|
|
1706
1793
|
* DO SQLite manages its own durability and rejects PRAGMA journal_mode
|
|
1707
1794
|
* outright ("not authorized") — verified against the real runtime, not
|
|
@@ -1709,20 +1796,12 @@ var DoSQLiteRecordAdapter = class _DoSQLiteRecordAdapter extends SharedSqlRecord
|
|
|
1709
1796
|
*/
|
|
1710
1797
|
static async open(storage, opts) {
|
|
1711
1798
|
const exec = new DurableObjectSqliteExecutor(storage);
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
config.entityId,
|
|
1719
|
-
"the Durable Object storage"
|
|
1720
|
-
);
|
|
1721
|
-
}
|
|
1722
|
-
} else {
|
|
1723
|
-
const ownerEntityId = typeof opts.ownerEntityId === "function" ? await opts.ownerEntityId() : opts.ownerEntityId;
|
|
1724
|
-
config = insertConfigRecord(exec, ownerEntityId, opts.timezone);
|
|
1725
|
-
}
|
|
1799
|
+
const config = await openStore(exec, {
|
|
1800
|
+
...opts,
|
|
1801
|
+
mode: "ifMissing",
|
|
1802
|
+
where: "the Durable Object storage",
|
|
1803
|
+
wal: false
|
|
1804
|
+
});
|
|
1726
1805
|
return new _DoSQLiteRecordAdapter(exec, config);
|
|
1727
1806
|
}
|
|
1728
1807
|
// -------------------------------------------------------
|