@haverstack/record-adapter-sqlite 0.34.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 +27 -21
- package/dist/index.js +740 -579
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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';
|
|
3
|
+
import { StackConflictError, applyMergePatch, StackNotFoundError, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
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,
|
|
@@ -27,41 +35,50 @@ var RECORD_SCHEMA_SQL = `
|
|
|
27
35
|
unlisted_at INTEGER
|
|
28
36
|
) STRICT;
|
|
29
37
|
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
|
|
34
|
-
-- See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
35
|
-
CREATE TABLE IF NOT EXISTS associations (
|
|
38
|
+
-- A record's data associations: tags, attachments and relationships.
|
|
39
|
+
-- Its permissions are in their own table below, so no predicate over this
|
|
40
|
+
-- one can match a grantee as a relationship target.
|
|
41
|
+
CREATE TABLE associations (
|
|
36
42
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
37
|
-
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship'
|
|
43
|
+
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship')),
|
|
38
44
|
label TEXT NOT NULL,
|
|
39
45
|
file_id TEXT NOT NULL DEFAULT '',
|
|
40
46
|
-- A relationship target is (related_scope, related_id) qualified by
|
|
41
|
-
-- related_ns (a foreign namespace) or related_stack (another stack)
|
|
42
|
-
--
|
|
43
|
-
--
|
|
44
|
-
related_scope TEXT NOT NULL DEFAULT '' CHECK (related_scope IN ('', 'record', 'entity', 'external'
|
|
47
|
+
-- related_ns (a foreign namespace) or related_stack (another stack).
|
|
48
|
+
-- All are in the key, so targets differing only by a qualifier are
|
|
49
|
+
-- distinct associations.
|
|
50
|
+
related_scope TEXT NOT NULL DEFAULT '' CHECK (related_scope IN ('', 'record', 'entity', 'external')),
|
|
45
51
|
related_id TEXT NOT NULL DEFAULT '',
|
|
46
52
|
related_ns TEXT NOT NULL DEFAULT '',
|
|
47
53
|
related_stack TEXT NOT NULL DEFAULT '',
|
|
48
|
-
-- Set only by a group grantee; in the key because member and admin name
|
|
49
|
-
-- two different sets of people.
|
|
50
|
-
related_role TEXT NOT NULL DEFAULT '' CHECK (related_role IN ('', 'member', 'admin')),
|
|
51
54
|
-- Out of the key: it annotates a reference rather than naming one, so
|
|
52
55
|
-- re-pointing it updates the existing row. See docs/spec/attachments.md
|
|
53
56
|
-- \xA7 Naming the upload a reference came from.
|
|
54
57
|
attachment_record_id TEXT NOT NULL DEFAULT '',
|
|
55
58
|
-- Leads with record_id, so its implicit index serves every by-record
|
|
56
59
|
-- read and delete; the indexes below cover only the filter directions.
|
|
57
|
-
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack
|
|
60
|
+
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack)
|
|
61
|
+
) STRICT;
|
|
62
|
+
|
|
63
|
+
-- Who reaches a record, one row per element. An 'anyone' row names no
|
|
64
|
+
-- grantee; a group grantee's role is in the key, because member and
|
|
65
|
+
-- admin name two different sets of people.
|
|
66
|
+
-- See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
67
|
+
CREATE TABLE permissions (
|
|
68
|
+
record_id TEXT NOT NULL REFERENCES records(id),
|
|
69
|
+
kind TEXT NOT NULL CHECK (kind IN ('permission', 'anyone')),
|
|
70
|
+
label TEXT NOT NULL CHECK (label IN ('read', 'write')),
|
|
71
|
+
grantee_kind TEXT NOT NULL DEFAULT '' CHECK (grantee_kind IN ('', 'entity', 'group')),
|
|
72
|
+
grantee_id TEXT NOT NULL DEFAULT '',
|
|
73
|
+
role TEXT NOT NULL DEFAULT '' CHECK (role IN ('', 'member', 'admin')),
|
|
74
|
+
PRIMARY KEY (record_id, kind, label, grantee_kind, grantee_id, role)
|
|
58
75
|
) STRICT;
|
|
59
76
|
|
|
60
77
|
-- Content and the type it is read under. No parent_id, unlisted_at or
|
|
61
78
|
-- associations column: none of those aspects bumps version, so no
|
|
62
79
|
-- version ever snapshots one. See
|
|
63
80
|
-- docs/spec/versioning.md \xA7 Version history.
|
|
64
|
-
CREATE TABLE
|
|
81
|
+
CREATE TABLE versions (
|
|
65
82
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
66
83
|
version INTEGER NOT NULL,
|
|
67
84
|
type_id TEXT NOT NULL,
|
|
@@ -79,7 +96,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
79
96
|
-- by design \u2014 content lives on a versions row, and copying it here
|
|
80
97
|
-- would make the journal the larger of the two stores.
|
|
81
98
|
-- See docs/spec/journal.md.
|
|
82
|
-
CREATE TABLE
|
|
99
|
+
CREATE TABLE journal (
|
|
83
100
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
84
101
|
seq INTEGER NOT NULL,
|
|
85
102
|
at INTEGER NOT NULL,
|
|
@@ -103,7 +120,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
103
120
|
PRIMARY KEY (record_id, seq)
|
|
104
121
|
) STRICT;
|
|
105
122
|
|
|
106
|
-
CREATE TABLE
|
|
123
|
+
CREATE TABLE types (
|
|
107
124
|
id TEXT PRIMARY KEY,
|
|
108
125
|
base_id TEXT NOT NULL,
|
|
109
126
|
version INTEGER NOT NULL,
|
|
@@ -121,7 +138,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
121
138
|
-- value_rank is 0 for a numeric value and 1 for a text one \u2014 the same
|
|
122
139
|
-- split as "num_value IS NULL", but non-null, so one index can carry
|
|
123
140
|
-- the whole ordering. file_id is set only for a 'file-ref' field.
|
|
124
|
-
CREATE TABLE
|
|
141
|
+
CREATE TABLE content_index (
|
|
125
142
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
126
143
|
field TEXT NOT NULL,
|
|
127
144
|
value_rank INTEGER NOT NULL CHECK (value_rank IN (0, 1)),
|
|
@@ -137,30 +154,35 @@ var RECORD_SCHEMA_SQL = `
|
|
|
137
154
|
-- leaves the planner sorting every match before it can honor LIMIT.
|
|
138
155
|
-- deleted_at and unlisted_at get none: an index on a column every query
|
|
139
156
|
-- tests IS NULL only lures the planner off the one answering ORDER BY.
|
|
140
|
-
CREATE INDEX
|
|
141
|
-
CREATE INDEX
|
|
142
|
-
CREATE INDEX
|
|
143
|
-
CREATE INDEX
|
|
144
|
-
CREATE INDEX
|
|
145
|
-
CREATE INDEX
|
|
146
|
-
CREATE INDEX
|
|
147
|
-
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);
|
|
148
165
|
|
|
149
166
|
-- Each association index ends with record_id so the semi-joins in
|
|
150
167
|
-- query.ts read the matching record ids out of the index alone.
|
|
151
168
|
-- idx_assoc_related carries related_ns ahead of related_id because a
|
|
152
169
|
-- record- or entity-scoped target always stores '' there, which
|
|
153
170
|
-- buildQueryPlan states explicitly to keep the prefix contiguous.
|
|
154
|
-
CREATE INDEX
|
|
155
|
-
CREATE INDEX
|
|
156
|
-
CREATE INDEX
|
|
157
|
-
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);
|
|
175
|
+
|
|
176
|
+
-- The permissions key with record_id moved last, so a scoped query reads
|
|
177
|
+
-- the records carrying an element straight out of the index.
|
|
178
|
+
CREATE INDEX idx_permissions_element
|
|
179
|
+
ON permissions(kind, label, grantee_kind, grantee_id, role, record_id);
|
|
158
180
|
|
|
159
181
|
-- The whole content ordering, in index order, for one field at a time:
|
|
160
182
|
-- a content-sorted page is an ordered walk of a slice of this index.
|
|
161
|
-
CREATE INDEX
|
|
183
|
+
CREATE INDEX idx_content_index_sort
|
|
162
184
|
ON content_index(field, value_rank, num_value, text_key, text_value, record_id);
|
|
163
|
-
CREATE INDEX
|
|
185
|
+
CREATE INDEX idx_content_index_file
|
|
164
186
|
ON content_index(file_id, record_id) WHERE file_id IS NOT NULL;
|
|
165
187
|
`;
|
|
166
188
|
var TOKENS_SCHEMA_SQL = `
|
|
@@ -175,7 +197,7 @@ var TOKENS_SCHEMA_SQL = `
|
|
|
175
197
|
) STRICT;
|
|
176
198
|
`;
|
|
177
199
|
var FTS5_SCHEMA_SQL = `
|
|
178
|
-
CREATE VIRTUAL TABLE
|
|
200
|
+
CREATE VIRTUAL TABLE records_fts USING fts5(
|
|
179
201
|
content,
|
|
180
202
|
content='records',
|
|
181
203
|
content_rowid='rowid',
|
|
@@ -184,11 +206,10 @@ var FTS5_SCHEMA_SQL = `
|
|
|
184
206
|
`;
|
|
185
207
|
var PRAGMA_FOREIGN_KEYS_ON = `PRAGMA foreign_keys = ON;`;
|
|
186
208
|
var PRAGMA_JOURNAL_MODE_WAL = `PRAGMA journal_mode = WAL;`;
|
|
187
|
-
var
|
|
209
|
+
var applyConnectionPragmas = (exec, opts) => {
|
|
188
210
|
exec.exec(PRAGMA_FOREIGN_KEYS_ON);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
exec.exec(FTS5_SCHEMA_SQL);
|
|
211
|
+
if (opts.wal)
|
|
212
|
+
exec.exec(PRAGMA_JOURNAL_MODE_WAL);
|
|
192
213
|
};
|
|
193
214
|
|
|
194
215
|
// ../sqlite-shared/dist/fts5.js
|
|
@@ -274,28 +295,6 @@ var restoreImplicitAnd = (query) => withPhrasesMasked(query, (masked) => masked.
|
|
|
274
295
|
// ../sqlite-shared/dist/executor.js
|
|
275
296
|
var isForeignKeyViolation = (err) => err instanceof Error && err.message.includes("FOREIGN KEY constraint failed");
|
|
276
297
|
var isUniqueConstraintViolation = (err) => err instanceof Error && err.message.includes("UNIQUE constraint failed");
|
|
277
|
-
|
|
278
|
-
// ../sqlite-shared/dist/config.js
|
|
279
|
-
var CONFIG_ROW_SQL = `SELECT content FROM records WHERE id = '_config'`;
|
|
280
|
-
var insertConfigRecord = (exec, entityId, timezone) => {
|
|
281
|
-
const now = Date.now();
|
|
282
|
-
exec.run(`INSERT INTO records (id, type_id, created_at, updated_at, content, version)
|
|
283
|
-
VALUES ('_config', '_config@1', ?, ?, ?, 1)`, [now, now, JSON.stringify({ entityId, timezone })]);
|
|
284
|
-
return { entityId, timezone };
|
|
285
|
-
};
|
|
286
|
-
var tryReadStackConfig = (exec) => {
|
|
287
|
-
const row = exec.get(CONFIG_ROW_SQL);
|
|
288
|
-
if (!row)
|
|
289
|
-
return null;
|
|
290
|
-
const content = JSON.parse(row.content);
|
|
291
|
-
return { entityId: content.entityId, timezone: content.timezone };
|
|
292
|
-
};
|
|
293
|
-
var readStackConfig = (exec) => {
|
|
294
|
-
const config = tryReadStackConfig(exec);
|
|
295
|
-
if (!config)
|
|
296
|
-
throw new Error("Stack database is missing its config record.");
|
|
297
|
-
return config;
|
|
298
|
-
};
|
|
299
298
|
var toBase64 = (text) => {
|
|
300
299
|
const bytes = new TextEncoder().encode(text);
|
|
301
300
|
let binary = "";
|
|
@@ -346,24 +345,22 @@ var decodeCursor = (cursor) => {
|
|
|
346
345
|
throw malformed();
|
|
347
346
|
};
|
|
348
347
|
var byTypeDescriptor = (byType) => JSON.stringify(Object.entries(byType).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
349
|
-
var getSortField = (query) => query.sort?.field ?? "createdAt";
|
|
350
348
|
var getSortColumn = (field) => field === "createdAt" ? "created_at" : field === "updatedAt" ? "updated_at" : "version";
|
|
351
|
-
var makeCursor = (record, sort,
|
|
352
|
-
const byType = sort
|
|
349
|
+
var makeCursor = (record, sort, kindOf2) => {
|
|
350
|
+
const byType = sort.contentFieldByType;
|
|
353
351
|
if (byType) {
|
|
354
|
-
const
|
|
352
|
+
const field2 = byTypeDescriptor(byType);
|
|
355
353
|
const key = Object.hasOwn(byType, record.typeId) ? byType[record.typeId] : void 0;
|
|
356
|
-
const kind2 = key?.field === void 0 ? void 0 :
|
|
354
|
+
const kind2 = key?.field === void 0 ? void 0 : kindOf2?.(record, key.field);
|
|
357
355
|
const entry2 = key?.field !== void 0 ? kind2 && contentSortEntry(kind2, record.content[key.field]) : key && constantSortEntry(key.value ?? null);
|
|
358
|
-
return encodeEntry(
|
|
356
|
+
return encodeEntry(field2, entry2 || null, record.id);
|
|
359
357
|
}
|
|
360
|
-
const contentField = sort
|
|
361
|
-
if (
|
|
362
|
-
const field = getSortField({ sort });
|
|
358
|
+
const { contentField, field } = sort;
|
|
359
|
+
if (field !== void 0) {
|
|
363
360
|
const value = field === "updatedAt" ? record.updatedAt.getTime() : field === "version" ? record.version : record.createdAt.getTime();
|
|
364
361
|
return encodeCursor({ kind: "native", field, value, id: record.id });
|
|
365
362
|
}
|
|
366
|
-
const kind =
|
|
363
|
+
const kind = kindOf2?.(record, contentField);
|
|
367
364
|
const entry = kind ? contentSortEntry(kind, record.content[contentField]) : null;
|
|
368
365
|
return encodeEntry(contentField, entry, record.id);
|
|
369
366
|
};
|
|
@@ -373,38 +370,182 @@ var encodeEntry = (field, entry, id) => {
|
|
|
373
370
|
return entry.kind === "num" ? encodeCursor({ kind: "num", field, value: entry.num, id }) : encodeCursor({ kind: "text", field, value: entry.text, id });
|
|
374
371
|
};
|
|
375
372
|
|
|
373
|
+
// ../sqlite-shared/dist/mappers.js
|
|
374
|
+
var toMs = (d) => d.getTime();
|
|
375
|
+
var actorFromColumns = (subject, principal) => subject == null ? void 0 : {
|
|
376
|
+
subjectId: subject,
|
|
377
|
+
...principal != null && { principalId: principal }
|
|
378
|
+
};
|
|
379
|
+
var fromMs = (ms) => new Date(ms);
|
|
380
|
+
var rowToRecord = (row, associations, permissions) => {
|
|
381
|
+
const record = {
|
|
382
|
+
id: row.id,
|
|
383
|
+
typeId: row.type_id,
|
|
384
|
+
createdAt: fromMs(row.created_at),
|
|
385
|
+
updatedAt: fromMs(row.updated_at),
|
|
386
|
+
content: JSON.parse(row.content),
|
|
387
|
+
version: row.version
|
|
388
|
+
};
|
|
389
|
+
if (row.parent_id != null)
|
|
390
|
+
record.parentId = row.parent_id;
|
|
391
|
+
if (row.app_id != null)
|
|
392
|
+
record.appId = row.app_id;
|
|
393
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
394
|
+
if (createdBy)
|
|
395
|
+
record.createdBy = createdBy;
|
|
396
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
397
|
+
if (updatedBy)
|
|
398
|
+
record.updatedBy = updatedBy;
|
|
399
|
+
if (row.deleted_at != null)
|
|
400
|
+
record.deletedAt = fromMs(row.deleted_at);
|
|
401
|
+
if (row.unlisted_at != null)
|
|
402
|
+
record.unlistedAt = fromMs(row.unlisted_at);
|
|
403
|
+
if (associations.length)
|
|
404
|
+
record.associations = associations;
|
|
405
|
+
if (permissions.length)
|
|
406
|
+
record.permissions = permissions;
|
|
407
|
+
return record;
|
|
408
|
+
};
|
|
409
|
+
var rowToAssociation = (row) => {
|
|
410
|
+
if (row.kind === "tag") {
|
|
411
|
+
return { kind: "tag", label: row.label };
|
|
412
|
+
}
|
|
413
|
+
if (row.kind === "attachment") {
|
|
414
|
+
const attachmentRecordId = row.attachment_record_id;
|
|
415
|
+
return {
|
|
416
|
+
kind: "attachment",
|
|
417
|
+
label: row.label,
|
|
418
|
+
fileId: row.file_id,
|
|
419
|
+
// Stored as '' when absent, like every other optional column here —
|
|
420
|
+
// the field is omitted rather than served empty, so a round trip
|
|
421
|
+
// produces the association the caller wrote.
|
|
422
|
+
...attachmentRecordId && { attachmentRecordId }
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
kind: "relationship",
|
|
427
|
+
label: row.label,
|
|
428
|
+
target: rowToTarget(row)
|
|
429
|
+
};
|
|
430
|
+
};
|
|
431
|
+
var associationKeyColumns = (a) => {
|
|
432
|
+
if (a.kind === "attachment")
|
|
433
|
+
return [a.fileId, "", "", "", ""];
|
|
434
|
+
if (a.kind !== "relationship")
|
|
435
|
+
return ["", "", "", "", ""];
|
|
436
|
+
const t = a.target;
|
|
437
|
+
if (t.kind === "entity")
|
|
438
|
+
return ["", "entity", t.entityId, "", ""];
|
|
439
|
+
if (t.kind === "external")
|
|
440
|
+
return ["", "external", t.id, t.ns, ""];
|
|
441
|
+
return ["", "record", t.recordId, "", t.stackUrl ?? ""];
|
|
442
|
+
};
|
|
443
|
+
var permissionKeyColumns = (p) => {
|
|
444
|
+
if (p.kind === "anyone")
|
|
445
|
+
return ["", "", ""];
|
|
446
|
+
const g = p.grantee;
|
|
447
|
+
return g.kind === "entity" ? ["entity", g.entityId, ""] : ["group", g.groupId, g.role];
|
|
448
|
+
};
|
|
449
|
+
var rowToPermission = (row) => {
|
|
450
|
+
if (row.kind === "anyone")
|
|
451
|
+
return { kind: "anyone", label: "read" };
|
|
452
|
+
const id = row.grantee_id;
|
|
453
|
+
return {
|
|
454
|
+
kind: "permission",
|
|
455
|
+
label: row.label,
|
|
456
|
+
grantee: row.grantee_kind === "group" ? { kind: "group", groupId: id, role: row.role } : { kind: "entity", entityId: id }
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
var rowToTarget = (row) => {
|
|
460
|
+
const id = row.related_id;
|
|
461
|
+
if (row.related_scope === "entity")
|
|
462
|
+
return { kind: "entity", entityId: id };
|
|
463
|
+
if (row.related_scope === "external") {
|
|
464
|
+
return { kind: "external", ns: row.related_ns, id };
|
|
465
|
+
}
|
|
466
|
+
const stackUrl = row.related_stack;
|
|
467
|
+
return { kind: "record", recordId: id, ...stackUrl && { stackUrl } };
|
|
468
|
+
};
|
|
469
|
+
var rowToType = (row) => {
|
|
470
|
+
const t = {
|
|
471
|
+
id: row.id,
|
|
472
|
+
baseId: row.base_id,
|
|
473
|
+
version: row.version,
|
|
474
|
+
name: row.name,
|
|
475
|
+
schema: JSON.parse(row.schema),
|
|
476
|
+
schemaHash: row.schema_hash,
|
|
477
|
+
createdAt: fromMs(row.created_at)
|
|
478
|
+
};
|
|
479
|
+
if (row.migrates_from)
|
|
480
|
+
t.migratesFrom = row.migrates_from;
|
|
481
|
+
return t;
|
|
482
|
+
};
|
|
483
|
+
var rowToVersion = (row) => {
|
|
484
|
+
const v = {
|
|
485
|
+
version: row.version,
|
|
486
|
+
typeId: row.type_id,
|
|
487
|
+
content: JSON.parse(row.content),
|
|
488
|
+
updatedAt: fromMs(row.updated_at)
|
|
489
|
+
};
|
|
490
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
491
|
+
if (createdBy)
|
|
492
|
+
v.createdBy = createdBy;
|
|
493
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
494
|
+
if (updatedBy)
|
|
495
|
+
v.updatedBy = updatedBy;
|
|
496
|
+
return v;
|
|
497
|
+
};
|
|
498
|
+
var rowToJournalEntry = (row) => {
|
|
499
|
+
const e = {
|
|
500
|
+
seq: row.seq,
|
|
501
|
+
at: fromMs(row.at),
|
|
502
|
+
kind: row.kind,
|
|
503
|
+
ops: JSON.parse(row.ops),
|
|
504
|
+
version: row.version,
|
|
505
|
+
typeId: row.type_id
|
|
506
|
+
};
|
|
507
|
+
if (row.parent_id != null)
|
|
508
|
+
e.parentId = row.parent_id;
|
|
509
|
+
if (row.previous_parent_id != null) {
|
|
510
|
+
e.previousParentId = row.previous_parent_id === "" ? null : row.previous_parent_id;
|
|
511
|
+
}
|
|
512
|
+
if (row.actor != null)
|
|
513
|
+
e.actor = JSON.parse(row.actor);
|
|
514
|
+
if (row.associations != null)
|
|
515
|
+
e.associations = JSON.parse(row.associations);
|
|
516
|
+
return e;
|
|
517
|
+
};
|
|
518
|
+
|
|
376
519
|
// ../sqlite-shared/dist/query.js
|
|
377
|
-
var atBudget = (page,
|
|
520
|
+
var atBudget = (page, rows2) => ({
|
|
378
521
|
sql: page.sql,
|
|
379
|
-
params: [...page.params,
|
|
522
|
+
params: [...page.params, rows2]
|
|
380
523
|
});
|
|
381
|
-
var sqlDirection = (query) =>
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
524
|
+
var sqlDirection = (query) => query.sort.direction === "asc" ? "ASC" : "DESC";
|
|
525
|
+
var spread = (alias) => `CASE ${alias}.type WHEN 'array' THEN ${alias}.value WHEN 'object' THEN json_array(json(${alias}.value)) WHEN 'true' THEN '[true]' WHEN 'false' THEN '[false]' ELSE json_array(${alias}.value) END`;
|
|
526
|
+
var members = (alias) => `CASE WHEN ${alias}.t = 'object' THEN ${alias}.v ELSE '{}' END`;
|
|
527
|
+
var emptyStatement = () => ({
|
|
528
|
+
tables: [],
|
|
529
|
+
tableParams: [],
|
|
530
|
+
conditions: ["r.id != '_config'"],
|
|
531
|
+
params: []
|
|
532
|
+
});
|
|
533
|
+
var rows = (s, columns, tuples) => {
|
|
534
|
+
const name = `t${s.tables.length}`;
|
|
535
|
+
s.tables.push(`${name} AS MATERIALIZED ${rowsTable(columns)}`);
|
|
536
|
+
s.tableParams.push(JSON.stringify(tuples));
|
|
537
|
+
return name;
|
|
387
538
|
};
|
|
388
|
-
var
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
key = nextAlias();
|
|
398
|
-
from.push(`json_each(${members(value)}) AS ${key}`);
|
|
399
|
-
value = nextAlias();
|
|
400
|
-
from.push(`json_each(${spread(key)}) AS ${value}`);
|
|
401
|
-
where.push(`${key}.key = ?`);
|
|
402
|
-
params.push(segment);
|
|
403
|
-
}
|
|
404
|
-
where.push(leaf(value));
|
|
405
|
-
return `EXISTS (SELECT 1 FROM ${from.join(", ")} WHERE ${where.join(" AND ")})`;
|
|
539
|
+
var rowsTable = (columns) => {
|
|
540
|
+
const named = columns.map((column, i) => `json_extract(value, '$[${i}]') AS ${column}`);
|
|
541
|
+
return `(SELECT ${named.join(", ")} FROM json_each(?))`;
|
|
542
|
+
};
|
|
543
|
+
var CONDITION_COLUMNS = ["scope", "path", "op", "val", "kind"];
|
|
544
|
+
var kindOf = (type) => `CASE ${type} WHEN 'integer' THEN 'number' WHEN 'real' THEN 'number' ELSE ${type} END`;
|
|
545
|
+
var holds = (c, r) => {
|
|
546
|
+
const depth = `json_array_length(${c}.path)`;
|
|
547
|
+
return `(WITH RECURSIVE w(d, v, t) AS (SELECT 0, ${r}.content, 'object' UNION ALL SELECT w.d + 1, e.value, e.type FROM w JOIN json_each(${members("w")}) m JOIN json_each(${spread("m")}) e WHERE w.d < ${depth} AND m.key = json_extract(${c}.path, '$[' || w.d || ']')) SELECT CASE ${c}.op WHEN 'equals' THEN IFNULL(MAX(${kindOf("w.t")} = ${c}.kind AND w.v = ${c}.val), 0) WHEN 'present' THEN IFNULL(MAX(w.t != 'null'), 0) WHEN 'isNull' THEN IFNULL(MAX(w.t = 'null'), 0) OR NOT IFNULL(MAX(w.t != 'null'), 0) END FROM w WHERE w.d = ${depth})`;
|
|
406
548
|
};
|
|
407
|
-
var equalsLeaf = (alias) => `${alias}.type NOT IN ('object', 'array') AND ${alias}.value = ?`;
|
|
408
549
|
var SORT_ALIAS = "cs";
|
|
409
550
|
var cursorMismatch = (cursor, field) => new StackBadRequestError(`Cursor sort field "${cursor.field}" does not match query sort field "${field}"`);
|
|
410
551
|
var nativeCursor = (cursor, field) => {
|
|
@@ -417,37 +558,77 @@ var contentCursor = (cursor, field) => {
|
|
|
417
558
|
throw cursorMismatch(cursor, field);
|
|
418
559
|
return cursor;
|
|
419
560
|
};
|
|
420
|
-
var
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
561
|
+
var valueKind = (value) => {
|
|
562
|
+
if (typeof value === "boolean")
|
|
563
|
+
return `${value}`;
|
|
564
|
+
if (typeof value === "number")
|
|
565
|
+
return "number";
|
|
566
|
+
return typeof value === "string" ? "text" : null;
|
|
567
|
+
};
|
|
568
|
+
var contentRows = (scope, predicate) => {
|
|
569
|
+
const row = (key, op, value = null) => {
|
|
570
|
+
const path = parseContentFilterKey(key);
|
|
571
|
+
return [scope, path, op, value, valueKind(value)];
|
|
572
|
+
};
|
|
573
|
+
return [
|
|
574
|
+
...Object.entries(predicate.content ?? {}).map(([key, value]) => (
|
|
575
|
+
// undefined is the same absence null names — JSON drops it on the way
|
|
576
|
+
// over the wire, so treating it as a value would make an in-process
|
|
577
|
+
// query and its wire equivalent disagree.
|
|
578
|
+
value === null || value === void 0 ? row(key, "isNull") : row(key, "equals", value)
|
|
579
|
+
)),
|
|
580
|
+
...(predicate.contentPresent ?? []).map((key) => row(key, "present"))
|
|
581
|
+
];
|
|
582
|
+
};
|
|
583
|
+
var inList = (s, column, values) => {
|
|
584
|
+
if (values.length === 1) {
|
|
585
|
+
s.params.push(values[0]);
|
|
586
|
+
return `${column} = ?`;
|
|
433
587
|
}
|
|
434
|
-
|
|
435
|
-
|
|
588
|
+
const table = rows(s, ["value"], values.map((value) => [value]));
|
|
589
|
+
return `${column} IN (SELECT value FROM ${table})`;
|
|
590
|
+
};
|
|
591
|
+
var familyRange = (baseId) => [`${baseId}@`, `${baseId}A`];
|
|
592
|
+
var withinFamily = (column, lo, hi) => `(${column} >= ${lo} AND ${column} < ${hi})`;
|
|
593
|
+
var inFamily = (column, baseId) => ({
|
|
594
|
+
sql: withinFamily(column, "?", "?"),
|
|
595
|
+
params: familyRange(baseId)
|
|
596
|
+
});
|
|
597
|
+
var inFamilies = (s, record, baseIds) => {
|
|
598
|
+
if (baseIds.length === 1) {
|
|
599
|
+
const one = inFamily(`${record}.type_id`, baseIds[0]);
|
|
600
|
+
s.params.push(...one.params);
|
|
601
|
+
return one.sql;
|
|
602
|
+
}
|
|
603
|
+
const table = rows(s, ["lo", "hi"], baseIds.map(familyRange));
|
|
604
|
+
return `${record}.id IN (SELECT fr.id FROM ${table} f JOIN records fr ON ${withinFamily("fr.type_id", "f.lo", "f.hi")})`;
|
|
605
|
+
};
|
|
606
|
+
var carriesPermissions = (s, sets) => {
|
|
607
|
+
const elements = rows(s, ["set_index", "set_size", "kind", "label", "grantee_kind", "grantee_id", "role"], sets.flatMap((set, i) => set.map((p) => [i, set.length, p.kind, p.label, ...permissionKeyColumns(p)])));
|
|
608
|
+
return `r.id IN (SELECT p.record_id FROM ${elements} e JOIN permissions p ON p.kind = e.kind AND p.label = e.label AND p.grantee_kind = e.grantee_kind AND p.grantee_id = e.grantee_id AND p.role = e.role GROUP BY p.record_id, e.set_index HAVING COUNT(*) = MAX(e.set_size))`;
|
|
609
|
+
};
|
|
610
|
+
var reachConditions = (s, reach) => {
|
|
611
|
+
if (reach.fence)
|
|
612
|
+
s.conditions.push(inFamilies(s, "r", reach.fence));
|
|
613
|
+
const within = reach.within;
|
|
614
|
+
if (within) {
|
|
615
|
+
const arms = [inFamilies(s, "r", within.baseIds)];
|
|
616
|
+
if (within.authored) {
|
|
617
|
+
s.params.push(within.authored.subjectId);
|
|
618
|
+
arms.push(`(r.entity_id = ? AND ${inFamilies(s, "r", within.authored.baseIds)})`);
|
|
619
|
+
}
|
|
620
|
+
arms.push(carriesPermissions(s, within.permissions));
|
|
621
|
+
s.conditions.push(`(${arms.join(" OR ")})`);
|
|
436
622
|
}
|
|
437
|
-
return conditions;
|
|
438
623
|
};
|
|
439
|
-
var recordConditions = (query) => {
|
|
440
|
-
const
|
|
441
|
-
const params =
|
|
624
|
+
var recordConditions = (query, reach) => {
|
|
625
|
+
const s = emptyStatement();
|
|
626
|
+
const { conditions, params } = s;
|
|
442
627
|
const f = query.filter ?? {};
|
|
443
|
-
let aliasSeq = 0;
|
|
444
|
-
const nextContentAlias = () => `cp${aliasSeq++}`;
|
|
445
628
|
const inFilter = (column, value) => {
|
|
446
629
|
if (value === void 0)
|
|
447
630
|
return;
|
|
448
|
-
|
|
449
|
-
conditions.push(`r.${column} IN (${values.map(() => "?").join(",")})`);
|
|
450
|
-
params.push(...values);
|
|
631
|
+
conditions.push(inList(s, `r.${column}`, Array.isArray(value) ? value : [value]));
|
|
451
632
|
};
|
|
452
633
|
const rangeFilter = (column, range) => {
|
|
453
634
|
if (range?.after) {
|
|
@@ -465,7 +646,12 @@ var recordConditions = (query) => {
|
|
|
465
646
|
if (!f.includeUnlisted) {
|
|
466
647
|
conditions.push("r.unlisted_at IS NULL");
|
|
467
648
|
}
|
|
649
|
+
if (reach)
|
|
650
|
+
reachConditions(s, reach);
|
|
468
651
|
inFilter("type_id", f.typeId);
|
|
652
|
+
if (f.baseId !== void 0) {
|
|
653
|
+
conditions.push(inFamilies(s, "r", Array.isArray(f.baseId) ? f.baseId : [f.baseId]));
|
|
654
|
+
}
|
|
469
655
|
if (f.parentId !== void 0) {
|
|
470
656
|
if (f.parentId === null) {
|
|
471
657
|
conditions.push("r.parent_id IS NULL");
|
|
@@ -481,10 +667,10 @@ var recordConditions = (query) => {
|
|
|
481
667
|
rangeFilter("updated_at", f.updatedAt);
|
|
482
668
|
const hasAssociation = (kind, clauses) => `r.id IN (SELECT a.record_id FROM associations a WHERE a.kind = '${kind}'` + clauses.map((c) => ` AND ${c}`).join("") + `)`;
|
|
483
669
|
if (f.tags?.length) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
670
|
+
const tags = [...new Set(f.tags)];
|
|
671
|
+
const label = inList(s, "a.label", tags);
|
|
672
|
+
conditions.push(`r.id IN (SELECT a.record_id FROM associations a WHERE a.kind = 'tag' AND ${label} GROUP BY a.record_id HAVING COUNT(DISTINCT a.label) = ?)`);
|
|
673
|
+
params.push(tags.length);
|
|
488
674
|
}
|
|
489
675
|
if (f.attachment) {
|
|
490
676
|
const clauses = [];
|
|
@@ -530,15 +716,17 @@ var recordConditions = (query) => {
|
|
|
530
716
|
}
|
|
531
717
|
conditions.push(hasAssociation("relationship", clauses));
|
|
532
718
|
}
|
|
533
|
-
|
|
534
|
-
conditions.push(...contentConditions(f, params, nextContentAlias));
|
|
535
|
-
}
|
|
719
|
+
const content = contentRows(null, f);
|
|
536
720
|
if (f.contentByType) {
|
|
537
|
-
const arms = Object.entries(f.contentByType)
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
721
|
+
const arms = Object.entries(f.contentByType);
|
|
722
|
+
const typeIds = arms.map(([typeId]) => typeId);
|
|
723
|
+
conditions.push(inList(s, "r.type_id", typeIds));
|
|
724
|
+
for (const [typeId, arm] of arms)
|
|
725
|
+
content.push(...contentRows(typeId, arm));
|
|
726
|
+
}
|
|
727
|
+
if (content.length > 0) {
|
|
728
|
+
const table = rows(s, CONDITION_COLUMNS, content);
|
|
729
|
+
conditions.push(`NOT EXISTS (SELECT 1 FROM ${table} c WHERE (c.scope IS NULL OR c.scope = r.type_id) AND NOT ${holds("c", "r")})`);
|
|
542
730
|
}
|
|
543
731
|
if (f.search) {
|
|
544
732
|
const sanitized = sanitizeFts5Query(f.search);
|
|
@@ -549,7 +737,7 @@ var recordConditions = (query) => {
|
|
|
549
737
|
conditions.push("0");
|
|
550
738
|
}
|
|
551
739
|
}
|
|
552
|
-
return
|
|
740
|
+
return s;
|
|
553
741
|
};
|
|
554
742
|
var presentCursorCondition = (cursor, op, params) => {
|
|
555
743
|
if (cursor.kind === "num") {
|
|
@@ -560,56 +748,54 @@ var presentCursorCondition = (cursor, op, params) => {
|
|
|
560
748
|
params.push(key, key, cursor.value, cursor.value, cursor.id);
|
|
561
749
|
return `(${SORT_ALIAS}.value_rank ${op} 1 OR (${SORT_ALIAS}.value_rank = 1 AND (${SORT_ALIAS}.text_key ${op} ? OR (${SORT_ALIAS}.text_key = ? AND (${SORT_ALIAS}.text_value ${op} ? OR (${SORT_ALIAS}.text_value = ? AND ${SORT_ALIAS}.record_id ${op} ?))))))`;
|
|
562
750
|
};
|
|
563
|
-
var statement = (
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
|
|
751
|
+
var statement = (s, from, tail) => {
|
|
752
|
+
const select = `SELECT r.* FROM ${from} WHERE ${s.conditions.join(" AND ")}${tail}`;
|
|
753
|
+
const sql = s.tables.length === 0 ? select : `WITH ${s.tables.join(", ")} ${select}`;
|
|
754
|
+
return { sql, params: [...s.tableParams, ...s.params] };
|
|
755
|
+
};
|
|
756
|
+
var buildQueryPlan = (query, reach) => {
|
|
568
757
|
const dir = sqlDirection(query);
|
|
569
758
|
const op = dir === "ASC" ? ">" : "<";
|
|
570
759
|
const decoded = query.cursor ? decodeCursor(query.cursor) : null;
|
|
571
|
-
if (query.sort
|
|
572
|
-
return byTypePlan(query, query.sort.contentFieldByType, dir, op, decoded);
|
|
760
|
+
if (query.sort.contentFieldByType) {
|
|
761
|
+
return byTypePlan(query, reach, query.sort.contentFieldByType, dir, op, decoded);
|
|
573
762
|
}
|
|
574
|
-
const contentField = query.sort
|
|
575
|
-
if (
|
|
576
|
-
const
|
|
577
|
-
const field = getSortField(query);
|
|
763
|
+
const { contentField, field } = query.sort;
|
|
764
|
+
if (field !== void 0) {
|
|
765
|
+
const where = recordConditions(query, reach);
|
|
578
766
|
const col = getSortColumn(field);
|
|
579
767
|
if (decoded) {
|
|
580
768
|
const cursor2 = nativeCursor(decoded, field);
|
|
581
|
-
conditions.push(`(r.${col} ${op} ? OR (r.${col} = ? AND r.id ${op} ?))`);
|
|
582
|
-
params.push(cursor2.value, cursor2.value, cursor2.id);
|
|
769
|
+
where.conditions.push(`(r.${col} ${op} ? OR (r.${col} = ? AND r.id ${op} ?))`);
|
|
770
|
+
where.params.push(cursor2.value, cursor2.value, cursor2.id);
|
|
583
771
|
}
|
|
584
|
-
return [
|
|
585
|
-
statement("r.*", "records r", conditions, params, ` ORDER BY r.${col} ${dir}, r.id ${dir} LIMIT ?`)
|
|
586
|
-
];
|
|
772
|
+
return [statement(where, "records r", ` ORDER BY r.${col} ${dir}, r.id ${dir} LIMIT ?`)];
|
|
587
773
|
}
|
|
588
774
|
const cursor = decoded ? contentCursor(decoded, contentField) : null;
|
|
589
775
|
const pages = [];
|
|
590
776
|
if (cursor?.kind !== "absent") {
|
|
591
|
-
const
|
|
777
|
+
const where = recordConditions(query, reach);
|
|
592
778
|
const from = `content_index ${SORT_ALIAS} JOIN records r ON r.id = ${SORT_ALIAS}.record_id`;
|
|
593
|
-
conditions.push(`${SORT_ALIAS}.field = ?`);
|
|
594
|
-
params.push(contentField);
|
|
779
|
+
where.conditions.push(`${SORT_ALIAS}.field = ?`);
|
|
780
|
+
where.params.push(contentField);
|
|
595
781
|
if (cursor)
|
|
596
|
-
conditions.push(presentCursorCondition(cursor, op, params));
|
|
782
|
+
where.conditions.push(presentCursorCondition(cursor, op, where.params));
|
|
597
783
|
const order = `${SORT_ALIAS}.value_rank ${dir}, ${SORT_ALIAS}.num_value ${dir}, ${SORT_ALIAS}.text_key ${dir}, ${SORT_ALIAS}.text_value ${dir}, ${SORT_ALIAS}.record_id ${dir}`;
|
|
598
|
-
pages.push(statement(
|
|
784
|
+
pages.push(statement(where, from, ` ORDER BY ${order} LIMIT ?`));
|
|
599
785
|
}
|
|
600
786
|
{
|
|
601
|
-
const
|
|
602
|
-
conditions.push(`NOT EXISTS (SELECT 1 FROM content_index ${SORT_ALIAS} WHERE ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = ?)`);
|
|
603
|
-
params.push(contentField);
|
|
787
|
+
const where = recordConditions(query, reach);
|
|
788
|
+
where.conditions.push(`NOT EXISTS (SELECT 1 FROM content_index ${SORT_ALIAS} WHERE ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = ?)`);
|
|
789
|
+
where.params.push(contentField);
|
|
604
790
|
if (cursor?.kind === "absent") {
|
|
605
|
-
conditions.push(`r.id ${op} ?`);
|
|
606
|
-
params.push(cursor.id);
|
|
791
|
+
where.conditions.push(`r.id ${op} ?`);
|
|
792
|
+
where.params.push(cursor.id);
|
|
607
793
|
}
|
|
608
|
-
pages.push(statement(
|
|
794
|
+
pages.push(statement(where, "records r", ` ORDER BY r.id ${dir} LIMIT ?`));
|
|
609
795
|
}
|
|
610
796
|
return pages;
|
|
611
797
|
};
|
|
612
|
-
var byTypePlan = (query, byType, dir, op, decoded) => {
|
|
798
|
+
var byTypePlan = (query, reach, byType, dir, op, decoded) => {
|
|
613
799
|
const cursor = decoded ? contentCursor(decoded, byTypeDescriptor(byType)) : null;
|
|
614
800
|
const fields = [];
|
|
615
801
|
const constants = [];
|
|
@@ -623,42 +809,38 @@ var byTypePlan = (query, byType, dir, op, decoded) => {
|
|
|
623
809
|
constants.push([typeId, entry]);
|
|
624
810
|
}
|
|
625
811
|
const pages = [];
|
|
626
|
-
const fieldOf = (alias, record) => fields.map(() => `(${record}.type_id = ? AND ${alias}.field = ?)`).join(" OR ");
|
|
627
812
|
if (cursor?.kind !== "absent" && fields.length + constants.length > 0) {
|
|
813
|
+
const where = recordConditions(query, reach);
|
|
628
814
|
const sources = [];
|
|
629
|
-
const sourceParams = [];
|
|
630
815
|
if (fields.length > 0) {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
sourceParams.push(typeId, field);
|
|
816
|
+
const byField = rows(where, ["type_id", "field"], fields);
|
|
817
|
+
sources.push(`SELECT ci.record_id AS record_id, ci.value_rank AS value_rank, ci.num_value AS num_value, ci.text_key AS text_key, ci.text_value AS text_value FROM ${byField} f JOIN content_index ci ON ci.field = f.field JOIN records t ON t.id = ci.record_id AND t.type_id = f.type_id`);
|
|
634
818
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
819
|
+
if (constants.length > 0) {
|
|
820
|
+
const byConstant = rows(where, ["type_id", "value_rank", "num_value", "text_key", "text_value"], constants.map(([typeId, entry]) => entry.kind === "num" ? [typeId, 0, entry.num, null, null] : [typeId, 1, null, entry.key, entry.text]));
|
|
821
|
+
sources.push(`SELECT t.id AS record_id, k.value_rank AS value_rank, k.num_value AS num_value, k.text_key AS text_key, k.text_value AS text_value FROM ${byConstant} k JOIN records t ON t.type_id = k.type_id`);
|
|
638
822
|
}
|
|
639
|
-
const { conditions, params } = recordConditions(query);
|
|
640
823
|
if (cursor)
|
|
641
|
-
conditions.push(presentCursorCondition(cursor, op, params));
|
|
824
|
+
where.conditions.push(presentCursorCondition(cursor, op, where.params));
|
|
642
825
|
const from = `(${sources.join(" UNION ALL ")}) ${SORT_ALIAS} JOIN records r ON r.id = ${SORT_ALIAS}.record_id`;
|
|
643
826
|
const order = `${SORT_ALIAS}.value_rank ${dir}, ${SORT_ALIAS}.num_value ${dir}, ${SORT_ALIAS}.text_key ${dir}, ${SORT_ALIAS}.text_value ${dir}, ${SORT_ALIAS}.record_id ${dir}`;
|
|
644
|
-
pages.push(statement(
|
|
827
|
+
pages.push(statement(where, from, ` ORDER BY ${order} LIMIT ?`));
|
|
645
828
|
}
|
|
646
829
|
{
|
|
647
|
-
const
|
|
830
|
+
const where = recordConditions(query, reach);
|
|
648
831
|
if (fields.length > 0) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
params.push(typeId, field);
|
|
832
|
+
const byField = rows(where, ["type_id", "field"], fields);
|
|
833
|
+
where.conditions.push(`NOT EXISTS (SELECT 1 FROM ${byField} f JOIN content_index ${SORT_ALIAS} ON ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = f.field WHERE f.type_id = r.type_id)`);
|
|
652
834
|
}
|
|
653
835
|
if (constants.length > 0) {
|
|
654
|
-
|
|
655
|
-
|
|
836
|
+
const typeIds = constants.map(([typeId]) => typeId);
|
|
837
|
+
where.conditions.push(`NOT (${inList(where, "r.type_id", typeIds)})`);
|
|
656
838
|
}
|
|
657
839
|
if (cursor?.kind === "absent") {
|
|
658
|
-
conditions.push(`r.id ${op} ?`);
|
|
659
|
-
params.push(cursor.id);
|
|
840
|
+
where.conditions.push(`r.id ${op} ?`);
|
|
841
|
+
where.params.push(cursor.id);
|
|
660
842
|
}
|
|
661
|
-
pages.push(statement(
|
|
843
|
+
pages.push(statement(where, "records r", ` ORDER BY r.id ${dir} LIMIT ?`));
|
|
662
844
|
}
|
|
663
845
|
return pages;
|
|
664
846
|
};
|
|
@@ -751,154 +933,6 @@ var ContentIndex = class {
|
|
|
751
933
|
return fields;
|
|
752
934
|
}
|
|
753
935
|
};
|
|
754
|
-
var toMs = (d) => d.getTime();
|
|
755
|
-
var actorFromColumns = (subject, principal) => subject == null ? void 0 : {
|
|
756
|
-
subjectId: subject,
|
|
757
|
-
...principal != null && { principalId: principal }
|
|
758
|
-
};
|
|
759
|
-
var fromMs = (ms) => new Date(ms);
|
|
760
|
-
var rowToRecord = (row, associations) => {
|
|
761
|
-
const record = {
|
|
762
|
-
id: row.id,
|
|
763
|
-
typeId: row.type_id,
|
|
764
|
-
createdAt: fromMs(row.created_at),
|
|
765
|
-
updatedAt: fromMs(row.updated_at),
|
|
766
|
-
content: JSON.parse(row.content),
|
|
767
|
-
version: row.version
|
|
768
|
-
};
|
|
769
|
-
if (row.parent_id != null)
|
|
770
|
-
record.parentId = row.parent_id;
|
|
771
|
-
if (row.app_id != null)
|
|
772
|
-
record.appId = row.app_id;
|
|
773
|
-
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
774
|
-
if (createdBy)
|
|
775
|
-
record.createdBy = createdBy;
|
|
776
|
-
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
777
|
-
if (updatedBy)
|
|
778
|
-
record.updatedBy = updatedBy;
|
|
779
|
-
if (row.deleted_at != null)
|
|
780
|
-
record.deletedAt = fromMs(row.deleted_at);
|
|
781
|
-
if (row.unlisted_at != null)
|
|
782
|
-
record.unlistedAt = fromMs(row.unlisted_at);
|
|
783
|
-
const partitioned = partitionAssociations(associations);
|
|
784
|
-
if (partitioned.associations.length)
|
|
785
|
-
record.associations = partitioned.associations;
|
|
786
|
-
if (partitioned.permissions.length)
|
|
787
|
-
record.permissions = partitioned.permissions;
|
|
788
|
-
return record;
|
|
789
|
-
};
|
|
790
|
-
var rowToAssociation = (row) => {
|
|
791
|
-
if (row.kind === "tag") {
|
|
792
|
-
return { kind: "tag", label: row.label };
|
|
793
|
-
}
|
|
794
|
-
if (row.kind === "anyone") {
|
|
795
|
-
return { kind: "anyone", label: "read" };
|
|
796
|
-
}
|
|
797
|
-
if (row.kind === "permission") {
|
|
798
|
-
return {
|
|
799
|
-
kind: "permission",
|
|
800
|
-
label: row.label,
|
|
801
|
-
grantee: rowToGrantee(row)
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
if (row.kind === "attachment") {
|
|
805
|
-
const attachmentRecordId = row.attachment_record_id;
|
|
806
|
-
return {
|
|
807
|
-
kind: "attachment",
|
|
808
|
-
label: row.label,
|
|
809
|
-
fileId: row.file_id,
|
|
810
|
-
// Stored as '' when absent, like every other optional column here —
|
|
811
|
-
// the field is omitted rather than served empty, so a round trip
|
|
812
|
-
// produces the association the caller wrote.
|
|
813
|
-
...attachmentRecordId && { attachmentRecordId }
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
return {
|
|
817
|
-
kind: "relationship",
|
|
818
|
-
label: row.label,
|
|
819
|
-
target: rowToTarget(row)
|
|
820
|
-
};
|
|
821
|
-
};
|
|
822
|
-
var associationKeyColumns = (a) => {
|
|
823
|
-
if (a.kind === "attachment")
|
|
824
|
-
return [a.fileId, "", "", "", "", ""];
|
|
825
|
-
if (a.kind === "permission") {
|
|
826
|
-
const g = a.grantee;
|
|
827
|
-
return g.kind === "entity" ? ["", "entity", g.entityId, "", "", ""] : ["", "group", g.groupId, "", "", g.role];
|
|
828
|
-
}
|
|
829
|
-
if (a.kind !== "relationship")
|
|
830
|
-
return ["", "", "", "", "", ""];
|
|
831
|
-
const t = a.target;
|
|
832
|
-
if (t.kind === "entity")
|
|
833
|
-
return ["", "entity", t.entityId, "", "", ""];
|
|
834
|
-
if (t.kind === "external")
|
|
835
|
-
return ["", "external", t.id, t.ns, "", ""];
|
|
836
|
-
return ["", "record", t.recordId, "", t.stackUrl ?? "", ""];
|
|
837
|
-
};
|
|
838
|
-
var rowToGrantee = (row) => row.related_scope === "group" ? {
|
|
839
|
-
kind: "group",
|
|
840
|
-
groupId: row.related_id,
|
|
841
|
-
role: row.related_role
|
|
842
|
-
} : { kind: "entity", entityId: row.related_id };
|
|
843
|
-
var rowToTarget = (row) => {
|
|
844
|
-
const id = row.related_id;
|
|
845
|
-
if (row.related_scope === "entity")
|
|
846
|
-
return { kind: "entity", entityId: id };
|
|
847
|
-
if (row.related_scope === "external") {
|
|
848
|
-
return { kind: "external", ns: row.related_ns, id };
|
|
849
|
-
}
|
|
850
|
-
const stackUrl = row.related_stack;
|
|
851
|
-
return { kind: "record", recordId: id, ...stackUrl && { stackUrl } };
|
|
852
|
-
};
|
|
853
|
-
var rowToType = (row) => {
|
|
854
|
-
const t = {
|
|
855
|
-
id: row.id,
|
|
856
|
-
baseId: row.base_id,
|
|
857
|
-
version: row.version,
|
|
858
|
-
name: row.name,
|
|
859
|
-
schema: JSON.parse(row.schema),
|
|
860
|
-
schemaHash: row.schema_hash,
|
|
861
|
-
createdAt: fromMs(row.created_at)
|
|
862
|
-
};
|
|
863
|
-
if (row.migrates_from)
|
|
864
|
-
t.migratesFrom = row.migrates_from;
|
|
865
|
-
return t;
|
|
866
|
-
};
|
|
867
|
-
var rowToVersion = (row) => {
|
|
868
|
-
const v = {
|
|
869
|
-
version: row.version,
|
|
870
|
-
typeId: row.type_id,
|
|
871
|
-
content: JSON.parse(row.content),
|
|
872
|
-
updatedAt: fromMs(row.updated_at)
|
|
873
|
-
};
|
|
874
|
-
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
875
|
-
if (createdBy)
|
|
876
|
-
v.createdBy = createdBy;
|
|
877
|
-
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
878
|
-
if (updatedBy)
|
|
879
|
-
v.updatedBy = updatedBy;
|
|
880
|
-
return v;
|
|
881
|
-
};
|
|
882
|
-
var rowToJournalEntry = (row) => {
|
|
883
|
-
const e = {
|
|
884
|
-
seq: row.seq,
|
|
885
|
-
at: fromMs(row.at),
|
|
886
|
-
kind: row.kind,
|
|
887
|
-
ops: JSON.parse(row.ops),
|
|
888
|
-
version: row.version,
|
|
889
|
-
typeId: row.type_id
|
|
890
|
-
};
|
|
891
|
-
if (row.parent_id != null)
|
|
892
|
-
e.parentId = row.parent_id;
|
|
893
|
-
if (row.previous_parent_id != null) {
|
|
894
|
-
e.previousParentId = row.previous_parent_id === "" ? null : row.previous_parent_id;
|
|
895
|
-
}
|
|
896
|
-
if (row.actor != null)
|
|
897
|
-
e.actor = JSON.parse(row.actor);
|
|
898
|
-
if (row.associations != null)
|
|
899
|
-
e.associations = JSON.parse(row.associations);
|
|
900
|
-
return e;
|
|
901
|
-
};
|
|
902
936
|
|
|
903
937
|
// ../sqlite-shared/dist/record-logic.js
|
|
904
938
|
var asStackBadRequestError = (query, run) => {
|
|
@@ -930,6 +964,35 @@ var versionRowValues = (version) => [
|
|
|
930
964
|
version.updatedBy?.subjectId ?? null,
|
|
931
965
|
version.updatedBy?.principalId ?? null
|
|
932
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
|
+
];
|
|
933
996
|
var SharedSqlRecordLogic = class {
|
|
934
997
|
exec;
|
|
935
998
|
searchIndex;
|
|
@@ -957,6 +1020,18 @@ var SharedSqlRecordLogic = class {
|
|
|
957
1020
|
throw recordNotFound(id);
|
|
958
1021
|
throw versionConflict(id, expectedVersion, version);
|
|
959
1022
|
}
|
|
1023
|
+
/**
|
|
1024
|
+
* An aspect write's record, inside its transaction: missing is not found,
|
|
1025
|
+
* a tombstone is refused, and anything else answers its version.
|
|
1026
|
+
*/
|
|
1027
|
+
liveVersion(id) {
|
|
1028
|
+
const row = this.exec.get("SELECT version, deleted_at FROM records WHERE id = ?", [id]);
|
|
1029
|
+
if (!row)
|
|
1030
|
+
throw recordNotFound(id);
|
|
1031
|
+
if (row.deleted_at !== null)
|
|
1032
|
+
throw softDeletedRefusal(id);
|
|
1033
|
+
return row.version;
|
|
1034
|
+
}
|
|
960
1035
|
currentVersion(id) {
|
|
961
1036
|
return this.exec.get("SELECT version FROM records WHERE id = ?", [id])?.version;
|
|
962
1037
|
}
|
|
@@ -1013,41 +1088,71 @@ var SharedSqlRecordLogic = class {
|
|
|
1013
1088
|
*/
|
|
1014
1089
|
async createRecord(record, opts = {}) {
|
|
1015
1090
|
this.exec.transaction(() => {
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
|
1018
1125
|
(id, type_id, created_at, updated_at, content, version,
|
|
1019
1126
|
parent_id, entity_id, app_id, principal_id, updated_by, updated_via,
|
|
1020
1127
|
deleted_at, unlisted_at)
|
|
1021
1128
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
}
|
|
1041
|
-
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}"`);
|
|
1042
1147
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
this.
|
|
1047
|
-
|
|
1048
|
-
this.
|
|
1049
|
-
|
|
1050
|
-
|
|
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);
|
|
1051
1156
|
}
|
|
1052
1157
|
async getRecord(id) {
|
|
1053
1158
|
return this.readRecord(id);
|
|
@@ -1060,56 +1165,55 @@ var SharedSqlRecordLogic = class {
|
|
|
1060
1165
|
*/
|
|
1061
1166
|
readRecord(id) {
|
|
1062
1167
|
const row = this.exec.get("SELECT * FROM records WHERE id = ?", [id]);
|
|
1063
|
-
return row ? this.hydrate(row) : null;
|
|
1168
|
+
return row ? this.hydrate([row])[0] : null;
|
|
1064
1169
|
}
|
|
1065
|
-
/**
|
|
1066
|
-
|
|
1067
|
-
|
|
1170
|
+
/**
|
|
1171
|
+
* Records rows with their associations and permissions attached: one
|
|
1172
|
+
* statement per element table for the whole list, so a page costs three
|
|
1173
|
+
* reads however many records it holds.
|
|
1174
|
+
*/
|
|
1175
|
+
hydrate(rows2) {
|
|
1176
|
+
const ids = rows2.map((row) => row.id);
|
|
1177
|
+
const associations = this.elementsByRecord("associations", ids, rowToAssociation);
|
|
1178
|
+
const permissions = this.elementsByRecord("permissions", ids, rowToPermission);
|
|
1179
|
+
return rows2.map((row) => rowToRecord(row, associations.get(row.id) ?? [], permissions.get(row.id) ?? []));
|
|
1068
1180
|
}
|
|
1069
1181
|
/**
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1182
|
+
* The patch applies to the content as it stands when the write lands,
|
|
1183
|
+
* read inside the same transaction.
|
|
1072
1184
|
*/
|
|
1073
|
-
async
|
|
1074
|
-
const now = toMs(/* @__PURE__ */ new Date());
|
|
1075
|
-
const sets = [];
|
|
1076
|
-
const values = [];
|
|
1077
|
-
if (changes.parentId !== void 0) {
|
|
1078
|
-
sets.push("parent_id = ?");
|
|
1079
|
-
values.push(changes.parentId);
|
|
1080
|
-
}
|
|
1081
|
-
if (changes.unlisted !== void 0) {
|
|
1082
|
-
sets.push("unlisted_at = ?");
|
|
1083
|
-
values.push(changes.unlisted ? now : null);
|
|
1084
|
-
}
|
|
1185
|
+
async patchContent(id, patch, opts = {}) {
|
|
1085
1186
|
this.exec.transaction(() => {
|
|
1086
1187
|
const current = this.readForWrite(id, opts.ifVersion);
|
|
1087
|
-
if (opts.bumpsVersion === false) {
|
|
1088
|
-
if (sets.length > 0) {
|
|
1089
|
-
this.exec.run(`UPDATE records SET ${sets.join(", ")} WHERE id = ?`, [...values, id]);
|
|
1090
|
-
}
|
|
1091
|
-
this.replaceAssociationHalves(id, changes);
|
|
1092
|
-
this.appendJournal(id, opts.journal);
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
const merged = changes.contentPatch ? applyMergePatch(current.content, changes.contentPatch) : void 0;
|
|
1096
|
-
if (merged !== void 0) {
|
|
1097
|
-
sets.unshift("content = ?");
|
|
1098
|
-
values.unshift(JSON.stringify(merged));
|
|
1099
|
-
}
|
|
1100
1188
|
if (opts.snapshot)
|
|
1101
1189
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1190
|
+
this.rewriteContent(id, current.typeId, applyMergePatch(current.content, patch), opts);
|
|
1191
|
+
this.appendJournal(id, opts.journal);
|
|
1192
|
+
});
|
|
1193
|
+
return this.reread(id, "patchContent");
|
|
1194
|
+
}
|
|
1195
|
+
async setParent(id, parentId, opts = {}) {
|
|
1196
|
+
return this.aspectUpdate(id, "parent_id = ?", parentId, opts, "setParent");
|
|
1197
|
+
}
|
|
1198
|
+
/** Stamped fresh: `Stack` hands this only a transition. */
|
|
1199
|
+
async setUnlisted(id, unlisted, opts = {}) {
|
|
1200
|
+
return this.aspectUpdate(id, "unlisted_at = ?", unlisted ? toMs(/* @__PURE__ */ new Date()) : null, opts, "setUnlisted");
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* One records column that never bumps. The tombstone refusal and the
|
|
1204
|
+
* `ifVersion` guard ride in the WHERE clause, as on a versioned update.
|
|
1205
|
+
*/
|
|
1206
|
+
aspectUpdate(id, set, value, opts, verb) {
|
|
1207
|
+
const guarded = opts.ifVersion !== void 0;
|
|
1208
|
+
this.exec.transaction(() => {
|
|
1209
|
+
const changed = this.exec.run(`UPDATE records SET ${set}
|
|
1210
|
+
WHERE id = ? AND deleted_at IS NULL${guarded ? " AND version = ?" : ""}`, [value, id, ...guarded ? [opts.ifVersion] : []]);
|
|
1211
|
+
if (changed === 0) {
|
|
1212
|
+
throw versionConflict(id, opts.ifVersion, this.liveVersion(id));
|
|
1109
1213
|
}
|
|
1110
1214
|
this.appendJournal(id, opts.journal);
|
|
1111
1215
|
});
|
|
1112
|
-
return this.reread(id,
|
|
1216
|
+
return this.reread(id, verb);
|
|
1113
1217
|
}
|
|
1114
1218
|
async deleteRecord(id, opts = {}) {
|
|
1115
1219
|
if (opts.purge) {
|
|
@@ -1139,6 +1243,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1139
1243
|
this.checkExpectedVersion(id, purged.version, ifVersion);
|
|
1140
1244
|
this.searchIndex.remove(id);
|
|
1141
1245
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
1246
|
+
this.exec.run("DELETE FROM permissions WHERE record_id = ?", [id]);
|
|
1142
1247
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
1143
1248
|
this.exec.run("DELETE FROM journal WHERE record_id = ?", [id]);
|
|
1144
1249
|
this.contentIndex.remove(id);
|
|
@@ -1189,20 +1294,27 @@ var SharedSqlRecordLogic = class {
|
|
|
1189
1294
|
this.searchIndex.insert(id, json);
|
|
1190
1295
|
this.contentIndex.sync(id, typeId, content);
|
|
1191
1296
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1297
|
+
queryRecords(query) {
|
|
1298
|
+
return this.page(query);
|
|
1299
|
+
}
|
|
1300
|
+
queryReadable(query, reach) {
|
|
1301
|
+
return this.page(query, reach);
|
|
1302
|
+
}
|
|
1303
|
+
/** `reach`, when given, is one more condition of every statement in the plan. */
|
|
1304
|
+
async page(query, reach) {
|
|
1305
|
+
const { limit } = query;
|
|
1194
1306
|
const fetch = limit + 1;
|
|
1195
|
-
const
|
|
1196
|
-
for (const page2 of buildQueryPlan(query)) {
|
|
1197
|
-
const budget = fetch -
|
|
1307
|
+
const rows2 = [];
|
|
1308
|
+
for (const page2 of buildQueryPlan(query, reach)) {
|
|
1309
|
+
const budget = fetch - rows2.length;
|
|
1198
1310
|
if (budget <= 0)
|
|
1199
1311
|
break;
|
|
1200
1312
|
const statement2 = atBudget(page2, budget);
|
|
1201
|
-
|
|
1313
|
+
rows2.push(...asStackBadRequestError(query, () => this.exec.all(statement2.sql, statement2.params)));
|
|
1202
1314
|
}
|
|
1203
|
-
const hasMore =
|
|
1204
|
-
const page = hasMore ?
|
|
1205
|
-
const records =
|
|
1315
|
+
const hasMore = rows2.length > limit;
|
|
1316
|
+
const page = hasMore ? rows2.slice(0, limit) : rows2;
|
|
1317
|
+
const records = this.hydrate(page);
|
|
1206
1318
|
const lastRecord = records[records.length - 1];
|
|
1207
1319
|
const cursor = hasMore && lastRecord ? makeCursor(lastRecord, query.sort, (record, field) => this.contentIndex.kindOf(record.typeId, field)) : null;
|
|
1208
1320
|
return { records, cursor };
|
|
@@ -1211,7 +1323,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1211
1323
|
* The reference check and the purges share one synchronous transaction,
|
|
1212
1324
|
* so nothing can add a reference between the two.
|
|
1213
1325
|
*/
|
|
1214
|
-
async deleteUnreferencedAttachmentRecords(fileId,
|
|
1326
|
+
async deleteUnreferencedAttachmentRecords(fileId, metadataBaseId) {
|
|
1215
1327
|
return this.exec.transaction(() => {
|
|
1216
1328
|
const referenced = this.exec.all(`SELECT 1 as found FROM associations WHERE kind = 'attachment' AND file_id = ?
|
|
1217
1329
|
UNION ALL
|
|
@@ -1220,11 +1332,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1220
1332
|
if (referenced.length) {
|
|
1221
1333
|
throw new StackConflictError("Attachment is still referenced by one or more records");
|
|
1222
1334
|
}
|
|
1223
|
-
|
|
1224
|
-
return [];
|
|
1225
|
-
const placeholders = metadataTypeIds.map(() => "?").join(", ");
|
|
1335
|
+
const family = inFamily("type_id", metadataBaseId);
|
|
1226
1336
|
const metaRows = this.exec.all(`SELECT id FROM records
|
|
1227
|
-
WHERE
|
|
1337
|
+
WHERE ${family.sql} AND json_extract(content, '$.fileId') = ?`, [...family.params, fileId]);
|
|
1228
1338
|
const deleted = [];
|
|
1229
1339
|
for (const row of metaRows) {
|
|
1230
1340
|
const purged = this.purgeRecord(row.id);
|
|
@@ -1244,8 +1354,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1244
1354
|
conditions.push("version < ?");
|
|
1245
1355
|
values.push(query.beforeVersion);
|
|
1246
1356
|
}
|
|
1247
|
-
const
|
|
1248
|
-
return
|
|
1357
|
+
const rows2 = this.exec.all(`SELECT * FROM versions WHERE ${conditions.join(" AND ")} ORDER BY version DESC${query.limit !== void 0 ? " LIMIT ?" : ""}`, query.limit !== void 0 ? [...values, query.limit] : values);
|
|
1358
|
+
return rows2.map(rowToVersion);
|
|
1249
1359
|
}
|
|
1250
1360
|
async getVersion(id, version) {
|
|
1251
1361
|
return this.readVersion(id, version);
|
|
@@ -1267,7 +1377,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1267
1377
|
VALUES (?, ?, ${VERSION_COLUMNS.map(() => "?").join(", ")})`, [id, version.version, ...versionRowValues(version)]);
|
|
1268
1378
|
} catch (err) {
|
|
1269
1379
|
if (isUniqueConstraintViolation(err)) {
|
|
1270
|
-
throw new StackConflictError(`Version ${version.version} already exists for record "${id}"
|
|
1380
|
+
throw new StackConflictError(`Version ${version.version} already exists for record "${id}"`);
|
|
1271
1381
|
}
|
|
1272
1382
|
throw err;
|
|
1273
1383
|
}
|
|
@@ -1283,14 +1393,6 @@ var SharedSqlRecordLogic = class {
|
|
|
1283
1393
|
SET ${VERSION_COLUMNS.map((c) => `${c} = ?`).join(", ")}
|
|
1284
1394
|
WHERE record_id = ? AND version = ?`, [...versionRowValues(version), id, version.version]);
|
|
1285
1395
|
}
|
|
1286
|
-
/**
|
|
1287
|
-
* Standalone snapshot write for tooling and tests — loud on any
|
|
1288
|
-
* collision, since nothing here is about to bump the version. Mutating
|
|
1289
|
-
* methods take a `snapshot` option instead; see snapshotBeforeMutation.
|
|
1290
|
-
*/
|
|
1291
|
-
async saveVersion(id, version) {
|
|
1292
|
-
this.insertVersionRow(id, version);
|
|
1293
|
-
}
|
|
1294
1396
|
/**
|
|
1295
1397
|
* The snapshot half of a mutating method's atomic snapshot-then-mutate
|
|
1296
1398
|
* transaction. A collision is a racing-writer conflict, except an orphan
|
|
@@ -1304,8 +1406,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1304
1406
|
} catch (err) {
|
|
1305
1407
|
if (!(err instanceof StackConflictError))
|
|
1306
1408
|
throw err;
|
|
1307
|
-
if (this.currentVersion(id) !== version.version)
|
|
1308
|
-
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
|
+
}
|
|
1309
1412
|
this.overwriteVersionRow(id, version);
|
|
1310
1413
|
}
|
|
1311
1414
|
}
|
|
@@ -1314,9 +1417,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1314
1417
|
// -------------------------------------------------------
|
|
1315
1418
|
/**
|
|
1316
1419
|
* Append one entry, stamped from the row as it now stands, so callers run
|
|
1317
|
-
* this inside their write's transaction, after the write. `seq` is
|
|
1318
|
-
*
|
|
1319
|
-
* 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.
|
|
1320
1423
|
*/
|
|
1321
1424
|
appendJournal(recordId, entry) {
|
|
1322
1425
|
if (!entry)
|
|
@@ -1324,26 +1427,20 @@ var SharedSqlRecordLogic = class {
|
|
|
1324
1427
|
const row = this.exec.get("SELECT version, type_id, parent_id FROM records WHERE id = ?", [recordId]);
|
|
1325
1428
|
if (!row)
|
|
1326
1429
|
throw recordNotFound(recordId);
|
|
1327
|
-
this.exec.
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
entry.
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
row.parent_id,
|
|
1342
|
-
// '' is the root; NULL is an entry that names no origin at all.
|
|
1343
|
-
entry.previousParentId === void 0 ? null : entry.previousParentId ?? "",
|
|
1344
|
-
entry.actor ? JSON.stringify(entry.actor) : null,
|
|
1345
|
-
entry.associations?.length ? JSON.stringify(entry.associations) : null
|
|
1346
|
-
]);
|
|
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
|
+
}));
|
|
1347
1444
|
}
|
|
1348
1445
|
async getJournal(id, query = {}) {
|
|
1349
1446
|
if (!this.exec.get("SELECT 1 FROM records WHERE id = ?", [id])) {
|
|
@@ -1355,8 +1452,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1355
1452
|
conditions.push("seq > ?");
|
|
1356
1453
|
values.push(query.afterSeq);
|
|
1357
1454
|
}
|
|
1358
|
-
const
|
|
1359
|
-
return
|
|
1455
|
+
const rows2 = this.exec.all(`SELECT * FROM journal WHERE ${conditions.join(" AND ")} ORDER BY seq ASC${query.limit !== void 0 ? " LIMIT ?" : ""}`, query.limit !== void 0 ? [...values, query.limit] : values);
|
|
1456
|
+
return rows2.map(rowToJournalEntry);
|
|
1360
1457
|
}
|
|
1361
1458
|
// -------------------------------------------------------
|
|
1362
1459
|
// Types
|
|
@@ -1381,66 +1478,71 @@ var SharedSqlRecordLogic = class {
|
|
|
1381
1478
|
return row ? rowToType(row) : null;
|
|
1382
1479
|
}
|
|
1383
1480
|
async listTypes() {
|
|
1384
|
-
const
|
|
1385
|
-
return
|
|
1481
|
+
const rows2 = this.exec.all("SELECT * FROM types ORDER BY base_id, version");
|
|
1482
|
+
return rows2.map(rowToType);
|
|
1386
1483
|
}
|
|
1387
1484
|
// -------------------------------------------------------
|
|
1388
1485
|
// Associations
|
|
1389
1486
|
// -------------------------------------------------------
|
|
1390
1487
|
/**
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1488
|
+
* A write to one of a record's two element tables, inside a transaction
|
|
1489
|
+
* that first checks the record exists, is not soft-deleted, is at
|
|
1490
|
+
* `ifVersion`, and that the table holds `expectSet`. Never bumps and
|
|
1491
|
+
* never snapshots.
|
|
1394
1492
|
*/
|
|
1395
|
-
|
|
1396
|
-
assertOneSurface(changes);
|
|
1493
|
+
elementWrite(id, table, opts, write, verb) {
|
|
1397
1494
|
this.exec.transaction(() => {
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
this.exec.run(`DELETE FROM associations
|
|
1405
|
-
WHERE record_id = ?
|
|
1406
|
-
AND kind = ?
|
|
1407
|
-
AND label = ?
|
|
1408
|
-
AND file_id = ?
|
|
1409
|
-
AND related_scope = ?
|
|
1410
|
-
AND related_id = ?
|
|
1411
|
-
AND related_ns = ?
|
|
1412
|
-
AND related_stack = ?
|
|
1413
|
-
AND related_role = ?`, [recordId, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1495
|
+
this.checkExpectedVersion(id, this.liveVersion(id), opts.ifVersion);
|
|
1496
|
+
if (opts.expectSet) {
|
|
1497
|
+
const map = table === "associations" ? rowToAssociation : rowToPermission;
|
|
1498
|
+
const stored = this.elementsByRecord(table, [id], map).get(id) ?? [];
|
|
1499
|
+
if (!sameAssociationSet(stored, opts.expectSet))
|
|
1500
|
+
throw new ElementSetMovedError(id);
|
|
1414
1501
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
this.insertAssociations(recordId, adds);
|
|
1418
|
-
this.appendJournal(recordId, opts.journal);
|
|
1502
|
+
write();
|
|
1503
|
+
this.appendJournal(id, opts.journal);
|
|
1419
1504
|
});
|
|
1420
|
-
return this.reread(
|
|
1505
|
+
return this.reread(id, verb);
|
|
1421
1506
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1507
|
+
async editAssociations(id, edits, opts = {}) {
|
|
1508
|
+
return this.elementWrite(id, "associations", opts, () => {
|
|
1509
|
+
for (const edit of edits) {
|
|
1510
|
+
if (edit.op !== "remove")
|
|
1511
|
+
continue;
|
|
1512
|
+
const association = edit.association;
|
|
1513
|
+
this.exec.run(`DELETE FROM associations
|
|
1514
|
+
WHERE record_id = ? AND kind = ? AND label = ?
|
|
1515
|
+
AND file_id = ? AND related_scope = ? AND related_id = ?
|
|
1516
|
+
AND related_ns = ? AND related_stack = ?`, [id, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1517
|
+
}
|
|
1518
|
+
this.insertAssociations(id, addsOf(edits));
|
|
1519
|
+
}, "editAssociations");
|
|
1520
|
+
}
|
|
1521
|
+
async setAssociations(id, associations, opts = {}) {
|
|
1522
|
+
return this.elementWrite(id, "associations", opts, () => {
|
|
1523
|
+
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
1524
|
+
this.insertAssociations(id, associations);
|
|
1525
|
+
}, "setAssociations");
|
|
1526
|
+
}
|
|
1527
|
+
/** editAssociations() over the permissions table. */
|
|
1528
|
+
async editAccess(id, edits, opts = {}) {
|
|
1529
|
+
return this.elementWrite(id, "permissions", opts, () => {
|
|
1530
|
+
for (const edit of edits) {
|
|
1531
|
+
if (edit.op !== "remove")
|
|
1532
|
+
continue;
|
|
1533
|
+
const permission = edit.association;
|
|
1534
|
+
this.exec.run(`DELETE FROM permissions
|
|
1535
|
+
WHERE record_id = ? AND kind = ? AND label = ?
|
|
1536
|
+
AND grantee_kind = ? AND grantee_id = ? AND role = ?`, [id, permission.kind, permission.label, ...permissionKeyColumns(permission)]);
|
|
1537
|
+
}
|
|
1538
|
+
this.insertPermissions(id, addsOf(edits));
|
|
1539
|
+
}, "editAccess");
|
|
1434
1540
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
replaceAssociations(recordId, associations, half) {
|
|
1441
|
-
this.exec.run(`DELETE FROM associations WHERE record_id = ? AND kind ${half === "data" ? "NOT IN" : "IN"} (${AUTHORITY_ASSOCIATION_KINDS.map(() => "?").join(", ")})`, [recordId, ...AUTHORITY_ASSOCIATION_KINDS]);
|
|
1442
|
-
if (associations.length)
|
|
1443
|
-
this.insertAssociations(recordId, associations);
|
|
1541
|
+
async setAccess(id, permissions, opts = {}) {
|
|
1542
|
+
return this.elementWrite(id, "permissions", opts, () => {
|
|
1543
|
+
this.exec.run("DELETE FROM permissions WHERE record_id = ?", [id]);
|
|
1544
|
+
this.insertPermissions(id, permissions);
|
|
1545
|
+
}, "setAccess");
|
|
1444
1546
|
}
|
|
1445
1547
|
/**
|
|
1446
1548
|
* Upserts on association identity, so re-pointing `attachment_record_id`
|
|
@@ -1449,30 +1551,104 @@ var SharedSqlRecordLogic = class {
|
|
|
1449
1551
|
*/
|
|
1450
1552
|
insertAssociations(recordId, associations) {
|
|
1451
1553
|
for (const assoc of associations) {
|
|
1452
|
-
|
|
1453
|
-
this.exec.run(`INSERT INTO associations
|
|
1554
|
+
this.insertRow(recordId, () => this.exec.run(`INSERT INTO associations
|
|
1454
1555
|
(record_id, kind, label, file_id, related_scope, related_id, related_ns,
|
|
1455
|
-
related_stack,
|
|
1456
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
1556
|
+
related_stack, attachment_record_id)
|
|
1557
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1457
1558
|
ON CONFLICT DO UPDATE SET attachment_record_id = excluded.attachment_record_id`, [
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1559
|
+
recordId,
|
|
1560
|
+
assoc.kind,
|
|
1561
|
+
assoc.label,
|
|
1562
|
+
...associationKeyColumns(assoc),
|
|
1563
|
+
assoc.kind === "attachment" ? assoc.attachmentRecordId ?? "" : ""
|
|
1564
|
+
]));
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
/** Every column is in the key, so a repeated element is the same row. */
|
|
1568
|
+
insertPermissions(recordId, permissions) {
|
|
1569
|
+
for (const p of permissions) {
|
|
1570
|
+
this.insertRow(recordId, () => this.exec.run(`INSERT INTO permissions (record_id, kind, label, grantee_kind, grantee_id, role)
|
|
1571
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1572
|
+
ON CONFLICT DO NOTHING`, [recordId, p.kind, p.label, ...permissionKeyColumns(p)]));
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
insertRow(recordId, insert) {
|
|
1576
|
+
try {
|
|
1577
|
+
insert();
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
if (isForeignKeyViolation(err))
|
|
1580
|
+
throw recordNotFound(recordId);
|
|
1581
|
+
throw err;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* One element table's rows for every record in `ids`, in primary-key
|
|
1586
|
+
* order — the order a single record's read has always had.
|
|
1587
|
+
*/
|
|
1588
|
+
elementsByRecord(table, ids, map) {
|
|
1589
|
+
const byRecord = /* @__PURE__ */ new Map();
|
|
1590
|
+
const rows2 = this.exec.all(`WITH ids AS MATERIALIZED ${rowsTable(["id"])}
|
|
1591
|
+
SELECT * FROM ${table} WHERE record_id IN (SELECT id FROM ids)
|
|
1592
|
+
ORDER BY ${table === "associations" ? ASSOCIATION_KEY : PERMISSION_KEY}`, [JSON.stringify(ids.map((id) => [id]))]);
|
|
1593
|
+
for (const row of rows2) {
|
|
1594
|
+
const id = row.record_id;
|
|
1595
|
+
const list = byRecord.get(id);
|
|
1596
|
+
if (list)
|
|
1597
|
+
list.push(map(row));
|
|
1598
|
+
else
|
|
1599
|
+
byRecord.set(id, [map(row)]);
|
|
1600
|
+
}
|
|
1601
|
+
return byRecord;
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
var ASSOCIATION_KEY = "record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack";
|
|
1605
|
+
var PERMISSION_KEY = "record_id, kind, label, grantee_kind, grantee_id, role";
|
|
1606
|
+
var addsOf = (edits) => edits.flatMap((e) => e.op === "add" ? [e.association] : []);
|
|
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);
|
|
1470
1642
|
}
|
|
1643
|
+
return stored;
|
|
1471
1644
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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}.`);
|
|
1475
1650
|
}
|
|
1651
|
+
return writeNewStore(exec, newStoreSeed({ ownerEntityId: owner }));
|
|
1476
1652
|
};
|
|
1477
1653
|
|
|
1478
1654
|
// ../sqlite-shared/dist/record-adapter.js
|
|
@@ -1480,15 +1656,13 @@ var SQLITE_RECORD_CAPABILITIES = {
|
|
|
1480
1656
|
filter: {
|
|
1481
1657
|
content: "path",
|
|
1482
1658
|
contentPresent: true,
|
|
1483
|
-
|
|
1484
|
-
// docs/spec/adapters.md § Adapter capabilities.
|
|
1485
|
-
contentByType: 64,
|
|
1659
|
+
contentByType: true,
|
|
1486
1660
|
search: true
|
|
1487
1661
|
},
|
|
1488
1662
|
sort: {
|
|
1489
1663
|
fields: ["createdAt", "updatedAt", "version"],
|
|
1490
1664
|
contentField: true,
|
|
1491
|
-
contentFieldByType:
|
|
1665
|
+
contentFieldByType: true
|
|
1492
1666
|
},
|
|
1493
1667
|
limits: {
|
|
1494
1668
|
attachmentBytes: null,
|
|
@@ -1498,12 +1672,10 @@ var SQLITE_RECORD_CAPABILITIES = {
|
|
|
1498
1672
|
var SharedSqlRecordAdapter = class {
|
|
1499
1673
|
capabilities = SQLITE_RECORD_CAPABILITIES;
|
|
1500
1674
|
ownerEntityId;
|
|
1501
|
-
timezone;
|
|
1502
1675
|
record;
|
|
1503
1676
|
constructor(exec, config) {
|
|
1504
1677
|
this.record = new SharedSqlRecordLogic({ exec });
|
|
1505
|
-
this.ownerEntityId = config.
|
|
1506
|
-
this.timezone = config.timezone;
|
|
1678
|
+
this.ownerEntityId = config.ownerEntityId;
|
|
1507
1679
|
}
|
|
1508
1680
|
// -------------------------------------------------------
|
|
1509
1681
|
// Records
|
|
@@ -1511,11 +1683,20 @@ var SharedSqlRecordAdapter = class {
|
|
|
1511
1683
|
createRecord(record, opts) {
|
|
1512
1684
|
return this.record.createRecord(record, opts);
|
|
1513
1685
|
}
|
|
1686
|
+
importRecord(record, history) {
|
|
1687
|
+
return this.record.importRecord(record, history);
|
|
1688
|
+
}
|
|
1514
1689
|
getRecord(id) {
|
|
1515
1690
|
return this.record.getRecord(id);
|
|
1516
1691
|
}
|
|
1517
|
-
|
|
1518
|
-
return this.record.
|
|
1692
|
+
patchContent(id, patch, opts) {
|
|
1693
|
+
return this.record.patchContent(id, patch, opts);
|
|
1694
|
+
}
|
|
1695
|
+
setParent(id, parentId, opts) {
|
|
1696
|
+
return this.record.setParent(id, parentId, opts);
|
|
1697
|
+
}
|
|
1698
|
+
setUnlisted(id, unlisted, opts) {
|
|
1699
|
+
return this.record.setUnlisted(id, unlisted, opts);
|
|
1519
1700
|
}
|
|
1520
1701
|
deleteRecord(id, opts) {
|
|
1521
1702
|
return this.record.deleteRecord(id, opts);
|
|
@@ -1532,8 +1713,11 @@ var SharedSqlRecordAdapter = class {
|
|
|
1532
1713
|
queryRecords(query) {
|
|
1533
1714
|
return this.record.queryRecords(query);
|
|
1534
1715
|
}
|
|
1535
|
-
|
|
1536
|
-
return this.record.
|
|
1716
|
+
queryReadable(query, reach) {
|
|
1717
|
+
return this.record.queryReadable(query, reach);
|
|
1718
|
+
}
|
|
1719
|
+
deleteUnreferencedAttachmentRecords(fileId, metadataBaseId) {
|
|
1720
|
+
return this.record.deleteUnreferencedAttachmentRecords(fileId, metadataBaseId);
|
|
1537
1721
|
}
|
|
1538
1722
|
// -------------------------------------------------------
|
|
1539
1723
|
// Versions
|
|
@@ -1544,9 +1728,6 @@ var SharedSqlRecordAdapter = class {
|
|
|
1544
1728
|
getVersion(id, version) {
|
|
1545
1729
|
return this.record.getVersion(id, version);
|
|
1546
1730
|
}
|
|
1547
|
-
saveVersion(id, version) {
|
|
1548
|
-
return this.record.saveVersion(id, version);
|
|
1549
|
-
}
|
|
1550
1731
|
// -------------------------------------------------------
|
|
1551
1732
|
// Change journal
|
|
1552
1733
|
// -------------------------------------------------------
|
|
@@ -1566,10 +1747,19 @@ var SharedSqlRecordAdapter = class {
|
|
|
1566
1747
|
return this.record.listTypes();
|
|
1567
1748
|
}
|
|
1568
1749
|
// -------------------------------------------------------
|
|
1569
|
-
// Associations
|
|
1750
|
+
// Associations and permissions
|
|
1570
1751
|
// -------------------------------------------------------
|
|
1571
|
-
|
|
1572
|
-
return this.record.
|
|
1752
|
+
editAssociations(id, edits, opts) {
|
|
1753
|
+
return this.record.editAssociations(id, edits, opts);
|
|
1754
|
+
}
|
|
1755
|
+
setAssociations(id, associations, opts) {
|
|
1756
|
+
return this.record.setAssociations(id, associations, opts);
|
|
1757
|
+
}
|
|
1758
|
+
editAccess(id, edits, opts) {
|
|
1759
|
+
return this.record.editAccess(id, edits, opts);
|
|
1760
|
+
}
|
|
1761
|
+
setAccess(id, permissions, opts) {
|
|
1762
|
+
return this.record.setAccess(id, permissions, opts);
|
|
1573
1763
|
}
|
|
1574
1764
|
};
|
|
1575
1765
|
var lockPathFor = (dbPath) => `${dbPath}.lock`;
|
|
@@ -1641,8 +1831,8 @@ var SharedTokenLogic = class {
|
|
|
1641
1831
|
return { principalId: row.principal_id, subjectId: row.subject_id };
|
|
1642
1832
|
}
|
|
1643
1833
|
async listTokens() {
|
|
1644
|
-
const
|
|
1645
|
-
return
|
|
1834
|
+
const rows2 = this.exec.all("SELECT id, principal_id, subject_id, label, created_at, expires_at FROM tokens ORDER BY created_at DESC");
|
|
1835
|
+
return rows2.map((row) => ({
|
|
1646
1836
|
id: row.id,
|
|
1647
1837
|
principalId: row.principal_id,
|
|
1648
1838
|
subjectId: row.subject_id,
|
|
@@ -1741,51 +1931,22 @@ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter extends SharedS
|
|
|
1741
1931
|
* See docs/spec/adapters.md § Construction.
|
|
1742
1932
|
*/
|
|
1743
1933
|
static async open(opts) {
|
|
1744
|
-
const mode = opts.create ?? "never";
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
throw new Error(
|
|
1748
|
-
`Cannot open: no database found at "${opts.path}". Pass create: 'ifMissing' or 'exclusive' to create one.`
|
|
1749
|
-
);
|
|
1750
|
-
}
|
|
1751
|
-
if (exists && mode === "exclusive") {
|
|
1752
|
-
throw new Error(
|
|
1753
|
-
`Cannot create: database already exists at "${opts.path}". Pass create: 'never' or 'ifMissing' to open it.`
|
|
1754
|
-
);
|
|
1755
|
-
}
|
|
1756
|
-
};
|
|
1757
|
-
const existedBeforeLock = existsSync(opts.path);
|
|
1758
|
-
checkMode(existedBeforeLock);
|
|
1759
|
-
let newOwner;
|
|
1760
|
-
if (!existedBeforeLock) {
|
|
1761
|
-
newOwner = typeof opts.ownerEntityId === "function" ? await opts.ownerEntityId() : opts.ownerEntityId;
|
|
1762
|
-
if (!newOwner) {
|
|
1763
|
-
throw new Error(
|
|
1764
|
-
`Cannot create: no ownerEntityId given for the new database at "${opts.path}".`
|
|
1765
|
-
);
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1934
|
+
const mode = opts.seed ? "exclusive" : opts.create ?? "never";
|
|
1935
|
+
const where = `"${opts.path}"`;
|
|
1936
|
+
if (mode === "never" && !existsSync(opts.path)) throw missingStoreError(where);
|
|
1768
1937
|
acquireLock(opts.path, opts.force);
|
|
1938
|
+
const madeFile = !existsSync(opts.path);
|
|
1769
1939
|
let db;
|
|
1770
1940
|
try {
|
|
1771
|
-
const exists = existsSync(opts.path);
|
|
1772
|
-
checkMode(exists);
|
|
1773
1941
|
db = new DatabaseSync(opts.path);
|
|
1774
1942
|
const exec = new NativeSqliteExecutor(db);
|
|
1775
|
-
|
|
1776
|
-
let config;
|
|
1777
|
-
if (exists) {
|
|
1778
|
-
config = readStackConfig(exec);
|
|
1779
|
-
if (typeof opts.ownerEntityId === "string" && opts.ownerEntityId !== config.entityId) {
|
|
1780
|
-
throw new OwnerMismatchError(opts.ownerEntityId, config.entityId, `"${opts.path}"`);
|
|
1781
|
-
}
|
|
1782
|
-
} else {
|
|
1783
|
-
if (!newOwner) throw new Error(`Cannot create: "${opts.path}" vanished while opening.`);
|
|
1784
|
-
config = insertConfigRecord(exec, newOwner, opts.timezone);
|
|
1785
|
-
}
|
|
1943
|
+
const config = await openStore(exec, { ...opts, mode, where, wal: true });
|
|
1786
1944
|
return new _NativeSQLiteRecordAdapter(opts.path, db, exec, config);
|
|
1787
1945
|
} catch (err) {
|
|
1788
1946
|
db?.close();
|
|
1947
|
+
if (madeFile) {
|
|
1948
|
+
for (const suffix of ["", "-wal", "-shm"]) rmSync(`${opts.path}${suffix}`, { force: true });
|
|
1949
|
+
}
|
|
1789
1950
|
releaseLock(opts.path);
|
|
1790
1951
|
throw err;
|
|
1791
1952
|
}
|