@haverstack/record-adapter-sqlite 0.35.0 → 0.36.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 +16 -16
- package/dist/index.js +204 -159
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { StackCapabilities, StackRecord, TypeId, QueryResult, FileId, BaseId, VersionsQuery, RecordVersion, JournalQuery, RecordJournalEntry, StackType, AssociationEdit, DataAssociation, AuthorityAssociation, Actor } from '@haverstack/core';
|
|
2
|
-
import { StackRecordAdapter, JournalOptions, MutateOptions, AspectWriteOptions, AdapterQuery, Reach, ElementWriteOptions } from '@haverstack/core/adapter';
|
|
1
|
+
import { StackCapabilities, ConfigContent, StackRecord, TypeId, QueryResult, FileId, BaseId, VersionsQuery, RecordVersion, JournalQuery, RecordJournalEntry, StackType, AssociationEdit, DataAssociation, AuthorityAssociation, Actor } from '@haverstack/core';
|
|
2
|
+
import { StackRecordAdapter, JournalOptions, RecordHistory, MutateOptions, AspectWriteOptions, AdapterQuery, Reach, ElementWriteOptions, StoreSeed } from '@haverstack/core/adapter';
|
|
3
3
|
import { StackTokenStore, TokenSession, TokenInfo } from '@haverstack/core/wire';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -32,11 +32,10 @@ interface SqlExecutor {
|
|
|
32
32
|
transaction<T>(fn: () => T): T;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
35
|
+
/** What open() does when the store is missing or present, like O_CREAT / O_EXCL. */
|
|
36
|
+
type StoreCreateMode = 'never' | 'ifMissing' | 'exclusive';
|
|
37
|
+
/** A plain DID, or a lazy provider called only when a store is actually created. */
|
|
38
|
+
type OwnerEntityIdInput = string | (() => string | Promise<string>);
|
|
40
39
|
|
|
41
40
|
/**
|
|
42
41
|
* The engine-independent half of a SQLite-backed StackRecordAdapter: the
|
|
@@ -58,10 +57,10 @@ type StackConfig = {
|
|
|
58
57
|
declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
|
|
59
58
|
readonly capabilities: StackCapabilities;
|
|
60
59
|
readonly ownerEntityId: string;
|
|
61
|
-
readonly timezone: string | undefined;
|
|
62
60
|
private readonly record;
|
|
63
|
-
protected constructor(exec: SqlExecutor, config:
|
|
61
|
+
protected constructor(exec: SqlExecutor, config: ConfigContent);
|
|
64
62
|
createRecord(record: StackRecord, opts?: JournalOptions): Promise<StackRecord>;
|
|
63
|
+
importRecord(record: StackRecord, history: RecordHistory): Promise<StackRecord>;
|
|
65
64
|
getRecord(id: string): Promise<StackRecord | null>;
|
|
66
65
|
patchContent(id: string, patch: Record<string, unknown | null>, opts?: MutateOptions): Promise<StackRecord>;
|
|
67
66
|
setParent(id: string, parentId: string | null, opts?: AspectWriteOptions): Promise<StackRecord>;
|
|
@@ -77,7 +76,6 @@ declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
|
|
|
77
76
|
deleteUnreferencedAttachmentRecords(fileId: FileId, metadataBaseId: BaseId): Promise<StackRecord[]>;
|
|
78
77
|
getVersions(id: string, query?: VersionsQuery): Promise<RecordVersion[]>;
|
|
79
78
|
getVersion(id: string, version: number): Promise<RecordVersion | null>;
|
|
80
|
-
saveVersion(id: string, version: RecordVersion): Promise<void>;
|
|
81
79
|
getJournal(id: string, query?: JournalQuery): Promise<RecordJournalEntry[]>;
|
|
82
80
|
saveType(type: StackType): Promise<void>;
|
|
83
81
|
getType(id: TypeId): Promise<StackType | null>;
|
|
@@ -159,15 +157,9 @@ declare class NativeTokenStore implements StackTokenStore {
|
|
|
159
157
|
* spread-args run/get/all calls to it).
|
|
160
158
|
*/
|
|
161
159
|
|
|
162
|
-
/** What open() does when the store is missing or present, like O_CREAT / O_EXCL. */
|
|
163
|
-
type StoreCreateMode = 'never' | 'ifMissing' | 'exclusive';
|
|
164
|
-
/** A plain DID, or a lazy provider called only when a store is actually created. */
|
|
165
|
-
type OwnerEntityIdInput = string | (() => string | Promise<string>);
|
|
166
160
|
type NativeSQLiteRecordAdapterOpenOptions = {
|
|
167
161
|
/** Absolute path to the .db file. */
|
|
168
162
|
path: string;
|
|
169
|
-
/** IANA timezone string e.g. "America/New_York". Consulted only when a store is created. No default. */
|
|
170
|
-
timezone?: string;
|
|
171
163
|
/**
|
|
172
164
|
* Open even if a lock file from another live process is present.
|
|
173
165
|
* Only needed if that process is gone but its PID was reused by
|
|
@@ -180,6 +172,7 @@ type NativeSQLiteRecordAdapterOpenOptions = {
|
|
|
180
172
|
create?: 'never';
|
|
181
173
|
/** Checked against the existing store's owner; a mismatch throws OwnerMismatchError. */
|
|
182
174
|
ownerEntityId?: string;
|
|
175
|
+
seed?: never;
|
|
183
176
|
} | {
|
|
184
177
|
/** `'ifMissing'` opens the store if present and creates it if not; `'exclusive'` creates it and fails if present. */
|
|
185
178
|
create: 'ifMissing' | 'exclusive';
|
|
@@ -188,6 +181,13 @@ type NativeSQLiteRecordAdapterOpenOptions = {
|
|
|
188
181
|
* existing store's owner; a lazy provider is never called just to compare.
|
|
189
182
|
*/
|
|
190
183
|
ownerEntityId: OwnerEntityIdInput;
|
|
184
|
+
seed?: never;
|
|
185
|
+
} | {
|
|
186
|
+
/** A seed only creates, as `'exclusive'` does: an existing store refuses it. */
|
|
187
|
+
create?: never;
|
|
188
|
+
/** The `_config` record and history the store is created holding: a copy's source's. */
|
|
189
|
+
seed: StoreSeed;
|
|
190
|
+
ownerEntityId?: never;
|
|
191
191
|
});
|
|
192
192
|
declare class NativeSQLiteRecordAdapter extends SharedSqlRecordAdapter {
|
|
193
193
|
private readonly path;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
1
|
+
import { existsSync, rmSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
2
|
+
import { OwnerMismatchError, newStoreSeed, softDeletedRefusal, sameAssociationSet, ElementSetMovedError, FormatMismatchError, contentSortEntry, constantSortEntry, contentSortKey, parseContentFilterKey } from '@haverstack/core/adapter';
|
|
2
3
|
import { StackConflictError, applyMergePatch, StackNotFoundError, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
3
|
-
import { OwnerMismatchError, softDeletedRefusal, sameAssociationSet, ElementSetMovedError, contentSortEntry, constantSortEntry, contentSortKey, parseContentFilterKey } from '@haverstack/core/adapter';
|
|
4
4
|
import { randomBytes, createHash } from 'crypto';
|
|
5
5
|
|
|
6
6
|
// src/node-sqlite.ts
|
|
@@ -9,8 +9,16 @@ var { DatabaseSync } = process.getBuiltinModule(
|
|
|
9
9
|
);
|
|
10
10
|
|
|
11
11
|
// ../sqlite-shared/dist/schema.js
|
|
12
|
+
var FORMAT_VERSION = 1;
|
|
12
13
|
var RECORD_SCHEMA_SQL = `
|
|
13
|
-
|
|
14
|
+
-- Which layout this store is, in its one row, so an export names the one
|
|
15
|
+
-- a reader holds. See docs/spec/portability.md \xA7 The portable format.
|
|
16
|
+
CREATE TABLE format (
|
|
17
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
18
|
+
version INTEGER NOT NULL
|
|
19
|
+
) STRICT;
|
|
20
|
+
|
|
21
|
+
CREATE TABLE records (
|
|
14
22
|
id TEXT PRIMARY KEY,
|
|
15
23
|
type_id TEXT NOT NULL,
|
|
16
24
|
created_at INTEGER NOT NULL,
|
|
@@ -30,7 +38,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
30
38
|
-- A record's data associations: tags, attachments and relationships.
|
|
31
39
|
-- Its permissions are in their own table below, so no predicate over this
|
|
32
40
|
-- one can match a grantee as a relationship target.
|
|
33
|
-
CREATE TABLE
|
|
41
|
+
CREATE TABLE associations (
|
|
34
42
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
35
43
|
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship')),
|
|
36
44
|
label TEXT NOT NULL,
|
|
@@ -56,7 +64,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
56
64
|
-- grantee; a group grantee's role is in the key, because member and
|
|
57
65
|
-- admin name two different sets of people.
|
|
58
66
|
-- See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
59
|
-
CREATE TABLE
|
|
67
|
+
CREATE TABLE permissions (
|
|
60
68
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
61
69
|
kind TEXT NOT NULL CHECK (kind IN ('permission', 'anyone')),
|
|
62
70
|
label TEXT NOT NULL CHECK (label IN ('read', 'write')),
|
|
@@ -70,7 +78,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
70
78
|
-- associations column: none of those aspects bumps version, so no
|
|
71
79
|
-- version ever snapshots one. See
|
|
72
80
|
-- docs/spec/versioning.md \xA7 Version history.
|
|
73
|
-
CREATE TABLE
|
|
81
|
+
CREATE TABLE versions (
|
|
74
82
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
75
83
|
version INTEGER NOT NULL,
|
|
76
84
|
type_id TEXT NOT NULL,
|
|
@@ -88,7 +96,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
88
96
|
-- by design \u2014 content lives on a versions row, and copying it here
|
|
89
97
|
-- would make the journal the larger of the two stores.
|
|
90
98
|
-- See docs/spec/journal.md.
|
|
91
|
-
CREATE TABLE
|
|
99
|
+
CREATE TABLE journal (
|
|
92
100
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
93
101
|
seq INTEGER NOT NULL,
|
|
94
102
|
at INTEGER NOT NULL,
|
|
@@ -112,7 +120,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
112
120
|
PRIMARY KEY (record_id, seq)
|
|
113
121
|
) STRICT;
|
|
114
122
|
|
|
115
|
-
CREATE TABLE
|
|
123
|
+
CREATE TABLE types (
|
|
116
124
|
id TEXT PRIMARY KEY,
|
|
117
125
|
base_id TEXT NOT NULL,
|
|
118
126
|
version INTEGER NOT NULL,
|
|
@@ -130,7 +138,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
130
138
|
-- value_rank is 0 for a numeric value and 1 for a text one \u2014 the same
|
|
131
139
|
-- split as "num_value IS NULL", but non-null, so one index can carry
|
|
132
140
|
-- the whole ordering. file_id is set only for a 'file-ref' field.
|
|
133
|
-
CREATE TABLE
|
|
141
|
+
CREATE TABLE content_index (
|
|
134
142
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
135
143
|
field TEXT NOT NULL,
|
|
136
144
|
value_rank INTEGER NOT NULL CHECK (value_rank IN (0, 1)),
|
|
@@ -146,35 +154,35 @@ var RECORD_SCHEMA_SQL = `
|
|
|
146
154
|
-- leaves the planner sorting every match before it can honor LIMIT.
|
|
147
155
|
-- deleted_at and unlisted_at get none: an index on a column every query
|
|
148
156
|
-- tests IS NULL only lures the planner off the one answering ORDER BY.
|
|
149
|
-
CREATE INDEX
|
|
150
|
-
CREATE INDEX
|
|
151
|
-
CREATE INDEX
|
|
152
|
-
CREATE INDEX
|
|
153
|
-
CREATE INDEX
|
|
154
|
-
CREATE INDEX
|
|
155
|
-
CREATE INDEX
|
|
156
|
-
CREATE INDEX
|
|
157
|
+
CREATE INDEX idx_records_type_id ON records(type_id, created_at, id);
|
|
158
|
+
CREATE INDEX idx_records_parent_id ON records(parent_id, created_at, id);
|
|
159
|
+
CREATE INDEX idx_records_entity_id ON records(entity_id, created_at, id);
|
|
160
|
+
CREATE INDEX idx_records_app_id ON records(app_id, created_at, id);
|
|
161
|
+
CREATE INDEX idx_records_principal_id ON records(principal_id, created_at, id);
|
|
162
|
+
CREATE INDEX idx_records_created_at ON records(created_at, id);
|
|
163
|
+
CREATE INDEX idx_records_updated_at ON records(updated_at, id);
|
|
164
|
+
CREATE INDEX idx_records_version ON records(version, id);
|
|
157
165
|
|
|
158
166
|
-- Each association index ends with record_id so the semi-joins in
|
|
159
167
|
-- query.ts read the matching record ids out of the index alone.
|
|
160
168
|
-- idx_assoc_related carries related_ns ahead of related_id because a
|
|
161
169
|
-- record- or entity-scoped target always stores '' there, which
|
|
162
170
|
-- buildQueryPlan states explicitly to keep the prefix contiguous.
|
|
163
|
-
CREATE INDEX
|
|
164
|
-
CREATE INDEX
|
|
165
|
-
CREATE INDEX
|
|
166
|
-
CREATE INDEX
|
|
171
|
+
CREATE INDEX idx_assoc_kind_label ON associations(kind, label, record_id);
|
|
172
|
+
CREATE INDEX idx_assoc_kind_file_id ON associations(kind, file_id, record_id);
|
|
173
|
+
CREATE INDEX idx_assoc_related ON associations(kind, related_scope, related_ns, related_id, record_id);
|
|
174
|
+
CREATE INDEX idx_types_base_id ON types(base_id, version);
|
|
167
175
|
|
|
168
176
|
-- The permissions key with record_id moved last, so a scoped query reads
|
|
169
177
|
-- the records carrying an element straight out of the index.
|
|
170
|
-
CREATE INDEX
|
|
178
|
+
CREATE INDEX idx_permissions_element
|
|
171
179
|
ON permissions(kind, label, grantee_kind, grantee_id, role, record_id);
|
|
172
180
|
|
|
173
181
|
-- The whole content ordering, in index order, for one field at a time:
|
|
174
182
|
-- a content-sorted page is an ordered walk of a slice of this index.
|
|
175
|
-
CREATE INDEX
|
|
183
|
+
CREATE INDEX idx_content_index_sort
|
|
176
184
|
ON content_index(field, value_rank, num_value, text_key, text_value, record_id);
|
|
177
|
-
CREATE INDEX
|
|
185
|
+
CREATE INDEX idx_content_index_file
|
|
178
186
|
ON content_index(file_id, record_id) WHERE file_id IS NOT NULL;
|
|
179
187
|
`;
|
|
180
188
|
var TOKENS_SCHEMA_SQL = `
|
|
@@ -189,7 +197,7 @@ var TOKENS_SCHEMA_SQL = `
|
|
|
189
197
|
) STRICT;
|
|
190
198
|
`;
|
|
191
199
|
var FTS5_SCHEMA_SQL = `
|
|
192
|
-
CREATE VIRTUAL TABLE
|
|
200
|
+
CREATE VIRTUAL TABLE records_fts USING fts5(
|
|
193
201
|
content,
|
|
194
202
|
content='records',
|
|
195
203
|
content_rowid='rowid',
|
|
@@ -198,11 +206,10 @@ var FTS5_SCHEMA_SQL = `
|
|
|
198
206
|
`;
|
|
199
207
|
var PRAGMA_FOREIGN_KEYS_ON = `PRAGMA foreign_keys = ON;`;
|
|
200
208
|
var PRAGMA_JOURNAL_MODE_WAL = `PRAGMA journal_mode = WAL;`;
|
|
201
|
-
var
|
|
209
|
+
var applyConnectionPragmas = (exec, opts) => {
|
|
202
210
|
exec.exec(PRAGMA_FOREIGN_KEYS_ON);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
exec.exec(FTS5_SCHEMA_SQL);
|
|
211
|
+
if (opts.wal)
|
|
212
|
+
exec.exec(PRAGMA_JOURNAL_MODE_WAL);
|
|
206
213
|
};
|
|
207
214
|
|
|
208
215
|
// ../sqlite-shared/dist/fts5.js
|
|
@@ -288,28 +295,6 @@ var restoreImplicitAnd = (query) => withPhrasesMasked(query, (masked) => masked.
|
|
|
288
295
|
// ../sqlite-shared/dist/executor.js
|
|
289
296
|
var isForeignKeyViolation = (err) => err instanceof Error && err.message.includes("FOREIGN KEY constraint failed");
|
|
290
297
|
var isUniqueConstraintViolation = (err) => err instanceof Error && err.message.includes("UNIQUE constraint failed");
|
|
291
|
-
|
|
292
|
-
// ../sqlite-shared/dist/config.js
|
|
293
|
-
var CONFIG_ROW_SQL = `SELECT content FROM records WHERE id = '_config'`;
|
|
294
|
-
var insertConfigRecord = (exec, entityId, timezone) => {
|
|
295
|
-
const now = Date.now();
|
|
296
|
-
exec.run(`INSERT INTO records (id, type_id, created_at, updated_at, content, version)
|
|
297
|
-
VALUES ('_config', '_config@1', ?, ?, ?, 1)`, [now, now, JSON.stringify({ entityId, timezone })]);
|
|
298
|
-
return { entityId, timezone };
|
|
299
|
-
};
|
|
300
|
-
var tryReadStackConfig = (exec) => {
|
|
301
|
-
const row = exec.get(CONFIG_ROW_SQL);
|
|
302
|
-
if (!row)
|
|
303
|
-
return null;
|
|
304
|
-
const content = JSON.parse(row.content);
|
|
305
|
-
return { entityId: content.entityId, timezone: content.timezone };
|
|
306
|
-
};
|
|
307
|
-
var readStackConfig = (exec) => {
|
|
308
|
-
const config = tryReadStackConfig(exec);
|
|
309
|
-
if (!config)
|
|
310
|
-
throw new Error("Stack database is missing its config record.");
|
|
311
|
-
return config;
|
|
312
|
-
};
|
|
313
298
|
var toBase64 = (text) => {
|
|
314
299
|
const bytes = new TextEncoder().encode(text);
|
|
315
300
|
let binary = "";
|
|
@@ -979,6 +964,35 @@ var versionRowValues = (version) => [
|
|
|
979
964
|
version.updatedBy?.subjectId ?? null,
|
|
980
965
|
version.updatedBy?.principalId ?? null
|
|
981
966
|
];
|
|
967
|
+
var JOURNAL_COLUMNS = [
|
|
968
|
+
"record_id",
|
|
969
|
+
"seq",
|
|
970
|
+
"at",
|
|
971
|
+
"kind",
|
|
972
|
+
"ops",
|
|
973
|
+
"previous_parent_id",
|
|
974
|
+
"actor",
|
|
975
|
+
"associations",
|
|
976
|
+
"version",
|
|
977
|
+
"type_id",
|
|
978
|
+
"parent_id"
|
|
979
|
+
];
|
|
980
|
+
var INSERT_JOURNAL_SQL = `INSERT INTO journal (${JOURNAL_COLUMNS.join(", ")})
|
|
981
|
+
VALUES (${JOURNAL_COLUMNS.map(() => "?").join(", ")})`;
|
|
982
|
+
var journalValues = (recordId, seq, at, entry, row) => [
|
|
983
|
+
recordId,
|
|
984
|
+
seq,
|
|
985
|
+
toMs(at),
|
|
986
|
+
entry.kind,
|
|
987
|
+
JSON.stringify(entry.ops),
|
|
988
|
+
// '' is the root; NULL is an entry that names no origin at all.
|
|
989
|
+
entry.previousParentId === void 0 ? null : entry.previousParentId ?? "",
|
|
990
|
+
entry.actor ? JSON.stringify(entry.actor) : null,
|
|
991
|
+
entry.associations?.length ? JSON.stringify(entry.associations) : null,
|
|
992
|
+
row.version,
|
|
993
|
+
row.typeId,
|
|
994
|
+
row.parentId
|
|
995
|
+
];
|
|
982
996
|
var SharedSqlRecordLogic = class {
|
|
983
997
|
exec;
|
|
984
998
|
searchIndex;
|
|
@@ -1074,42 +1088,71 @@ var SharedSqlRecordLogic = class {
|
|
|
1074
1088
|
*/
|
|
1075
1089
|
async createRecord(record, opts = {}) {
|
|
1076
1090
|
this.exec.transaction(() => {
|
|
1077
|
-
|
|
1078
|
-
|
|
1091
|
+
this.insertRecord(record);
|
|
1092
|
+
this.appendJournal(record.id, opts.journal);
|
|
1093
|
+
});
|
|
1094
|
+
return record;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* createRecord() carrying the record's history as recorded: its version
|
|
1098
|
+
* rows and journal entries, `seq` and `at` included, in the same
|
|
1099
|
+
* transaction. Appends no entry of its own. See
|
|
1100
|
+
* docs/spec/portability.md § Writing history.
|
|
1101
|
+
*/
|
|
1102
|
+
async importRecord(record, history) {
|
|
1103
|
+
this.exec.transaction(() => this.writeImported(record, history));
|
|
1104
|
+
return record;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* importRecord()'s write, for a caller already inside a transaction:
|
|
1108
|
+
* writeNewStore() writes a store's `_config` seed through it, so the one
|
|
1109
|
+
* Record a store starts with lands as every other Record does.
|
|
1110
|
+
*/
|
|
1111
|
+
writeImported(record, history) {
|
|
1112
|
+
this.insertRecord(record);
|
|
1113
|
+
for (const version of history.versions)
|
|
1114
|
+
this.insertVersionRow(record.id, version);
|
|
1115
|
+
for (const entry of history.journal)
|
|
1116
|
+
this.insertJournalRow(record.id, entry);
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* The record row and every table describing it, for a caller already
|
|
1120
|
+
* inside a transaction. A duplicate id maps to StackConflictError.
|
|
1121
|
+
*/
|
|
1122
|
+
insertRecord(record) {
|
|
1123
|
+
try {
|
|
1124
|
+
this.exec.run(`INSERT INTO records
|
|
1079
1125
|
(id, type_id, created_at, updated_at, content, version,
|
|
1080
1126
|
parent_id, entity_id, app_id, principal_id, updated_by, updated_via,
|
|
1081
1127
|
deleted_at, unlisted_at)
|
|
1082
1128
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
}
|
|
1102
|
-
throw err;
|
|
1129
|
+
record.id,
|
|
1130
|
+
record.typeId,
|
|
1131
|
+
toMs(record.createdAt),
|
|
1132
|
+
toMs(record.updatedAt),
|
|
1133
|
+
JSON.stringify(record.content),
|
|
1134
|
+
record.version,
|
|
1135
|
+
record.parentId ?? null,
|
|
1136
|
+
record.createdBy?.subjectId ?? null,
|
|
1137
|
+
record.appId ?? null,
|
|
1138
|
+
record.createdBy?.principalId ?? null,
|
|
1139
|
+
record.updatedBy?.subjectId ?? null,
|
|
1140
|
+
record.updatedBy?.principalId ?? null,
|
|
1141
|
+
record.deletedAt ? toMs(record.deletedAt) : null,
|
|
1142
|
+
record.unlistedAt ? toMs(record.unlistedAt) : null
|
|
1143
|
+
]);
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
if (isUniqueConstraintViolation(err)) {
|
|
1146
|
+
throw new StackConflictError(`Record already exists: "${record.id}"`);
|
|
1103
1147
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
this.
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
return record;
|
|
1148
|
+
throw err;
|
|
1149
|
+
}
|
|
1150
|
+
if (record.associations?.length)
|
|
1151
|
+
this.insertAssociations(record.id, record.associations);
|
|
1152
|
+
if (record.permissions?.length)
|
|
1153
|
+
this.insertPermissions(record.id, record.permissions);
|
|
1154
|
+
this.searchIndex.insert(record.id, JSON.stringify(record.content));
|
|
1155
|
+
this.contentIndex.sync(record.id, record.typeId, record.content);
|
|
1113
1156
|
}
|
|
1114
1157
|
async getRecord(id) {
|
|
1115
1158
|
return this.readRecord(id);
|
|
@@ -1334,7 +1377,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1334
1377
|
VALUES (?, ?, ${VERSION_COLUMNS.map(() => "?").join(", ")})`, [id, version.version, ...versionRowValues(version)]);
|
|
1335
1378
|
} catch (err) {
|
|
1336
1379
|
if (isUniqueConstraintViolation(err)) {
|
|
1337
|
-
throw new StackConflictError(`Version ${version.version} already exists for record "${id}"
|
|
1380
|
+
throw new StackConflictError(`Version ${version.version} already exists for record "${id}"`);
|
|
1338
1381
|
}
|
|
1339
1382
|
throw err;
|
|
1340
1383
|
}
|
|
@@ -1350,14 +1393,6 @@ var SharedSqlRecordLogic = class {
|
|
|
1350
1393
|
SET ${VERSION_COLUMNS.map((c) => `${c} = ?`).join(", ")}
|
|
1351
1394
|
WHERE record_id = ? AND version = ?`, [...versionRowValues(version), id, version.version]);
|
|
1352
1395
|
}
|
|
1353
|
-
/**
|
|
1354
|
-
* Standalone snapshot write for tooling and tests — loud on any
|
|
1355
|
-
* collision, since nothing here is about to bump the version. Mutating
|
|
1356
|
-
* methods take a `snapshot` option instead; see snapshotBeforeMutation.
|
|
1357
|
-
*/
|
|
1358
|
-
async saveVersion(id, version) {
|
|
1359
|
-
this.insertVersionRow(id, version);
|
|
1360
|
-
}
|
|
1361
1396
|
/**
|
|
1362
1397
|
* The snapshot half of a mutating method's atomic snapshot-then-mutate
|
|
1363
1398
|
* transaction. A collision is a racing-writer conflict, except an orphan
|
|
@@ -1371,8 +1406,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1371
1406
|
} catch (err) {
|
|
1372
1407
|
if (!(err instanceof StackConflictError))
|
|
1373
1408
|
throw err;
|
|
1374
|
-
if (this.currentVersion(id) !== version.version)
|
|
1375
|
-
throw
|
|
1409
|
+
if (this.currentVersion(id) !== version.version) {
|
|
1410
|
+
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.`);
|
|
1411
|
+
}
|
|
1376
1412
|
this.overwriteVersionRow(id, version);
|
|
1377
1413
|
}
|
|
1378
1414
|
}
|
|
@@ -1381,9 +1417,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1381
1417
|
// -------------------------------------------------------
|
|
1382
1418
|
/**
|
|
1383
1419
|
* Append one entry, stamped from the row as it now stands, so callers run
|
|
1384
|
-
* this inside their write's transaction, after the write. `seq` is
|
|
1385
|
-
*
|
|
1386
|
-
* to be free. See docs/spec/journal.md § Ordering.
|
|
1420
|
+
* this inside their write's transaction, after the write. `seq` is read
|
|
1421
|
+
* from the log's maximum in that same transaction, so no caller ever holds
|
|
1422
|
+
* one it expects to be free. See docs/spec/journal.md § Ordering.
|
|
1387
1423
|
*/
|
|
1388
1424
|
appendJournal(recordId, entry) {
|
|
1389
1425
|
if (!entry)
|
|
@@ -1391,26 +1427,20 @@ var SharedSqlRecordLogic = class {
|
|
|
1391
1427
|
const row = this.exec.get("SELECT version, type_id, parent_id FROM records WHERE id = ?", [recordId]);
|
|
1392
1428
|
if (!row)
|
|
1393
1429
|
throw recordNotFound(recordId);
|
|
1394
|
-
this.exec.
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
entry.
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
row.parent_id,
|
|
1409
|
-
// '' is the root; NULL is an entry that names no origin at all.
|
|
1410
|
-
entry.previousParentId === void 0 ? null : entry.previousParentId ?? "",
|
|
1411
|
-
entry.actor ? JSON.stringify(entry.actor) : null,
|
|
1412
|
-
entry.associations?.length ? JSON.stringify(entry.associations) : null
|
|
1413
|
-
]);
|
|
1430
|
+
const { seq } = this.exec.get("SELECT COALESCE(MAX(seq), 0) + 1 AS seq FROM journal WHERE record_id = ?", [recordId]);
|
|
1431
|
+
this.exec.run(INSERT_JOURNAL_SQL, journalValues(recordId, seq, /* @__PURE__ */ new Date(), entry, {
|
|
1432
|
+
version: row.version,
|
|
1433
|
+
typeId: row.type_id,
|
|
1434
|
+
parentId: row.parent_id
|
|
1435
|
+
}));
|
|
1436
|
+
}
|
|
1437
|
+
/** A whole entry as recorded, `seq` and `at` included — importRecord()'s half of the journal. */
|
|
1438
|
+
insertJournalRow(recordId, entry) {
|
|
1439
|
+
this.exec.run(INSERT_JOURNAL_SQL, journalValues(recordId, entry.seq, entry.at, entry, {
|
|
1440
|
+
version: entry.version,
|
|
1441
|
+
typeId: entry.typeId,
|
|
1442
|
+
parentId: entry.parentId ?? null
|
|
1443
|
+
}));
|
|
1414
1444
|
}
|
|
1415
1445
|
async getJournal(id, query = {}) {
|
|
1416
1446
|
if (!this.exec.get("SELECT 1 FROM records WHERE id = ?", [id])) {
|
|
@@ -1575,6 +1605,52 @@ var ASSOCIATION_KEY = "record_id, kind, label, file_id, related_scope, related_i
|
|
|
1575
1605
|
var PERMISSION_KEY = "record_id, kind, label, grantee_kind, grantee_id, role";
|
|
1576
1606
|
var addsOf = (edits) => edits.flatMap((e) => e.op === "add" ? [e.association] : []);
|
|
1577
1607
|
|
|
1608
|
+
// ../sqlite-shared/dist/config.js
|
|
1609
|
+
var CONFIG_ROW_SQL = `SELECT content FROM records WHERE id = '_config'`;
|
|
1610
|
+
var missingStoreError = (where) => new Error(`Cannot open: no database found at ${where}. Pass create: 'ifMissing' or 'exclusive' to create one.`);
|
|
1611
|
+
var writeNewStore = (exec, seed) => {
|
|
1612
|
+
exec.transaction(() => {
|
|
1613
|
+
exec.exec(RECORD_SCHEMA_SQL);
|
|
1614
|
+
exec.exec(FTS5_SCHEMA_SQL);
|
|
1615
|
+
exec.run("INSERT INTO format (id, version) VALUES (1, ?)", [FORMAT_VERSION]);
|
|
1616
|
+
new SharedSqlRecordLogic({ exec }).writeImported(seed.config, seed.history);
|
|
1617
|
+
});
|
|
1618
|
+
return seed.config.content;
|
|
1619
|
+
};
|
|
1620
|
+
var readStoredConfig = (exec, where) => {
|
|
1621
|
+
const hasFormat = exec.get(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'format'`) !== void 0;
|
|
1622
|
+
if (!hasFormat)
|
|
1623
|
+
return null;
|
|
1624
|
+
const version = exec.get("SELECT version FROM format WHERE id = 1")?.version;
|
|
1625
|
+
if (version !== FORMAT_VERSION)
|
|
1626
|
+
throw new FormatMismatchError(FORMAT_VERSION, version, where);
|
|
1627
|
+
const { content } = exec.get(CONFIG_ROW_SQL);
|
|
1628
|
+
return JSON.parse(content);
|
|
1629
|
+
};
|
|
1630
|
+
var openStore = async (exec, opts) => {
|
|
1631
|
+
const stored = readStoredConfig(exec, opts.where);
|
|
1632
|
+
if (!stored && opts.mode === "never")
|
|
1633
|
+
throw missingStoreError(opts.where);
|
|
1634
|
+
if (stored && (opts.mode === "exclusive" || opts.seed)) {
|
|
1635
|
+
throw new Error(`Cannot create: database already exists at ${opts.where}. Pass create: 'never' or 'ifMissing' without a seed to open it.`);
|
|
1636
|
+
}
|
|
1637
|
+
applyConnectionPragmas(exec, { wal: opts.wal });
|
|
1638
|
+
if (stored) {
|
|
1639
|
+
const expected = typeof opts.ownerEntityId === "string" ? opts.ownerEntityId : void 0;
|
|
1640
|
+
if (expected !== void 0 && expected !== stored.ownerEntityId) {
|
|
1641
|
+
throw new OwnerMismatchError(expected, stored.ownerEntityId, opts.where);
|
|
1642
|
+
}
|
|
1643
|
+
return stored;
|
|
1644
|
+
}
|
|
1645
|
+
if (opts.seed)
|
|
1646
|
+
return writeNewStore(exec, opts.seed);
|
|
1647
|
+
const owner = typeof opts.ownerEntityId === "function" ? await opts.ownerEntityId() : opts.ownerEntityId;
|
|
1648
|
+
if (!owner) {
|
|
1649
|
+
throw new Error(`Cannot create: no ownerEntityId given for the new database at ${opts.where}.`);
|
|
1650
|
+
}
|
|
1651
|
+
return writeNewStore(exec, newStoreSeed({ ownerEntityId: owner }));
|
|
1652
|
+
};
|
|
1653
|
+
|
|
1578
1654
|
// ../sqlite-shared/dist/record-adapter.js
|
|
1579
1655
|
var SQLITE_RECORD_CAPABILITIES = {
|
|
1580
1656
|
filter: {
|
|
@@ -1596,12 +1672,10 @@ var SQLITE_RECORD_CAPABILITIES = {
|
|
|
1596
1672
|
var SharedSqlRecordAdapter = class {
|
|
1597
1673
|
capabilities = SQLITE_RECORD_CAPABILITIES;
|
|
1598
1674
|
ownerEntityId;
|
|
1599
|
-
timezone;
|
|
1600
1675
|
record;
|
|
1601
1676
|
constructor(exec, config) {
|
|
1602
1677
|
this.record = new SharedSqlRecordLogic({ exec });
|
|
1603
|
-
this.ownerEntityId = config.
|
|
1604
|
-
this.timezone = config.timezone;
|
|
1678
|
+
this.ownerEntityId = config.ownerEntityId;
|
|
1605
1679
|
}
|
|
1606
1680
|
// -------------------------------------------------------
|
|
1607
1681
|
// Records
|
|
@@ -1609,6 +1683,9 @@ var SharedSqlRecordAdapter = class {
|
|
|
1609
1683
|
createRecord(record, opts) {
|
|
1610
1684
|
return this.record.createRecord(record, opts);
|
|
1611
1685
|
}
|
|
1686
|
+
importRecord(record, history) {
|
|
1687
|
+
return this.record.importRecord(record, history);
|
|
1688
|
+
}
|
|
1612
1689
|
getRecord(id) {
|
|
1613
1690
|
return this.record.getRecord(id);
|
|
1614
1691
|
}
|
|
@@ -1651,9 +1728,6 @@ var SharedSqlRecordAdapter = class {
|
|
|
1651
1728
|
getVersion(id, version) {
|
|
1652
1729
|
return this.record.getVersion(id, version);
|
|
1653
1730
|
}
|
|
1654
|
-
saveVersion(id, version) {
|
|
1655
|
-
return this.record.saveVersion(id, version);
|
|
1656
|
-
}
|
|
1657
1731
|
// -------------------------------------------------------
|
|
1658
1732
|
// Change journal
|
|
1659
1733
|
// -------------------------------------------------------
|
|
@@ -1857,51 +1931,22 @@ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter extends SharedS
|
|
|
1857
1931
|
* See docs/spec/adapters.md § Construction.
|
|
1858
1932
|
*/
|
|
1859
1933
|
static async open(opts) {
|
|
1860
|
-
const mode = opts.create ?? "never";
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1863
|
-
throw new Error(
|
|
1864
|
-
`Cannot open: no database found at "${opts.path}". Pass create: 'ifMissing' or 'exclusive' to create one.`
|
|
1865
|
-
);
|
|
1866
|
-
}
|
|
1867
|
-
if (exists && mode === "exclusive") {
|
|
1868
|
-
throw new Error(
|
|
1869
|
-
`Cannot create: database already exists at "${opts.path}". Pass create: 'never' or 'ifMissing' to open it.`
|
|
1870
|
-
);
|
|
1871
|
-
}
|
|
1872
|
-
};
|
|
1873
|
-
const existedBeforeLock = existsSync(opts.path);
|
|
1874
|
-
checkMode(existedBeforeLock);
|
|
1875
|
-
let newOwner;
|
|
1876
|
-
if (!existedBeforeLock) {
|
|
1877
|
-
newOwner = typeof opts.ownerEntityId === "function" ? await opts.ownerEntityId() : opts.ownerEntityId;
|
|
1878
|
-
if (!newOwner) {
|
|
1879
|
-
throw new Error(
|
|
1880
|
-
`Cannot create: no ownerEntityId given for the new database at "${opts.path}".`
|
|
1881
|
-
);
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1934
|
+
const mode = opts.seed ? "exclusive" : opts.create ?? "never";
|
|
1935
|
+
const where = `"${opts.path}"`;
|
|
1936
|
+
if (mode === "never" && !existsSync(opts.path)) throw missingStoreError(where);
|
|
1884
1937
|
acquireLock(opts.path, opts.force);
|
|
1938
|
+
const madeFile = !existsSync(opts.path);
|
|
1885
1939
|
let db;
|
|
1886
1940
|
try {
|
|
1887
|
-
const exists = existsSync(opts.path);
|
|
1888
|
-
checkMode(exists);
|
|
1889
1941
|
db = new DatabaseSync(opts.path);
|
|
1890
1942
|
const exec = new NativeSqliteExecutor(db);
|
|
1891
|
-
|
|
1892
|
-
let config;
|
|
1893
|
-
if (exists) {
|
|
1894
|
-
config = readStackConfig(exec);
|
|
1895
|
-
if (typeof opts.ownerEntityId === "string" && opts.ownerEntityId !== config.entityId) {
|
|
1896
|
-
throw new OwnerMismatchError(opts.ownerEntityId, config.entityId, `"${opts.path}"`);
|
|
1897
|
-
}
|
|
1898
|
-
} else {
|
|
1899
|
-
if (!newOwner) throw new Error(`Cannot create: "${opts.path}" vanished while opening.`);
|
|
1900
|
-
config = insertConfigRecord(exec, newOwner, opts.timezone);
|
|
1901
|
-
}
|
|
1943
|
+
const config = await openStore(exec, { ...opts, mode, where, wal: true });
|
|
1902
1944
|
return new _NativeSQLiteRecordAdapter(opts.path, db, exec, config);
|
|
1903
1945
|
} catch (err) {
|
|
1904
1946
|
db?.close();
|
|
1947
|
+
if (madeFile) {
|
|
1948
|
+
for (const suffix of ["", "-wal", "-shm"]) rmSync(`${opts.path}${suffix}`, { force: true });
|
|
1949
|
+
}
|
|
1905
1950
|
releaseLock(opts.path);
|
|
1906
1951
|
throw err;
|
|
1907
1952
|
}
|