@haverstack/record-adapter-do-sqlite 0.27.0 → 0.29.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 +26 -21
- package/dist/index.js +741 -551
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { OwnerMismatchError, newStoreSeed, softDeletedRefusal, sameAssociationSet, ElementSetMovedError, FormatMismatchError, contentSortEntry, constantSortEntry, contentSortKey, parseContentFilterKey } from '@haverstack/core/adapter';
|
|
2
|
+
import { StackConflictError, applyMergePatch, StackNotFoundError, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
3
3
|
|
|
4
4
|
// ../sqlite-shared/dist/schema.js
|
|
5
|
+
var FORMAT_VERSION = 1;
|
|
5
6
|
var RECORD_SCHEMA_SQL = `
|
|
6
|
-
|
|
7
|
+
-- Which layout this store is, in its one row, so an export names the one
|
|
8
|
+
-- a reader holds. See docs/spec/portability.md \xA7 The portable format.
|
|
9
|
+
CREATE TABLE format (
|
|
10
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
11
|
+
version INTEGER NOT NULL
|
|
12
|
+
) STRICT;
|
|
13
|
+
|
|
14
|
+
CREATE TABLE records (
|
|
7
15
|
id TEXT PRIMARY KEY,
|
|
8
16
|
type_id TEXT NOT NULL,
|
|
9
17
|
created_at INTEGER NOT NULL,
|
|
@@ -20,41 +28,50 @@ var RECORD_SCHEMA_SQL = `
|
|
|
20
28
|
unlisted_at INTEGER
|
|
21
29
|
) STRICT;
|
|
22
30
|
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
|
|
27
|
-
-- See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
28
|
-
CREATE TABLE IF NOT EXISTS associations (
|
|
31
|
+
-- A record's data associations: tags, attachments and relationships.
|
|
32
|
+
-- Its permissions are in their own table below, so no predicate over this
|
|
33
|
+
-- one can match a grantee as a relationship target.
|
|
34
|
+
CREATE TABLE associations (
|
|
29
35
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
30
|
-
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship'
|
|
36
|
+
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship')),
|
|
31
37
|
label TEXT NOT NULL,
|
|
32
38
|
file_id TEXT NOT NULL DEFAULT '',
|
|
33
39
|
-- A relationship target is (related_scope, related_id) qualified by
|
|
34
|
-
-- related_ns (a foreign namespace) or related_stack (another stack)
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
related_scope TEXT NOT NULL DEFAULT '' CHECK (related_scope IN ('', 'record', 'entity', 'external'
|
|
40
|
+
-- related_ns (a foreign namespace) or related_stack (another stack).
|
|
41
|
+
-- All are in the key, so targets differing only by a qualifier are
|
|
42
|
+
-- distinct associations.
|
|
43
|
+
related_scope TEXT NOT NULL DEFAULT '' CHECK (related_scope IN ('', 'record', 'entity', 'external')),
|
|
38
44
|
related_id TEXT NOT NULL DEFAULT '',
|
|
39
45
|
related_ns TEXT NOT NULL DEFAULT '',
|
|
40
46
|
related_stack TEXT NOT NULL DEFAULT '',
|
|
41
|
-
-- Set only by a group grantee; in the key because member and admin name
|
|
42
|
-
-- two different sets of people.
|
|
43
|
-
related_role TEXT NOT NULL DEFAULT '' CHECK (related_role IN ('', 'member', 'admin')),
|
|
44
47
|
-- Out of the key: it annotates a reference rather than naming one, so
|
|
45
48
|
-- re-pointing it updates the existing row. See docs/spec/attachments.md
|
|
46
49
|
-- \xA7 Naming the upload a reference came from.
|
|
47
50
|
attachment_record_id TEXT NOT NULL DEFAULT '',
|
|
48
51
|
-- Leads with record_id, so its implicit index serves every by-record
|
|
49
52
|
-- read and delete; the indexes below cover only the filter directions.
|
|
50
|
-
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack
|
|
53
|
+
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack)
|
|
54
|
+
) STRICT;
|
|
55
|
+
|
|
56
|
+
-- Who reaches a record, one row per element. An 'anyone' row names no
|
|
57
|
+
-- grantee; a group grantee's role is in the key, because member and
|
|
58
|
+
-- admin name two different sets of people.
|
|
59
|
+
-- See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
60
|
+
CREATE TABLE permissions (
|
|
61
|
+
record_id TEXT NOT NULL REFERENCES records(id),
|
|
62
|
+
kind TEXT NOT NULL CHECK (kind IN ('permission', 'anyone')),
|
|
63
|
+
label TEXT NOT NULL CHECK (label IN ('read', 'write')),
|
|
64
|
+
grantee_kind TEXT NOT NULL DEFAULT '' CHECK (grantee_kind IN ('', 'entity', 'group')),
|
|
65
|
+
grantee_id TEXT NOT NULL DEFAULT '',
|
|
66
|
+
role TEXT NOT NULL DEFAULT '' CHECK (role IN ('', 'member', 'admin')),
|
|
67
|
+
PRIMARY KEY (record_id, kind, label, grantee_kind, grantee_id, role)
|
|
51
68
|
) STRICT;
|
|
52
69
|
|
|
53
70
|
-- Content and the type it is read under. No parent_id, unlisted_at or
|
|
54
71
|
-- associations column: none of those aspects bumps version, so no
|
|
55
72
|
-- version ever snapshots one. See
|
|
56
73
|
-- docs/spec/versioning.md \xA7 Version history.
|
|
57
|
-
CREATE TABLE
|
|
74
|
+
CREATE TABLE versions (
|
|
58
75
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
59
76
|
version INTEGER NOT NULL,
|
|
60
77
|
type_id TEXT NOT NULL,
|
|
@@ -72,7 +89,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
72
89
|
-- by design \u2014 content lives on a versions row, and copying it here
|
|
73
90
|
-- would make the journal the larger of the two stores.
|
|
74
91
|
-- See docs/spec/journal.md.
|
|
75
|
-
CREATE TABLE
|
|
92
|
+
CREATE TABLE journal (
|
|
76
93
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
77
94
|
seq INTEGER NOT NULL,
|
|
78
95
|
at INTEGER NOT NULL,
|
|
@@ -96,7 +113,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
96
113
|
PRIMARY KEY (record_id, seq)
|
|
97
114
|
) STRICT;
|
|
98
115
|
|
|
99
|
-
CREATE TABLE
|
|
116
|
+
CREATE TABLE types (
|
|
100
117
|
id TEXT PRIMARY KEY,
|
|
101
118
|
base_id TEXT NOT NULL,
|
|
102
119
|
version INTEGER NOT NULL,
|
|
@@ -114,7 +131,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
114
131
|
-- value_rank is 0 for a numeric value and 1 for a text one \u2014 the same
|
|
115
132
|
-- split as "num_value IS NULL", but non-null, so one index can carry
|
|
116
133
|
-- the whole ordering. file_id is set only for a 'file-ref' field.
|
|
117
|
-
CREATE TABLE
|
|
134
|
+
CREATE TABLE content_index (
|
|
118
135
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
119
136
|
field TEXT NOT NULL,
|
|
120
137
|
value_rank INTEGER NOT NULL CHECK (value_rank IN (0, 1)),
|
|
@@ -130,34 +147,39 @@ var RECORD_SCHEMA_SQL = `
|
|
|
130
147
|
-- leaves the planner sorting every match before it can honor LIMIT.
|
|
131
148
|
-- deleted_at and unlisted_at get none: an index on a column every query
|
|
132
149
|
-- tests IS NULL only lures the planner off the one answering ORDER BY.
|
|
133
|
-
CREATE INDEX
|
|
134
|
-
CREATE INDEX
|
|
135
|
-
CREATE INDEX
|
|
136
|
-
CREATE INDEX
|
|
137
|
-
CREATE INDEX
|
|
138
|
-
CREATE INDEX
|
|
139
|
-
CREATE INDEX
|
|
140
|
-
CREATE INDEX
|
|
150
|
+
CREATE INDEX idx_records_type_id ON records(type_id, created_at, id);
|
|
151
|
+
CREATE INDEX idx_records_parent_id ON records(parent_id, created_at, id);
|
|
152
|
+
CREATE INDEX idx_records_entity_id ON records(entity_id, created_at, id);
|
|
153
|
+
CREATE INDEX idx_records_app_id ON records(app_id, created_at, id);
|
|
154
|
+
CREATE INDEX idx_records_principal_id ON records(principal_id, created_at, id);
|
|
155
|
+
CREATE INDEX idx_records_created_at ON records(created_at, id);
|
|
156
|
+
CREATE INDEX idx_records_updated_at ON records(updated_at, id);
|
|
157
|
+
CREATE INDEX idx_records_version ON records(version, id);
|
|
141
158
|
|
|
142
159
|
-- Each association index ends with record_id so the semi-joins in
|
|
143
160
|
-- query.ts read the matching record ids out of the index alone.
|
|
144
161
|
-- idx_assoc_related carries related_ns ahead of related_id because a
|
|
145
162
|
-- record- or entity-scoped target always stores '' there, which
|
|
146
163
|
-- buildQueryPlan states explicitly to keep the prefix contiguous.
|
|
147
|
-
CREATE INDEX
|
|
148
|
-
CREATE INDEX
|
|
149
|
-
CREATE INDEX
|
|
150
|
-
CREATE INDEX
|
|
164
|
+
CREATE INDEX idx_assoc_kind_label ON associations(kind, label, record_id);
|
|
165
|
+
CREATE INDEX idx_assoc_kind_file_id ON associations(kind, file_id, record_id);
|
|
166
|
+
CREATE INDEX idx_assoc_related ON associations(kind, related_scope, related_ns, related_id, record_id);
|
|
167
|
+
CREATE INDEX idx_types_base_id ON types(base_id, version);
|
|
168
|
+
|
|
169
|
+
-- The permissions key with record_id moved last, so a scoped query reads
|
|
170
|
+
-- the records carrying an element straight out of the index.
|
|
171
|
+
CREATE INDEX idx_permissions_element
|
|
172
|
+
ON permissions(kind, label, grantee_kind, grantee_id, role, record_id);
|
|
151
173
|
|
|
152
174
|
-- The whole content ordering, in index order, for one field at a time:
|
|
153
175
|
-- a content-sorted page is an ordered walk of a slice of this index.
|
|
154
|
-
CREATE INDEX
|
|
176
|
+
CREATE INDEX idx_content_index_sort
|
|
155
177
|
ON content_index(field, value_rank, num_value, text_key, text_value, record_id);
|
|
156
|
-
CREATE INDEX
|
|
178
|
+
CREATE INDEX idx_content_index_file
|
|
157
179
|
ON content_index(file_id, record_id) WHERE file_id IS NOT NULL;
|
|
158
180
|
`;
|
|
159
181
|
var FTS5_SCHEMA_SQL = `
|
|
160
|
-
CREATE VIRTUAL TABLE
|
|
182
|
+
CREATE VIRTUAL TABLE records_fts USING fts5(
|
|
161
183
|
content,
|
|
162
184
|
content='records',
|
|
163
185
|
content_rowid='rowid',
|
|
@@ -165,10 +187,11 @@ var FTS5_SCHEMA_SQL = `
|
|
|
165
187
|
);
|
|
166
188
|
`;
|
|
167
189
|
var PRAGMA_FOREIGN_KEYS_ON = `PRAGMA foreign_keys = ON;`;
|
|
168
|
-
var
|
|
190
|
+
var PRAGMA_JOURNAL_MODE_WAL = `PRAGMA journal_mode = WAL;`;
|
|
191
|
+
var applyConnectionPragmas = (exec, opts) => {
|
|
169
192
|
exec.exec(PRAGMA_FOREIGN_KEYS_ON);
|
|
170
|
-
|
|
171
|
-
|
|
193
|
+
if (opts.wal)
|
|
194
|
+
exec.exec(PRAGMA_JOURNAL_MODE_WAL);
|
|
172
195
|
};
|
|
173
196
|
|
|
174
197
|
// ../sqlite-shared/dist/fts5.js
|
|
@@ -254,22 +277,6 @@ var restoreImplicitAnd = (query) => withPhrasesMasked(query, (masked) => masked.
|
|
|
254
277
|
// ../sqlite-shared/dist/executor.js
|
|
255
278
|
var isForeignKeyViolation = (err) => err instanceof Error && err.message.includes("FOREIGN KEY constraint failed");
|
|
256
279
|
var isUniqueConstraintViolation = (err) => err instanceof Error && err.message.includes("UNIQUE constraint failed");
|
|
257
|
-
|
|
258
|
-
// ../sqlite-shared/dist/config.js
|
|
259
|
-
var CONFIG_ROW_SQL = `SELECT content FROM records WHERE id = '_config'`;
|
|
260
|
-
var insertConfigRecord = (exec, entityId, timezone) => {
|
|
261
|
-
const now = Date.now();
|
|
262
|
-
exec.run(`INSERT INTO records (id, type_id, created_at, updated_at, content, version)
|
|
263
|
-
VALUES ('_config', '_config@1', ?, ?, ?, 1)`, [now, now, JSON.stringify({ entityId, timezone })]);
|
|
264
|
-
return { entityId, timezone };
|
|
265
|
-
};
|
|
266
|
-
var tryReadStackConfig = (exec) => {
|
|
267
|
-
const row = exec.get(CONFIG_ROW_SQL);
|
|
268
|
-
if (!row)
|
|
269
|
-
return null;
|
|
270
|
-
const content = JSON.parse(row.content);
|
|
271
|
-
return { entityId: content.entityId, timezone: content.timezone };
|
|
272
|
-
};
|
|
273
280
|
var toBase64 = (text) => {
|
|
274
281
|
const bytes = new TextEncoder().encode(text);
|
|
275
282
|
let binary = "";
|
|
@@ -320,24 +327,22 @@ var decodeCursor = (cursor) => {
|
|
|
320
327
|
throw malformed();
|
|
321
328
|
};
|
|
322
329
|
var byTypeDescriptor = (byType) => JSON.stringify(Object.entries(byType).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
323
|
-
var getSortField = (query) => query.sort?.field ?? "createdAt";
|
|
324
330
|
var getSortColumn = (field) => field === "createdAt" ? "created_at" : field === "updatedAt" ? "updated_at" : "version";
|
|
325
|
-
var makeCursor = (record, sort,
|
|
326
|
-
const byType = sort
|
|
331
|
+
var makeCursor = (record, sort, kindOf2) => {
|
|
332
|
+
const byType = sort.contentFieldByType;
|
|
327
333
|
if (byType) {
|
|
328
|
-
const
|
|
334
|
+
const field2 = byTypeDescriptor(byType);
|
|
329
335
|
const key = Object.hasOwn(byType, record.typeId) ? byType[record.typeId] : void 0;
|
|
330
|
-
const kind2 = key?.field === void 0 ? void 0 :
|
|
336
|
+
const kind2 = key?.field === void 0 ? void 0 : kindOf2?.(record, key.field);
|
|
331
337
|
const entry2 = key?.field !== void 0 ? kind2 && contentSortEntry(kind2, record.content[key.field]) : key && constantSortEntry(key.value ?? null);
|
|
332
|
-
return encodeEntry(
|
|
338
|
+
return encodeEntry(field2, entry2 || null, record.id);
|
|
333
339
|
}
|
|
334
|
-
const contentField = sort
|
|
335
|
-
if (
|
|
336
|
-
const field = getSortField({ sort });
|
|
340
|
+
const { contentField, field } = sort;
|
|
341
|
+
if (field !== void 0) {
|
|
337
342
|
const value = field === "updatedAt" ? record.updatedAt.getTime() : field === "version" ? record.version : record.createdAt.getTime();
|
|
338
343
|
return encodeCursor({ kind: "native", field, value, id: record.id });
|
|
339
344
|
}
|
|
340
|
-
const kind =
|
|
345
|
+
const kind = kindOf2?.(record, contentField);
|
|
341
346
|
const entry = kind ? contentSortEntry(kind, record.content[contentField]) : null;
|
|
342
347
|
return encodeEntry(contentField, entry, record.id);
|
|
343
348
|
};
|
|
@@ -347,38 +352,182 @@ var encodeEntry = (field, entry, id) => {
|
|
|
347
352
|
return entry.kind === "num" ? encodeCursor({ kind: "num", field, value: entry.num, id }) : encodeCursor({ kind: "text", field, value: entry.text, id });
|
|
348
353
|
};
|
|
349
354
|
|
|
355
|
+
// ../sqlite-shared/dist/mappers.js
|
|
356
|
+
var toMs = (d) => d.getTime();
|
|
357
|
+
var actorFromColumns = (subject, principal) => subject == null ? void 0 : {
|
|
358
|
+
subjectId: subject,
|
|
359
|
+
...principal != null && { principalId: principal }
|
|
360
|
+
};
|
|
361
|
+
var fromMs = (ms) => new Date(ms);
|
|
362
|
+
var rowToRecord = (row, associations, permissions) => {
|
|
363
|
+
const record = {
|
|
364
|
+
id: row.id,
|
|
365
|
+
typeId: row.type_id,
|
|
366
|
+
createdAt: fromMs(row.created_at),
|
|
367
|
+
updatedAt: fromMs(row.updated_at),
|
|
368
|
+
content: JSON.parse(row.content),
|
|
369
|
+
version: row.version
|
|
370
|
+
};
|
|
371
|
+
if (row.parent_id != null)
|
|
372
|
+
record.parentId = row.parent_id;
|
|
373
|
+
if (row.app_id != null)
|
|
374
|
+
record.appId = row.app_id;
|
|
375
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
376
|
+
if (createdBy)
|
|
377
|
+
record.createdBy = createdBy;
|
|
378
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
379
|
+
if (updatedBy)
|
|
380
|
+
record.updatedBy = updatedBy;
|
|
381
|
+
if (row.deleted_at != null)
|
|
382
|
+
record.deletedAt = fromMs(row.deleted_at);
|
|
383
|
+
if (row.unlisted_at != null)
|
|
384
|
+
record.unlistedAt = fromMs(row.unlisted_at);
|
|
385
|
+
if (associations.length)
|
|
386
|
+
record.associations = associations;
|
|
387
|
+
if (permissions.length)
|
|
388
|
+
record.permissions = permissions;
|
|
389
|
+
return record;
|
|
390
|
+
};
|
|
391
|
+
var rowToAssociation = (row) => {
|
|
392
|
+
if (row.kind === "tag") {
|
|
393
|
+
return { kind: "tag", label: row.label };
|
|
394
|
+
}
|
|
395
|
+
if (row.kind === "attachment") {
|
|
396
|
+
const attachmentRecordId = row.attachment_record_id;
|
|
397
|
+
return {
|
|
398
|
+
kind: "attachment",
|
|
399
|
+
label: row.label,
|
|
400
|
+
fileId: row.file_id,
|
|
401
|
+
// Stored as '' when absent, like every other optional column here —
|
|
402
|
+
// the field is omitted rather than served empty, so a round trip
|
|
403
|
+
// produces the association the caller wrote.
|
|
404
|
+
...attachmentRecordId && { attachmentRecordId }
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
kind: "relationship",
|
|
409
|
+
label: row.label,
|
|
410
|
+
target: rowToTarget(row)
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
var associationKeyColumns = (a) => {
|
|
414
|
+
if (a.kind === "attachment")
|
|
415
|
+
return [a.fileId, "", "", "", ""];
|
|
416
|
+
if (a.kind !== "relationship")
|
|
417
|
+
return ["", "", "", "", ""];
|
|
418
|
+
const t = a.target;
|
|
419
|
+
if (t.kind === "entity")
|
|
420
|
+
return ["", "entity", t.entityId, "", ""];
|
|
421
|
+
if (t.kind === "external")
|
|
422
|
+
return ["", "external", t.id, t.ns, ""];
|
|
423
|
+
return ["", "record", t.recordId, "", t.stackUrl ?? ""];
|
|
424
|
+
};
|
|
425
|
+
var permissionKeyColumns = (p) => {
|
|
426
|
+
if (p.kind === "anyone")
|
|
427
|
+
return ["", "", ""];
|
|
428
|
+
const g = p.grantee;
|
|
429
|
+
return g.kind === "entity" ? ["entity", g.entityId, ""] : ["group", g.groupId, g.role];
|
|
430
|
+
};
|
|
431
|
+
var rowToPermission = (row) => {
|
|
432
|
+
if (row.kind === "anyone")
|
|
433
|
+
return { kind: "anyone", label: "read" };
|
|
434
|
+
const id = row.grantee_id;
|
|
435
|
+
return {
|
|
436
|
+
kind: "permission",
|
|
437
|
+
label: row.label,
|
|
438
|
+
grantee: row.grantee_kind === "group" ? { kind: "group", groupId: id, role: row.role } : { kind: "entity", entityId: id }
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
var rowToTarget = (row) => {
|
|
442
|
+
const id = row.related_id;
|
|
443
|
+
if (row.related_scope === "entity")
|
|
444
|
+
return { kind: "entity", entityId: id };
|
|
445
|
+
if (row.related_scope === "external") {
|
|
446
|
+
return { kind: "external", ns: row.related_ns, id };
|
|
447
|
+
}
|
|
448
|
+
const stackUrl = row.related_stack;
|
|
449
|
+
return { kind: "record", recordId: id, ...stackUrl && { stackUrl } };
|
|
450
|
+
};
|
|
451
|
+
var rowToType = (row) => {
|
|
452
|
+
const t = {
|
|
453
|
+
id: row.id,
|
|
454
|
+
baseId: row.base_id,
|
|
455
|
+
version: row.version,
|
|
456
|
+
name: row.name,
|
|
457
|
+
schema: JSON.parse(row.schema),
|
|
458
|
+
schemaHash: row.schema_hash,
|
|
459
|
+
createdAt: fromMs(row.created_at)
|
|
460
|
+
};
|
|
461
|
+
if (row.migrates_from)
|
|
462
|
+
t.migratesFrom = row.migrates_from;
|
|
463
|
+
return t;
|
|
464
|
+
};
|
|
465
|
+
var rowToVersion = (row) => {
|
|
466
|
+
const v = {
|
|
467
|
+
version: row.version,
|
|
468
|
+
typeId: row.type_id,
|
|
469
|
+
content: JSON.parse(row.content),
|
|
470
|
+
updatedAt: fromMs(row.updated_at)
|
|
471
|
+
};
|
|
472
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
473
|
+
if (createdBy)
|
|
474
|
+
v.createdBy = createdBy;
|
|
475
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
476
|
+
if (updatedBy)
|
|
477
|
+
v.updatedBy = updatedBy;
|
|
478
|
+
return v;
|
|
479
|
+
};
|
|
480
|
+
var rowToJournalEntry = (row) => {
|
|
481
|
+
const e = {
|
|
482
|
+
seq: row.seq,
|
|
483
|
+
at: fromMs(row.at),
|
|
484
|
+
kind: row.kind,
|
|
485
|
+
ops: JSON.parse(row.ops),
|
|
486
|
+
version: row.version,
|
|
487
|
+
typeId: row.type_id
|
|
488
|
+
};
|
|
489
|
+
if (row.parent_id != null)
|
|
490
|
+
e.parentId = row.parent_id;
|
|
491
|
+
if (row.previous_parent_id != null) {
|
|
492
|
+
e.previousParentId = row.previous_parent_id === "" ? null : row.previous_parent_id;
|
|
493
|
+
}
|
|
494
|
+
if (row.actor != null)
|
|
495
|
+
e.actor = JSON.parse(row.actor);
|
|
496
|
+
if (row.associations != null)
|
|
497
|
+
e.associations = JSON.parse(row.associations);
|
|
498
|
+
return e;
|
|
499
|
+
};
|
|
500
|
+
|
|
350
501
|
// ../sqlite-shared/dist/query.js
|
|
351
|
-
var atBudget = (page,
|
|
502
|
+
var atBudget = (page, rows2) => ({
|
|
352
503
|
sql: page.sql,
|
|
353
|
-
params: [...page.params,
|
|
504
|
+
params: [...page.params, rows2]
|
|
354
505
|
});
|
|
355
|
-
var sqlDirection = (query) =>
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
506
|
+
var sqlDirection = (query) => query.sort.direction === "asc" ? "ASC" : "DESC";
|
|
507
|
+
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`;
|
|
508
|
+
var members = (alias) => `CASE WHEN ${alias}.t = 'object' THEN ${alias}.v ELSE '{}' END`;
|
|
509
|
+
var emptyStatement = () => ({
|
|
510
|
+
tables: [],
|
|
511
|
+
tableParams: [],
|
|
512
|
+
conditions: ["r.id != '_config'"],
|
|
513
|
+
params: []
|
|
514
|
+
});
|
|
515
|
+
var rows = (s, columns, tuples) => {
|
|
516
|
+
const name = `t${s.tables.length}`;
|
|
517
|
+
s.tables.push(`${name} AS MATERIALIZED ${rowsTable(columns)}`);
|
|
518
|
+
s.tableParams.push(JSON.stringify(tuples));
|
|
519
|
+
return name;
|
|
361
520
|
};
|
|
362
|
-
var
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
key = nextAlias();
|
|
372
|
-
from.push(`json_each(${members(value)}) AS ${key}`);
|
|
373
|
-
value = nextAlias();
|
|
374
|
-
from.push(`json_each(${spread(key)}) AS ${value}`);
|
|
375
|
-
where.push(`${key}.key = ?`);
|
|
376
|
-
params.push(segment);
|
|
377
|
-
}
|
|
378
|
-
where.push(leaf(value));
|
|
379
|
-
return `EXISTS (SELECT 1 FROM ${from.join(", ")} WHERE ${where.join(" AND ")})`;
|
|
521
|
+
var rowsTable = (columns) => {
|
|
522
|
+
const named = columns.map((column, i) => `json_extract(value, '$[${i}]') AS ${column}`);
|
|
523
|
+
return `(SELECT ${named.join(", ")} FROM json_each(?))`;
|
|
524
|
+
};
|
|
525
|
+
var CONDITION_COLUMNS = ["scope", "path", "op", "val", "kind"];
|
|
526
|
+
var kindOf = (type) => `CASE ${type} WHEN 'integer' THEN 'number' WHEN 'real' THEN 'number' ELSE ${type} END`;
|
|
527
|
+
var holds = (c, r) => {
|
|
528
|
+
const depth = `json_array_length(${c}.path)`;
|
|
529
|
+
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})`;
|
|
380
530
|
};
|
|
381
|
-
var equalsLeaf = (alias) => `${alias}.type NOT IN ('object', 'array') AND ${alias}.value = ?`;
|
|
382
531
|
var SORT_ALIAS = "cs";
|
|
383
532
|
var cursorMismatch = (cursor, field) => new StackBadRequestError(`Cursor sort field "${cursor.field}" does not match query sort field "${field}"`);
|
|
384
533
|
var nativeCursor = (cursor, field) => {
|
|
@@ -391,37 +540,77 @@ var contentCursor = (cursor, field) => {
|
|
|
391
540
|
throw cursorMismatch(cursor, field);
|
|
392
541
|
return cursor;
|
|
393
542
|
};
|
|
394
|
-
var
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
543
|
+
var valueKind = (value) => {
|
|
544
|
+
if (typeof value === "boolean")
|
|
545
|
+
return `${value}`;
|
|
546
|
+
if (typeof value === "number")
|
|
547
|
+
return "number";
|
|
548
|
+
return typeof value === "string" ? "text" : null;
|
|
549
|
+
};
|
|
550
|
+
var contentRows = (scope, predicate) => {
|
|
551
|
+
const row = (key, op, value = null) => {
|
|
552
|
+
const path = parseContentFilterKey(key);
|
|
553
|
+
return [scope, path, op, value, valueKind(value)];
|
|
554
|
+
};
|
|
555
|
+
return [
|
|
556
|
+
...Object.entries(predicate.content ?? {}).map(([key, value]) => (
|
|
557
|
+
// undefined is the same absence null names — JSON drops it on the way
|
|
558
|
+
// over the wire, so treating it as a value would make an in-process
|
|
559
|
+
// query and its wire equivalent disagree.
|
|
560
|
+
value === null || value === void 0 ? row(key, "isNull") : row(key, "equals", value)
|
|
561
|
+
)),
|
|
562
|
+
...(predicate.contentPresent ?? []).map((key) => row(key, "present"))
|
|
563
|
+
];
|
|
564
|
+
};
|
|
565
|
+
var inList = (s, column, values) => {
|
|
566
|
+
if (values.length === 1) {
|
|
567
|
+
s.params.push(values[0]);
|
|
568
|
+
return `${column} = ?`;
|
|
407
569
|
}
|
|
408
|
-
|
|
409
|
-
|
|
570
|
+
const table = rows(s, ["value"], values.map((value) => [value]));
|
|
571
|
+
return `${column} IN (SELECT value FROM ${table})`;
|
|
572
|
+
};
|
|
573
|
+
var familyRange = (baseId) => [`${baseId}@`, `${baseId}A`];
|
|
574
|
+
var withinFamily = (column, lo, hi) => `(${column} >= ${lo} AND ${column} < ${hi})`;
|
|
575
|
+
var inFamily = (column, baseId) => ({
|
|
576
|
+
sql: withinFamily(column, "?", "?"),
|
|
577
|
+
params: familyRange(baseId)
|
|
578
|
+
});
|
|
579
|
+
var inFamilies = (s, record, baseIds) => {
|
|
580
|
+
if (baseIds.length === 1) {
|
|
581
|
+
const one = inFamily(`${record}.type_id`, baseIds[0]);
|
|
582
|
+
s.params.push(...one.params);
|
|
583
|
+
return one.sql;
|
|
584
|
+
}
|
|
585
|
+
const table = rows(s, ["lo", "hi"], baseIds.map(familyRange));
|
|
586
|
+
return `${record}.id IN (SELECT fr.id FROM ${table} f JOIN records fr ON ${withinFamily("fr.type_id", "f.lo", "f.hi")})`;
|
|
587
|
+
};
|
|
588
|
+
var carriesPermissions = (s, sets) => {
|
|
589
|
+
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)])));
|
|
590
|
+
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))`;
|
|
591
|
+
};
|
|
592
|
+
var reachConditions = (s, reach) => {
|
|
593
|
+
if (reach.fence)
|
|
594
|
+
s.conditions.push(inFamilies(s, "r", reach.fence));
|
|
595
|
+
const within = reach.within;
|
|
596
|
+
if (within) {
|
|
597
|
+
const arms = [inFamilies(s, "r", within.baseIds)];
|
|
598
|
+
if (within.authored) {
|
|
599
|
+
s.params.push(within.authored.subjectId);
|
|
600
|
+
arms.push(`(r.entity_id = ? AND ${inFamilies(s, "r", within.authored.baseIds)})`);
|
|
601
|
+
}
|
|
602
|
+
arms.push(carriesPermissions(s, within.permissions));
|
|
603
|
+
s.conditions.push(`(${arms.join(" OR ")})`);
|
|
410
604
|
}
|
|
411
|
-
return conditions;
|
|
412
605
|
};
|
|
413
|
-
var recordConditions = (query) => {
|
|
414
|
-
const
|
|
415
|
-
const params =
|
|
606
|
+
var recordConditions = (query, reach) => {
|
|
607
|
+
const s = emptyStatement();
|
|
608
|
+
const { conditions, params } = s;
|
|
416
609
|
const f = query.filter ?? {};
|
|
417
|
-
let aliasSeq = 0;
|
|
418
|
-
const nextContentAlias = () => `cp${aliasSeq++}`;
|
|
419
610
|
const inFilter = (column, value) => {
|
|
420
611
|
if (value === void 0)
|
|
421
612
|
return;
|
|
422
|
-
|
|
423
|
-
conditions.push(`r.${column} IN (${values.map(() => "?").join(",")})`);
|
|
424
|
-
params.push(...values);
|
|
613
|
+
conditions.push(inList(s, `r.${column}`, Array.isArray(value) ? value : [value]));
|
|
425
614
|
};
|
|
426
615
|
const rangeFilter = (column, range) => {
|
|
427
616
|
if (range?.after) {
|
|
@@ -439,7 +628,12 @@ var recordConditions = (query) => {
|
|
|
439
628
|
if (!f.includeUnlisted) {
|
|
440
629
|
conditions.push("r.unlisted_at IS NULL");
|
|
441
630
|
}
|
|
631
|
+
if (reach)
|
|
632
|
+
reachConditions(s, reach);
|
|
442
633
|
inFilter("type_id", f.typeId);
|
|
634
|
+
if (f.baseId !== void 0) {
|
|
635
|
+
conditions.push(inFamilies(s, "r", Array.isArray(f.baseId) ? f.baseId : [f.baseId]));
|
|
636
|
+
}
|
|
443
637
|
if (f.parentId !== void 0) {
|
|
444
638
|
if (f.parentId === null) {
|
|
445
639
|
conditions.push("r.parent_id IS NULL");
|
|
@@ -455,10 +649,10 @@ var recordConditions = (query) => {
|
|
|
455
649
|
rangeFilter("updated_at", f.updatedAt);
|
|
456
650
|
const hasAssociation = (kind, clauses) => `r.id IN (SELECT a.record_id FROM associations a WHERE a.kind = '${kind}'` + clauses.map((c) => ` AND ${c}`).join("") + `)`;
|
|
457
651
|
if (f.tags?.length) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
652
|
+
const tags = [...new Set(f.tags)];
|
|
653
|
+
const label = inList(s, "a.label", tags);
|
|
654
|
+
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) = ?)`);
|
|
655
|
+
params.push(tags.length);
|
|
462
656
|
}
|
|
463
657
|
if (f.attachment) {
|
|
464
658
|
const clauses = [];
|
|
@@ -504,15 +698,17 @@ var recordConditions = (query) => {
|
|
|
504
698
|
}
|
|
505
699
|
conditions.push(hasAssociation("relationship", clauses));
|
|
506
700
|
}
|
|
507
|
-
|
|
508
|
-
conditions.push(...contentConditions(f, params, nextContentAlias));
|
|
509
|
-
}
|
|
701
|
+
const content = contentRows(null, f);
|
|
510
702
|
if (f.contentByType) {
|
|
511
|
-
const arms = Object.entries(f.contentByType)
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
703
|
+
const arms = Object.entries(f.contentByType);
|
|
704
|
+
const typeIds = arms.map(([typeId]) => typeId);
|
|
705
|
+
conditions.push(inList(s, "r.type_id", typeIds));
|
|
706
|
+
for (const [typeId, arm] of arms)
|
|
707
|
+
content.push(...contentRows(typeId, arm));
|
|
708
|
+
}
|
|
709
|
+
if (content.length > 0) {
|
|
710
|
+
const table = rows(s, CONDITION_COLUMNS, content);
|
|
711
|
+
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")})`);
|
|
516
712
|
}
|
|
517
713
|
if (f.search) {
|
|
518
714
|
const sanitized = sanitizeFts5Query(f.search);
|
|
@@ -523,7 +719,7 @@ var recordConditions = (query) => {
|
|
|
523
719
|
conditions.push("0");
|
|
524
720
|
}
|
|
525
721
|
}
|
|
526
|
-
return
|
|
722
|
+
return s;
|
|
527
723
|
};
|
|
528
724
|
var presentCursorCondition = (cursor, op, params) => {
|
|
529
725
|
if (cursor.kind === "num") {
|
|
@@ -534,56 +730,54 @@ var presentCursorCondition = (cursor, op, params) => {
|
|
|
534
730
|
params.push(key, key, cursor.value, cursor.value, cursor.id);
|
|
535
731
|
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} ?))))))`;
|
|
536
732
|
};
|
|
537
|
-
var statement = (
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
733
|
+
var statement = (s, from, tail) => {
|
|
734
|
+
const select = `SELECT r.* FROM ${from} WHERE ${s.conditions.join(" AND ")}${tail}`;
|
|
735
|
+
const sql = s.tables.length === 0 ? select : `WITH ${s.tables.join(", ")} ${select}`;
|
|
736
|
+
return { sql, params: [...s.tableParams, ...s.params] };
|
|
737
|
+
};
|
|
738
|
+
var buildQueryPlan = (query, reach) => {
|
|
542
739
|
const dir = sqlDirection(query);
|
|
543
740
|
const op = dir === "ASC" ? ">" : "<";
|
|
544
741
|
const decoded = query.cursor ? decodeCursor(query.cursor) : null;
|
|
545
|
-
if (query.sort
|
|
546
|
-
return byTypePlan(query, query.sort.contentFieldByType, dir, op, decoded);
|
|
742
|
+
if (query.sort.contentFieldByType) {
|
|
743
|
+
return byTypePlan(query, reach, query.sort.contentFieldByType, dir, op, decoded);
|
|
547
744
|
}
|
|
548
|
-
const contentField = query.sort
|
|
549
|
-
if (
|
|
550
|
-
const
|
|
551
|
-
const field = getSortField(query);
|
|
745
|
+
const { contentField, field } = query.sort;
|
|
746
|
+
if (field !== void 0) {
|
|
747
|
+
const where = recordConditions(query, reach);
|
|
552
748
|
const col = getSortColumn(field);
|
|
553
749
|
if (decoded) {
|
|
554
750
|
const cursor2 = nativeCursor(decoded, field);
|
|
555
|
-
conditions.push(`(r.${col} ${op} ? OR (r.${col} = ? AND r.id ${op} ?))`);
|
|
556
|
-
params.push(cursor2.value, cursor2.value, cursor2.id);
|
|
751
|
+
where.conditions.push(`(r.${col} ${op} ? OR (r.${col} = ? AND r.id ${op} ?))`);
|
|
752
|
+
where.params.push(cursor2.value, cursor2.value, cursor2.id);
|
|
557
753
|
}
|
|
558
|
-
return [
|
|
559
|
-
statement("r.*", "records r", conditions, params, ` ORDER BY r.${col} ${dir}, r.id ${dir} LIMIT ?`)
|
|
560
|
-
];
|
|
754
|
+
return [statement(where, "records r", ` ORDER BY r.${col} ${dir}, r.id ${dir} LIMIT ?`)];
|
|
561
755
|
}
|
|
562
756
|
const cursor = decoded ? contentCursor(decoded, contentField) : null;
|
|
563
757
|
const pages = [];
|
|
564
758
|
if (cursor?.kind !== "absent") {
|
|
565
|
-
const
|
|
759
|
+
const where = recordConditions(query, reach);
|
|
566
760
|
const from = `content_index ${SORT_ALIAS} JOIN records r ON r.id = ${SORT_ALIAS}.record_id`;
|
|
567
|
-
conditions.push(`${SORT_ALIAS}.field = ?`);
|
|
568
|
-
params.push(contentField);
|
|
761
|
+
where.conditions.push(`${SORT_ALIAS}.field = ?`);
|
|
762
|
+
where.params.push(contentField);
|
|
569
763
|
if (cursor)
|
|
570
|
-
conditions.push(presentCursorCondition(cursor, op, params));
|
|
764
|
+
where.conditions.push(presentCursorCondition(cursor, op, where.params));
|
|
571
765
|
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}`;
|
|
572
|
-
pages.push(statement(
|
|
766
|
+
pages.push(statement(where, from, ` ORDER BY ${order} LIMIT ?`));
|
|
573
767
|
}
|
|
574
768
|
{
|
|
575
|
-
const
|
|
576
|
-
conditions.push(`NOT EXISTS (SELECT 1 FROM content_index ${SORT_ALIAS} WHERE ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = ?)`);
|
|
577
|
-
params.push(contentField);
|
|
769
|
+
const where = recordConditions(query, reach);
|
|
770
|
+
where.conditions.push(`NOT EXISTS (SELECT 1 FROM content_index ${SORT_ALIAS} WHERE ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = ?)`);
|
|
771
|
+
where.params.push(contentField);
|
|
578
772
|
if (cursor?.kind === "absent") {
|
|
579
|
-
conditions.push(`r.id ${op} ?`);
|
|
580
|
-
params.push(cursor.id);
|
|
773
|
+
where.conditions.push(`r.id ${op} ?`);
|
|
774
|
+
where.params.push(cursor.id);
|
|
581
775
|
}
|
|
582
|
-
pages.push(statement(
|
|
776
|
+
pages.push(statement(where, "records r", ` ORDER BY r.id ${dir} LIMIT ?`));
|
|
583
777
|
}
|
|
584
778
|
return pages;
|
|
585
779
|
};
|
|
586
|
-
var byTypePlan = (query, byType, dir, op, decoded) => {
|
|
780
|
+
var byTypePlan = (query, reach, byType, dir, op, decoded) => {
|
|
587
781
|
const cursor = decoded ? contentCursor(decoded, byTypeDescriptor(byType)) : null;
|
|
588
782
|
const fields = [];
|
|
589
783
|
const constants = [];
|
|
@@ -597,42 +791,38 @@ var byTypePlan = (query, byType, dir, op, decoded) => {
|
|
|
597
791
|
constants.push([typeId, entry]);
|
|
598
792
|
}
|
|
599
793
|
const pages = [];
|
|
600
|
-
const fieldOf = (alias, record) => fields.map(() => `(${record}.type_id = ? AND ${alias}.field = ?)`).join(" OR ");
|
|
601
794
|
if (cursor?.kind !== "absent" && fields.length + constants.length > 0) {
|
|
795
|
+
const where = recordConditions(query, reach);
|
|
602
796
|
const sources = [];
|
|
603
|
-
const sourceParams = [];
|
|
604
797
|
if (fields.length > 0) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
sourceParams.push(typeId, field);
|
|
798
|
+
const byField = rows(where, ["type_id", "field"], fields);
|
|
799
|
+
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`);
|
|
608
800
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
801
|
+
if (constants.length > 0) {
|
|
802
|
+
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]));
|
|
803
|
+
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`);
|
|
612
804
|
}
|
|
613
|
-
const { conditions, params } = recordConditions(query);
|
|
614
805
|
if (cursor)
|
|
615
|
-
conditions.push(presentCursorCondition(cursor, op, params));
|
|
806
|
+
where.conditions.push(presentCursorCondition(cursor, op, where.params));
|
|
616
807
|
const from = `(${sources.join(" UNION ALL ")}) ${SORT_ALIAS} JOIN records r ON r.id = ${SORT_ALIAS}.record_id`;
|
|
617
808
|
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}`;
|
|
618
|
-
pages.push(statement(
|
|
809
|
+
pages.push(statement(where, from, ` ORDER BY ${order} LIMIT ?`));
|
|
619
810
|
}
|
|
620
811
|
{
|
|
621
|
-
const
|
|
812
|
+
const where = recordConditions(query, reach);
|
|
622
813
|
if (fields.length > 0) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
params.push(typeId, field);
|
|
814
|
+
const byField = rows(where, ["type_id", "field"], fields);
|
|
815
|
+
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)`);
|
|
626
816
|
}
|
|
627
817
|
if (constants.length > 0) {
|
|
628
|
-
|
|
629
|
-
|
|
818
|
+
const typeIds = constants.map(([typeId]) => typeId);
|
|
819
|
+
where.conditions.push(`NOT (${inList(where, "r.type_id", typeIds)})`);
|
|
630
820
|
}
|
|
631
821
|
if (cursor?.kind === "absent") {
|
|
632
|
-
conditions.push(`r.id ${op} ?`);
|
|
633
|
-
params.push(cursor.id);
|
|
822
|
+
where.conditions.push(`r.id ${op} ?`);
|
|
823
|
+
where.params.push(cursor.id);
|
|
634
824
|
}
|
|
635
|
-
pages.push(statement(
|
|
825
|
+
pages.push(statement(where, "records r", ` ORDER BY r.id ${dir} LIMIT ?`));
|
|
636
826
|
}
|
|
637
827
|
return pages;
|
|
638
828
|
};
|
|
@@ -725,154 +915,6 @@ var ContentIndex = class {
|
|
|
725
915
|
return fields;
|
|
726
916
|
}
|
|
727
917
|
};
|
|
728
|
-
var toMs = (d) => d.getTime();
|
|
729
|
-
var actorFromColumns = (subject, principal) => subject == null ? void 0 : {
|
|
730
|
-
subjectId: subject,
|
|
731
|
-
...principal != null && { principalId: principal }
|
|
732
|
-
};
|
|
733
|
-
var fromMs = (ms) => new Date(ms);
|
|
734
|
-
var rowToRecord = (row, associations) => {
|
|
735
|
-
const record = {
|
|
736
|
-
id: row.id,
|
|
737
|
-
typeId: row.type_id,
|
|
738
|
-
createdAt: fromMs(row.created_at),
|
|
739
|
-
updatedAt: fromMs(row.updated_at),
|
|
740
|
-
content: JSON.parse(row.content),
|
|
741
|
-
version: row.version
|
|
742
|
-
};
|
|
743
|
-
if (row.parent_id != null)
|
|
744
|
-
record.parentId = row.parent_id;
|
|
745
|
-
if (row.app_id != null)
|
|
746
|
-
record.appId = row.app_id;
|
|
747
|
-
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
748
|
-
if (createdBy)
|
|
749
|
-
record.createdBy = createdBy;
|
|
750
|
-
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
751
|
-
if (updatedBy)
|
|
752
|
-
record.updatedBy = updatedBy;
|
|
753
|
-
if (row.deleted_at != null)
|
|
754
|
-
record.deletedAt = fromMs(row.deleted_at);
|
|
755
|
-
if (row.unlisted_at != null)
|
|
756
|
-
record.unlistedAt = fromMs(row.unlisted_at);
|
|
757
|
-
const partitioned = partitionAssociations(associations);
|
|
758
|
-
if (partitioned.associations.length)
|
|
759
|
-
record.associations = partitioned.associations;
|
|
760
|
-
if (partitioned.permissions.length)
|
|
761
|
-
record.permissions = partitioned.permissions;
|
|
762
|
-
return record;
|
|
763
|
-
};
|
|
764
|
-
var rowToAssociation = (row) => {
|
|
765
|
-
if (row.kind === "tag") {
|
|
766
|
-
return { kind: "tag", label: row.label };
|
|
767
|
-
}
|
|
768
|
-
if (row.kind === "anyone") {
|
|
769
|
-
return { kind: "anyone", label: "read" };
|
|
770
|
-
}
|
|
771
|
-
if (row.kind === "permission") {
|
|
772
|
-
return {
|
|
773
|
-
kind: "permission",
|
|
774
|
-
label: row.label,
|
|
775
|
-
grantee: rowToGrantee(row)
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
if (row.kind === "attachment") {
|
|
779
|
-
const attachmentRecordId = row.attachment_record_id;
|
|
780
|
-
return {
|
|
781
|
-
kind: "attachment",
|
|
782
|
-
label: row.label,
|
|
783
|
-
fileId: row.file_id,
|
|
784
|
-
// Stored as '' when absent, like every other optional column here —
|
|
785
|
-
// the field is omitted rather than served empty, so a round trip
|
|
786
|
-
// produces the association the caller wrote.
|
|
787
|
-
...attachmentRecordId && { attachmentRecordId }
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
return {
|
|
791
|
-
kind: "relationship",
|
|
792
|
-
label: row.label,
|
|
793
|
-
target: rowToTarget(row)
|
|
794
|
-
};
|
|
795
|
-
};
|
|
796
|
-
var associationKeyColumns = (a) => {
|
|
797
|
-
if (a.kind === "attachment")
|
|
798
|
-
return [a.fileId, "", "", "", "", ""];
|
|
799
|
-
if (a.kind === "permission") {
|
|
800
|
-
const g = a.grantee;
|
|
801
|
-
return g.kind === "entity" ? ["", "entity", g.entityId, "", "", ""] : ["", "group", g.groupId, "", "", g.role];
|
|
802
|
-
}
|
|
803
|
-
if (a.kind !== "relationship")
|
|
804
|
-
return ["", "", "", "", "", ""];
|
|
805
|
-
const t = a.target;
|
|
806
|
-
if (t.kind === "entity")
|
|
807
|
-
return ["", "entity", t.entityId, "", "", ""];
|
|
808
|
-
if (t.kind === "external")
|
|
809
|
-
return ["", "external", t.id, t.ns, "", ""];
|
|
810
|
-
return ["", "record", t.recordId, "", t.stackUrl ?? "", ""];
|
|
811
|
-
};
|
|
812
|
-
var rowToGrantee = (row) => row.related_scope === "group" ? {
|
|
813
|
-
kind: "group",
|
|
814
|
-
groupId: row.related_id,
|
|
815
|
-
role: row.related_role
|
|
816
|
-
} : { kind: "entity", entityId: row.related_id };
|
|
817
|
-
var rowToTarget = (row) => {
|
|
818
|
-
const id = row.related_id;
|
|
819
|
-
if (row.related_scope === "entity")
|
|
820
|
-
return { kind: "entity", entityId: id };
|
|
821
|
-
if (row.related_scope === "external") {
|
|
822
|
-
return { kind: "external", ns: row.related_ns, id };
|
|
823
|
-
}
|
|
824
|
-
const stackUrl = row.related_stack;
|
|
825
|
-
return { kind: "record", recordId: id, ...stackUrl && { stackUrl } };
|
|
826
|
-
};
|
|
827
|
-
var rowToType = (row) => {
|
|
828
|
-
const t = {
|
|
829
|
-
id: row.id,
|
|
830
|
-
baseId: row.base_id,
|
|
831
|
-
version: row.version,
|
|
832
|
-
name: row.name,
|
|
833
|
-
schema: JSON.parse(row.schema),
|
|
834
|
-
schemaHash: row.schema_hash,
|
|
835
|
-
createdAt: fromMs(row.created_at)
|
|
836
|
-
};
|
|
837
|
-
if (row.migrates_from)
|
|
838
|
-
t.migratesFrom = row.migrates_from;
|
|
839
|
-
return t;
|
|
840
|
-
};
|
|
841
|
-
var rowToVersion = (row) => {
|
|
842
|
-
const v = {
|
|
843
|
-
version: row.version,
|
|
844
|
-
typeId: row.type_id,
|
|
845
|
-
content: JSON.parse(row.content),
|
|
846
|
-
updatedAt: fromMs(row.updated_at)
|
|
847
|
-
};
|
|
848
|
-
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
849
|
-
if (createdBy)
|
|
850
|
-
v.createdBy = createdBy;
|
|
851
|
-
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
852
|
-
if (updatedBy)
|
|
853
|
-
v.updatedBy = updatedBy;
|
|
854
|
-
return v;
|
|
855
|
-
};
|
|
856
|
-
var rowToJournalEntry = (row) => {
|
|
857
|
-
const e = {
|
|
858
|
-
seq: row.seq,
|
|
859
|
-
at: fromMs(row.at),
|
|
860
|
-
kind: row.kind,
|
|
861
|
-
ops: JSON.parse(row.ops),
|
|
862
|
-
version: row.version,
|
|
863
|
-
typeId: row.type_id
|
|
864
|
-
};
|
|
865
|
-
if (row.parent_id != null)
|
|
866
|
-
e.parentId = row.parent_id;
|
|
867
|
-
if (row.previous_parent_id != null) {
|
|
868
|
-
e.previousParentId = row.previous_parent_id === "" ? null : row.previous_parent_id;
|
|
869
|
-
}
|
|
870
|
-
if (row.actor != null)
|
|
871
|
-
e.actor = JSON.parse(row.actor);
|
|
872
|
-
if (row.associations != null)
|
|
873
|
-
e.associations = JSON.parse(row.associations);
|
|
874
|
-
return e;
|
|
875
|
-
};
|
|
876
918
|
|
|
877
919
|
// ../sqlite-shared/dist/record-logic.js
|
|
878
920
|
var asStackBadRequestError = (query, run) => {
|
|
@@ -904,6 +946,35 @@ var versionRowValues = (version) => [
|
|
|
904
946
|
version.updatedBy?.subjectId ?? null,
|
|
905
947
|
version.updatedBy?.principalId ?? null
|
|
906
948
|
];
|
|
949
|
+
var JOURNAL_COLUMNS = [
|
|
950
|
+
"record_id",
|
|
951
|
+
"seq",
|
|
952
|
+
"at",
|
|
953
|
+
"kind",
|
|
954
|
+
"ops",
|
|
955
|
+
"previous_parent_id",
|
|
956
|
+
"actor",
|
|
957
|
+
"associations",
|
|
958
|
+
"version",
|
|
959
|
+
"type_id",
|
|
960
|
+
"parent_id"
|
|
961
|
+
];
|
|
962
|
+
var INSERT_JOURNAL_SQL = `INSERT INTO journal (${JOURNAL_COLUMNS.join(", ")})
|
|
963
|
+
VALUES (${JOURNAL_COLUMNS.map(() => "?").join(", ")})`;
|
|
964
|
+
var journalValues = (recordId, seq, at, entry, row) => [
|
|
965
|
+
recordId,
|
|
966
|
+
seq,
|
|
967
|
+
toMs(at),
|
|
968
|
+
entry.kind,
|
|
969
|
+
JSON.stringify(entry.ops),
|
|
970
|
+
// '' is the root; NULL is an entry that names no origin at all.
|
|
971
|
+
entry.previousParentId === void 0 ? null : entry.previousParentId ?? "",
|
|
972
|
+
entry.actor ? JSON.stringify(entry.actor) : null,
|
|
973
|
+
entry.associations?.length ? JSON.stringify(entry.associations) : null,
|
|
974
|
+
row.version,
|
|
975
|
+
row.typeId,
|
|
976
|
+
row.parentId
|
|
977
|
+
];
|
|
907
978
|
var SharedSqlRecordLogic = class {
|
|
908
979
|
exec;
|
|
909
980
|
searchIndex;
|
|
@@ -931,6 +1002,18 @@ var SharedSqlRecordLogic = class {
|
|
|
931
1002
|
throw recordNotFound(id);
|
|
932
1003
|
throw versionConflict(id, expectedVersion, version);
|
|
933
1004
|
}
|
|
1005
|
+
/**
|
|
1006
|
+
* An aspect write's record, inside its transaction: missing is not found,
|
|
1007
|
+
* a tombstone is refused, and anything else answers its version.
|
|
1008
|
+
*/
|
|
1009
|
+
liveVersion(id) {
|
|
1010
|
+
const row = this.exec.get("SELECT version, deleted_at FROM records WHERE id = ?", [id]);
|
|
1011
|
+
if (!row)
|
|
1012
|
+
throw recordNotFound(id);
|
|
1013
|
+
if (row.deleted_at !== null)
|
|
1014
|
+
throw softDeletedRefusal(id);
|
|
1015
|
+
return row.version;
|
|
1016
|
+
}
|
|
934
1017
|
currentVersion(id) {
|
|
935
1018
|
return this.exec.get("SELECT version FROM records WHERE id = ?", [id])?.version;
|
|
936
1019
|
}
|
|
@@ -987,41 +1070,71 @@ var SharedSqlRecordLogic = class {
|
|
|
987
1070
|
*/
|
|
988
1071
|
async createRecord(record, opts = {}) {
|
|
989
1072
|
this.exec.transaction(() => {
|
|
990
|
-
|
|
991
|
-
|
|
1073
|
+
this.insertRecord(record);
|
|
1074
|
+
this.appendJournal(record.id, opts.journal);
|
|
1075
|
+
});
|
|
1076
|
+
return record;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* createRecord() carrying the record's history as recorded: its version
|
|
1080
|
+
* rows and journal entries, `seq` and `at` included, in the same
|
|
1081
|
+
* transaction. Appends no entry of its own. See
|
|
1082
|
+
* docs/spec/portability.md § Writing history.
|
|
1083
|
+
*/
|
|
1084
|
+
async importRecord(record, history) {
|
|
1085
|
+
this.exec.transaction(() => this.writeImported(record, history));
|
|
1086
|
+
return record;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* importRecord()'s write, for a caller already inside a transaction:
|
|
1090
|
+
* writeNewStore() writes a store's `_config` seed through it, so the one
|
|
1091
|
+
* Record a store starts with lands as every other Record does.
|
|
1092
|
+
*/
|
|
1093
|
+
writeImported(record, history) {
|
|
1094
|
+
this.insertRecord(record);
|
|
1095
|
+
for (const version of history.versions)
|
|
1096
|
+
this.insertVersionRow(record.id, version);
|
|
1097
|
+
for (const entry of history.journal)
|
|
1098
|
+
this.insertJournalRow(record.id, entry);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* The record row and every table describing it, for a caller already
|
|
1102
|
+
* inside a transaction. A duplicate id maps to StackConflictError.
|
|
1103
|
+
*/
|
|
1104
|
+
insertRecord(record) {
|
|
1105
|
+
try {
|
|
1106
|
+
this.exec.run(`INSERT INTO records
|
|
992
1107
|
(id, type_id, created_at, updated_at, content, version,
|
|
993
1108
|
parent_id, entity_id, app_id, principal_id, updated_by, updated_via,
|
|
994
1109
|
deleted_at, unlisted_at)
|
|
995
1110
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
}
|
|
1015
|
-
throw err;
|
|
1111
|
+
record.id,
|
|
1112
|
+
record.typeId,
|
|
1113
|
+
toMs(record.createdAt),
|
|
1114
|
+
toMs(record.updatedAt),
|
|
1115
|
+
JSON.stringify(record.content),
|
|
1116
|
+
record.version,
|
|
1117
|
+
record.parentId ?? null,
|
|
1118
|
+
record.createdBy?.subjectId ?? null,
|
|
1119
|
+
record.appId ?? null,
|
|
1120
|
+
record.createdBy?.principalId ?? null,
|
|
1121
|
+
record.updatedBy?.subjectId ?? null,
|
|
1122
|
+
record.updatedBy?.principalId ?? null,
|
|
1123
|
+
record.deletedAt ? toMs(record.deletedAt) : null,
|
|
1124
|
+
record.unlistedAt ? toMs(record.unlistedAt) : null
|
|
1125
|
+
]);
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
if (isUniqueConstraintViolation(err)) {
|
|
1128
|
+
throw new StackConflictError(`Record already exists: "${record.id}"`);
|
|
1016
1129
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
this.
|
|
1021
|
-
|
|
1022
|
-
this.
|
|
1023
|
-
|
|
1024
|
-
|
|
1130
|
+
throw err;
|
|
1131
|
+
}
|
|
1132
|
+
if (record.associations?.length)
|
|
1133
|
+
this.insertAssociations(record.id, record.associations);
|
|
1134
|
+
if (record.permissions?.length)
|
|
1135
|
+
this.insertPermissions(record.id, record.permissions);
|
|
1136
|
+
this.searchIndex.insert(record.id, JSON.stringify(record.content));
|
|
1137
|
+
this.contentIndex.sync(record.id, record.typeId, record.content);
|
|
1025
1138
|
}
|
|
1026
1139
|
async getRecord(id) {
|
|
1027
1140
|
return this.readRecord(id);
|
|
@@ -1034,56 +1147,55 @@ var SharedSqlRecordLogic = class {
|
|
|
1034
1147
|
*/
|
|
1035
1148
|
readRecord(id) {
|
|
1036
1149
|
const row = this.exec.get("SELECT * FROM records WHERE id = ?", [id]);
|
|
1037
|
-
return row ? this.hydrate(row) : null;
|
|
1150
|
+
return row ? this.hydrate([row])[0] : null;
|
|
1038
1151
|
}
|
|
1039
|
-
/**
|
|
1040
|
-
|
|
1041
|
-
|
|
1152
|
+
/**
|
|
1153
|
+
* Records rows with their associations and permissions attached: one
|
|
1154
|
+
* statement per element table for the whole list, so a page costs three
|
|
1155
|
+
* reads however many records it holds.
|
|
1156
|
+
*/
|
|
1157
|
+
hydrate(rows2) {
|
|
1158
|
+
const ids = rows2.map((row) => row.id);
|
|
1159
|
+
const associations = this.elementsByRecord("associations", ids, rowToAssociation);
|
|
1160
|
+
const permissions = this.elementsByRecord("permissions", ids, rowToPermission);
|
|
1161
|
+
return rows2.map((row) => rowToRecord(row, associations.get(row.id) ?? [], permissions.get(row.id) ?? []));
|
|
1042
1162
|
}
|
|
1043
1163
|
/**
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1164
|
+
* The patch applies to the content as it stands when the write lands,
|
|
1165
|
+
* read inside the same transaction.
|
|
1046
1166
|
*/
|
|
1047
|
-
async
|
|
1048
|
-
const now = toMs(/* @__PURE__ */ new Date());
|
|
1049
|
-
const sets = [];
|
|
1050
|
-
const values = [];
|
|
1051
|
-
if (changes.parentId !== void 0) {
|
|
1052
|
-
sets.push("parent_id = ?");
|
|
1053
|
-
values.push(changes.parentId);
|
|
1054
|
-
}
|
|
1055
|
-
if (changes.unlisted !== void 0) {
|
|
1056
|
-
sets.push("unlisted_at = ?");
|
|
1057
|
-
values.push(changes.unlisted ? now : null);
|
|
1058
|
-
}
|
|
1167
|
+
async patchContent(id, patch, opts = {}) {
|
|
1059
1168
|
this.exec.transaction(() => {
|
|
1060
1169
|
const current = this.readForWrite(id, opts.ifVersion);
|
|
1061
|
-
if (opts.bumpsVersion === false) {
|
|
1062
|
-
if (sets.length > 0) {
|
|
1063
|
-
this.exec.run(`UPDATE records SET ${sets.join(", ")} WHERE id = ?`, [...values, id]);
|
|
1064
|
-
}
|
|
1065
|
-
this.replaceAssociationHalves(id, changes);
|
|
1066
|
-
this.appendJournal(id, opts.journal);
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
const merged = changes.contentPatch ? applyMergePatch(current.content, changes.contentPatch) : void 0;
|
|
1070
|
-
if (merged !== void 0) {
|
|
1071
|
-
sets.unshift("content = ?");
|
|
1072
|
-
values.unshift(JSON.stringify(merged));
|
|
1073
|
-
}
|
|
1074
1170
|
if (opts.snapshot)
|
|
1075
1171
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1172
|
+
this.rewriteContent(id, current.typeId, applyMergePatch(current.content, patch), opts);
|
|
1173
|
+
this.appendJournal(id, opts.journal);
|
|
1174
|
+
});
|
|
1175
|
+
return this.reread(id, "patchContent");
|
|
1176
|
+
}
|
|
1177
|
+
async setParent(id, parentId, opts = {}) {
|
|
1178
|
+
return this.aspectUpdate(id, "parent_id = ?", parentId, opts, "setParent");
|
|
1179
|
+
}
|
|
1180
|
+
/** Stamped fresh: `Stack` hands this only a transition. */
|
|
1181
|
+
async setUnlisted(id, unlisted, opts = {}) {
|
|
1182
|
+
return this.aspectUpdate(id, "unlisted_at = ?", unlisted ? toMs(/* @__PURE__ */ new Date()) : null, opts, "setUnlisted");
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* One records column that never bumps. The tombstone refusal and the
|
|
1186
|
+
* `ifVersion` guard ride in the WHERE clause, as on a versioned update.
|
|
1187
|
+
*/
|
|
1188
|
+
aspectUpdate(id, set, value, opts, verb) {
|
|
1189
|
+
const guarded = opts.ifVersion !== void 0;
|
|
1190
|
+
this.exec.transaction(() => {
|
|
1191
|
+
const changed = this.exec.run(`UPDATE records SET ${set}
|
|
1192
|
+
WHERE id = ? AND deleted_at IS NULL${guarded ? " AND version = ?" : ""}`, [value, id, ...guarded ? [opts.ifVersion] : []]);
|
|
1193
|
+
if (changed === 0) {
|
|
1194
|
+
throw versionConflict(id, opts.ifVersion, this.liveVersion(id));
|
|
1083
1195
|
}
|
|
1084
1196
|
this.appendJournal(id, opts.journal);
|
|
1085
1197
|
});
|
|
1086
|
-
return this.reread(id,
|
|
1198
|
+
return this.reread(id, verb);
|
|
1087
1199
|
}
|
|
1088
1200
|
async deleteRecord(id, opts = {}) {
|
|
1089
1201
|
if (opts.purge) {
|
|
@@ -1113,6 +1225,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1113
1225
|
this.checkExpectedVersion(id, purged.version, ifVersion);
|
|
1114
1226
|
this.searchIndex.remove(id);
|
|
1115
1227
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
1228
|
+
this.exec.run("DELETE FROM permissions WHERE record_id = ?", [id]);
|
|
1116
1229
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
1117
1230
|
this.exec.run("DELETE FROM journal WHERE record_id = ?", [id]);
|
|
1118
1231
|
this.contentIndex.remove(id);
|
|
@@ -1163,20 +1276,27 @@ var SharedSqlRecordLogic = class {
|
|
|
1163
1276
|
this.searchIndex.insert(id, json);
|
|
1164
1277
|
this.contentIndex.sync(id, typeId, content);
|
|
1165
1278
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1279
|
+
queryRecords(query) {
|
|
1280
|
+
return this.page(query);
|
|
1281
|
+
}
|
|
1282
|
+
queryReadable(query, reach) {
|
|
1283
|
+
return this.page(query, reach);
|
|
1284
|
+
}
|
|
1285
|
+
/** `reach`, when given, is one more condition of every statement in the plan. */
|
|
1286
|
+
async page(query, reach) {
|
|
1287
|
+
const { limit } = query;
|
|
1168
1288
|
const fetch = limit + 1;
|
|
1169
|
-
const
|
|
1170
|
-
for (const page2 of buildQueryPlan(query)) {
|
|
1171
|
-
const budget = fetch -
|
|
1289
|
+
const rows2 = [];
|
|
1290
|
+
for (const page2 of buildQueryPlan(query, reach)) {
|
|
1291
|
+
const budget = fetch - rows2.length;
|
|
1172
1292
|
if (budget <= 0)
|
|
1173
1293
|
break;
|
|
1174
1294
|
const statement2 = atBudget(page2, budget);
|
|
1175
|
-
|
|
1295
|
+
rows2.push(...asStackBadRequestError(query, () => this.exec.all(statement2.sql, statement2.params)));
|
|
1176
1296
|
}
|
|
1177
|
-
const hasMore =
|
|
1178
|
-
const page = hasMore ?
|
|
1179
|
-
const records =
|
|
1297
|
+
const hasMore = rows2.length > limit;
|
|
1298
|
+
const page = hasMore ? rows2.slice(0, limit) : rows2;
|
|
1299
|
+
const records = this.hydrate(page);
|
|
1180
1300
|
const lastRecord = records[records.length - 1];
|
|
1181
1301
|
const cursor = hasMore && lastRecord ? makeCursor(lastRecord, query.sort, (record, field) => this.contentIndex.kindOf(record.typeId, field)) : null;
|
|
1182
1302
|
return { records, cursor };
|
|
@@ -1185,7 +1305,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1185
1305
|
* The reference check and the purges share one synchronous transaction,
|
|
1186
1306
|
* so nothing can add a reference between the two.
|
|
1187
1307
|
*/
|
|
1188
|
-
async deleteUnreferencedAttachmentRecords(fileId,
|
|
1308
|
+
async deleteUnreferencedAttachmentRecords(fileId, metadataBaseId) {
|
|
1189
1309
|
return this.exec.transaction(() => {
|
|
1190
1310
|
const referenced = this.exec.all(`SELECT 1 as found FROM associations WHERE kind = 'attachment' AND file_id = ?
|
|
1191
1311
|
UNION ALL
|
|
@@ -1194,11 +1314,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1194
1314
|
if (referenced.length) {
|
|
1195
1315
|
throw new StackConflictError("Attachment is still referenced by one or more records");
|
|
1196
1316
|
}
|
|
1197
|
-
|
|
1198
|
-
return [];
|
|
1199
|
-
const placeholders = metadataTypeIds.map(() => "?").join(", ");
|
|
1317
|
+
const family = inFamily("type_id", metadataBaseId);
|
|
1200
1318
|
const metaRows = this.exec.all(`SELECT id FROM records
|
|
1201
|
-
WHERE
|
|
1319
|
+
WHERE ${family.sql} AND json_extract(content, '$.fileId') = ?`, [...family.params, fileId]);
|
|
1202
1320
|
const deleted = [];
|
|
1203
1321
|
for (const row of metaRows) {
|
|
1204
1322
|
const purged = this.purgeRecord(row.id);
|
|
@@ -1218,8 +1336,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1218
1336
|
conditions.push("version < ?");
|
|
1219
1337
|
values.push(query.beforeVersion);
|
|
1220
1338
|
}
|
|
1221
|
-
const
|
|
1222
|
-
return
|
|
1339
|
+
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);
|
|
1340
|
+
return rows2.map(rowToVersion);
|
|
1223
1341
|
}
|
|
1224
1342
|
async getVersion(id, version) {
|
|
1225
1343
|
return this.readVersion(id, version);
|
|
@@ -1241,7 +1359,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1241
1359
|
VALUES (?, ?, ${VERSION_COLUMNS.map(() => "?").join(", ")})`, [id, version.version, ...versionRowValues(version)]);
|
|
1242
1360
|
} catch (err) {
|
|
1243
1361
|
if (isUniqueConstraintViolation(err)) {
|
|
1244
|
-
throw new StackConflictError(`Version ${version.version} already exists for record "${id}"
|
|
1362
|
+
throw new StackConflictError(`Version ${version.version} already exists for record "${id}"`);
|
|
1245
1363
|
}
|
|
1246
1364
|
throw err;
|
|
1247
1365
|
}
|
|
@@ -1257,14 +1375,6 @@ var SharedSqlRecordLogic = class {
|
|
|
1257
1375
|
SET ${VERSION_COLUMNS.map((c) => `${c} = ?`).join(", ")}
|
|
1258
1376
|
WHERE record_id = ? AND version = ?`, [...versionRowValues(version), id, version.version]);
|
|
1259
1377
|
}
|
|
1260
|
-
/**
|
|
1261
|
-
* Standalone snapshot write for tooling and tests — loud on any
|
|
1262
|
-
* collision, since nothing here is about to bump the version. Mutating
|
|
1263
|
-
* methods take a `snapshot` option instead; see snapshotBeforeMutation.
|
|
1264
|
-
*/
|
|
1265
|
-
async saveVersion(id, version) {
|
|
1266
|
-
this.insertVersionRow(id, version);
|
|
1267
|
-
}
|
|
1268
1378
|
/**
|
|
1269
1379
|
* The snapshot half of a mutating method's atomic snapshot-then-mutate
|
|
1270
1380
|
* transaction. A collision is a racing-writer conflict, except an orphan
|
|
@@ -1278,8 +1388,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1278
1388
|
} catch (err) {
|
|
1279
1389
|
if (!(err instanceof StackConflictError))
|
|
1280
1390
|
throw err;
|
|
1281
|
-
if (this.currentVersion(id) !== version.version)
|
|
1282
|
-
throw
|
|
1391
|
+
if (this.currentVersion(id) !== version.version) {
|
|
1392
|
+
throw new StackConflictError(`Version ${version.version} already exists for record "${id}" \u2014 a concurrent writer raced past this version. Use ifVersion to detect this before it happens.`);
|
|
1393
|
+
}
|
|
1283
1394
|
this.overwriteVersionRow(id, version);
|
|
1284
1395
|
}
|
|
1285
1396
|
}
|
|
@@ -1288,9 +1399,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1288
1399
|
// -------------------------------------------------------
|
|
1289
1400
|
/**
|
|
1290
1401
|
* Append one entry, stamped from the row as it now stands, so callers run
|
|
1291
|
-
* this inside their write's transaction, after the write. `seq` is
|
|
1292
|
-
*
|
|
1293
|
-
* to be free. See docs/spec/journal.md § Ordering.
|
|
1402
|
+
* this inside their write's transaction, after the write. `seq` is read
|
|
1403
|
+
* from the log's maximum in that same transaction, so no caller ever holds
|
|
1404
|
+
* one it expects to be free. See docs/spec/journal.md § Ordering.
|
|
1294
1405
|
*/
|
|
1295
1406
|
appendJournal(recordId, entry) {
|
|
1296
1407
|
if (!entry)
|
|
@@ -1298,26 +1409,20 @@ var SharedSqlRecordLogic = class {
|
|
|
1298
1409
|
const row = this.exec.get("SELECT version, type_id, parent_id FROM records WHERE id = ?", [recordId]);
|
|
1299
1410
|
if (!row)
|
|
1300
1411
|
throw recordNotFound(recordId);
|
|
1301
|
-
this.exec.
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
entry.
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
row.parent_id,
|
|
1316
|
-
// '' is the root; NULL is an entry that names no origin at all.
|
|
1317
|
-
entry.previousParentId === void 0 ? null : entry.previousParentId ?? "",
|
|
1318
|
-
entry.actor ? JSON.stringify(entry.actor) : null,
|
|
1319
|
-
entry.associations?.length ? JSON.stringify(entry.associations) : null
|
|
1320
|
-
]);
|
|
1412
|
+
const { seq } = this.exec.get("SELECT COALESCE(MAX(seq), 0) + 1 AS seq FROM journal WHERE record_id = ?", [recordId]);
|
|
1413
|
+
this.exec.run(INSERT_JOURNAL_SQL, journalValues(recordId, seq, /* @__PURE__ */ new Date(), entry, {
|
|
1414
|
+
version: row.version,
|
|
1415
|
+
typeId: row.type_id,
|
|
1416
|
+
parentId: row.parent_id
|
|
1417
|
+
}));
|
|
1418
|
+
}
|
|
1419
|
+
/** A whole entry as recorded, `seq` and `at` included — importRecord()'s half of the journal. */
|
|
1420
|
+
insertJournalRow(recordId, entry) {
|
|
1421
|
+
this.exec.run(INSERT_JOURNAL_SQL, journalValues(recordId, entry.seq, entry.at, entry, {
|
|
1422
|
+
version: entry.version,
|
|
1423
|
+
typeId: entry.typeId,
|
|
1424
|
+
parentId: entry.parentId ?? null
|
|
1425
|
+
}));
|
|
1321
1426
|
}
|
|
1322
1427
|
async getJournal(id, query = {}) {
|
|
1323
1428
|
if (!this.exec.get("SELECT 1 FROM records WHERE id = ?", [id])) {
|
|
@@ -1329,8 +1434,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1329
1434
|
conditions.push("seq > ?");
|
|
1330
1435
|
values.push(query.afterSeq);
|
|
1331
1436
|
}
|
|
1332
|
-
const
|
|
1333
|
-
return
|
|
1437
|
+
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);
|
|
1438
|
+
return rows2.map(rowToJournalEntry);
|
|
1334
1439
|
}
|
|
1335
1440
|
// -------------------------------------------------------
|
|
1336
1441
|
// Types
|
|
@@ -1355,66 +1460,71 @@ var SharedSqlRecordLogic = class {
|
|
|
1355
1460
|
return row ? rowToType(row) : null;
|
|
1356
1461
|
}
|
|
1357
1462
|
async listTypes() {
|
|
1358
|
-
const
|
|
1359
|
-
return
|
|
1463
|
+
const rows2 = this.exec.all("SELECT * FROM types ORDER BY base_id, version");
|
|
1464
|
+
return rows2.map(rowToType);
|
|
1360
1465
|
}
|
|
1361
1466
|
// -------------------------------------------------------
|
|
1362
1467
|
// Associations
|
|
1363
1468
|
// -------------------------------------------------------
|
|
1364
1469
|
/**
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1470
|
+
* A write to one of a record's two element tables, inside a transaction
|
|
1471
|
+
* that first checks the record exists, is not soft-deleted, is at
|
|
1472
|
+
* `ifVersion`, and that the table holds `expectSet`. Never bumps and
|
|
1473
|
+
* never snapshots.
|
|
1368
1474
|
*/
|
|
1369
|
-
|
|
1370
|
-
assertOneSurface(changes);
|
|
1475
|
+
elementWrite(id, table, opts, write, verb) {
|
|
1371
1476
|
this.exec.transaction(() => {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
this.exec.run(`DELETE FROM associations
|
|
1379
|
-
WHERE record_id = ?
|
|
1380
|
-
AND kind = ?
|
|
1381
|
-
AND label = ?
|
|
1382
|
-
AND file_id = ?
|
|
1383
|
-
AND related_scope = ?
|
|
1384
|
-
AND related_id = ?
|
|
1385
|
-
AND related_ns = ?
|
|
1386
|
-
AND related_stack = ?
|
|
1387
|
-
AND related_role = ?`, [recordId, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1477
|
+
this.checkExpectedVersion(id, this.liveVersion(id), opts.ifVersion);
|
|
1478
|
+
if (opts.expectSet) {
|
|
1479
|
+
const map = table === "associations" ? rowToAssociation : rowToPermission;
|
|
1480
|
+
const stored = this.elementsByRecord(table, [id], map).get(id) ?? [];
|
|
1481
|
+
if (!sameAssociationSet(stored, opts.expectSet))
|
|
1482
|
+
throw new ElementSetMovedError(id);
|
|
1388
1483
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
this.insertAssociations(recordId, adds);
|
|
1392
|
-
this.appendJournal(recordId, opts.journal);
|
|
1484
|
+
write();
|
|
1485
|
+
this.appendJournal(id, opts.journal);
|
|
1393
1486
|
});
|
|
1394
|
-
return this.reread(
|
|
1487
|
+
return this.reread(id, verb);
|
|
1395
1488
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1489
|
+
async editAssociations(id, edits, opts = {}) {
|
|
1490
|
+
return this.elementWrite(id, "associations", opts, () => {
|
|
1491
|
+
for (const edit of edits) {
|
|
1492
|
+
if (edit.op !== "remove")
|
|
1493
|
+
continue;
|
|
1494
|
+
const association = edit.association;
|
|
1495
|
+
this.exec.run(`DELETE FROM associations
|
|
1496
|
+
WHERE record_id = ? AND kind = ? AND label = ?
|
|
1497
|
+
AND file_id = ? AND related_scope = ? AND related_id = ?
|
|
1498
|
+
AND related_ns = ? AND related_stack = ?`, [id, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1499
|
+
}
|
|
1500
|
+
this.insertAssociations(id, addsOf(edits));
|
|
1501
|
+
}, "editAssociations");
|
|
1502
|
+
}
|
|
1503
|
+
async setAssociations(id, associations, opts = {}) {
|
|
1504
|
+
return this.elementWrite(id, "associations", opts, () => {
|
|
1505
|
+
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
1506
|
+
this.insertAssociations(id, associations);
|
|
1507
|
+
}, "setAssociations");
|
|
1508
|
+
}
|
|
1509
|
+
/** editAssociations() over the permissions table. */
|
|
1510
|
+
async editAccess(id, edits, opts = {}) {
|
|
1511
|
+
return this.elementWrite(id, "permissions", opts, () => {
|
|
1512
|
+
for (const edit of edits) {
|
|
1513
|
+
if (edit.op !== "remove")
|
|
1514
|
+
continue;
|
|
1515
|
+
const permission = edit.association;
|
|
1516
|
+
this.exec.run(`DELETE FROM permissions
|
|
1517
|
+
WHERE record_id = ? AND kind = ? AND label = ?
|
|
1518
|
+
AND grantee_kind = ? AND grantee_id = ? AND role = ?`, [id, permission.kind, permission.label, ...permissionKeyColumns(permission)]);
|
|
1519
|
+
}
|
|
1520
|
+
this.insertPermissions(id, addsOf(edits));
|
|
1521
|
+
}, "editAccess");
|
|
1408
1522
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
replaceAssociations(recordId, associations, half) {
|
|
1415
|
-
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]);
|
|
1416
|
-
if (associations.length)
|
|
1417
|
-
this.insertAssociations(recordId, associations);
|
|
1523
|
+
async setAccess(id, permissions, opts = {}) {
|
|
1524
|
+
return this.elementWrite(id, "permissions", opts, () => {
|
|
1525
|
+
this.exec.run("DELETE FROM permissions WHERE record_id = ?", [id]);
|
|
1526
|
+
this.insertPermissions(id, permissions);
|
|
1527
|
+
}, "setAccess");
|
|
1418
1528
|
}
|
|
1419
1529
|
/**
|
|
1420
1530
|
* Upserts on association identity, so re-pointing `attachment_record_id`
|
|
@@ -1423,46 +1533,118 @@ var SharedSqlRecordLogic = class {
|
|
|
1423
1533
|
*/
|
|
1424
1534
|
insertAssociations(recordId, associations) {
|
|
1425
1535
|
for (const assoc of associations) {
|
|
1426
|
-
|
|
1427
|
-
this.exec.run(`INSERT INTO associations
|
|
1536
|
+
this.insertRow(recordId, () => this.exec.run(`INSERT INTO associations
|
|
1428
1537
|
(record_id, kind, label, file_id, related_scope, related_id, related_ns,
|
|
1429
|
-
related_stack,
|
|
1430
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
1538
|
+
related_stack, attachment_record_id)
|
|
1539
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1431
1540
|
ON CONFLICT DO UPDATE SET attachment_record_id = excluded.attachment_record_id`, [
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1541
|
+
recordId,
|
|
1542
|
+
assoc.kind,
|
|
1543
|
+
assoc.label,
|
|
1544
|
+
...associationKeyColumns(assoc),
|
|
1545
|
+
assoc.kind === "attachment" ? assoc.attachmentRecordId ?? "" : ""
|
|
1546
|
+
]));
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
/** Every column is in the key, so a repeated element is the same row. */
|
|
1550
|
+
insertPermissions(recordId, permissions) {
|
|
1551
|
+
for (const p of permissions) {
|
|
1552
|
+
this.insertRow(recordId, () => this.exec.run(`INSERT INTO permissions (record_id, kind, label, grantee_kind, grantee_id, role)
|
|
1553
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1554
|
+
ON CONFLICT DO NOTHING`, [recordId, p.kind, p.label, ...permissionKeyColumns(p)]));
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
insertRow(recordId, insert) {
|
|
1558
|
+
try {
|
|
1559
|
+
insert();
|
|
1560
|
+
} catch (err) {
|
|
1561
|
+
if (isForeignKeyViolation(err))
|
|
1562
|
+
throw recordNotFound(recordId);
|
|
1563
|
+
throw err;
|
|
1444
1564
|
}
|
|
1445
1565
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1566
|
+
/**
|
|
1567
|
+
* One element table's rows for every record in `ids`, in primary-key
|
|
1568
|
+
* order — the order a single record's read has always had.
|
|
1569
|
+
*/
|
|
1570
|
+
elementsByRecord(table, ids, map) {
|
|
1571
|
+
const byRecord = /* @__PURE__ */ new Map();
|
|
1572
|
+
const rows2 = this.exec.all(`WITH ids AS MATERIALIZED ${rowsTable(["id"])}
|
|
1573
|
+
SELECT * FROM ${table} WHERE record_id IN (SELECT id FROM ids)
|
|
1574
|
+
ORDER BY ${table === "associations" ? ASSOCIATION_KEY : PERMISSION_KEY}`, [JSON.stringify(ids.map((id) => [id]))]);
|
|
1575
|
+
for (const row of rows2) {
|
|
1576
|
+
const id = row.record_id;
|
|
1577
|
+
const list = byRecord.get(id);
|
|
1578
|
+
if (list)
|
|
1579
|
+
list.push(map(row));
|
|
1580
|
+
else
|
|
1581
|
+
byRecord.set(id, [map(row)]);
|
|
1582
|
+
}
|
|
1583
|
+
return byRecord;
|
|
1449
1584
|
}
|
|
1450
1585
|
};
|
|
1586
|
+
var ASSOCIATION_KEY = "record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack";
|
|
1587
|
+
var PERMISSION_KEY = "record_id, kind, label, grantee_kind, grantee_id, role";
|
|
1588
|
+
var addsOf = (edits) => edits.flatMap((e) => e.op === "add" ? [e.association] : []);
|
|
1589
|
+
|
|
1590
|
+
// ../sqlite-shared/dist/config.js
|
|
1591
|
+
var CONFIG_ROW_SQL = `SELECT content FROM records WHERE id = '_config'`;
|
|
1592
|
+
var missingStoreError = (where) => new Error(`Cannot open: no database found at ${where}. Pass create: 'ifMissing' or 'exclusive' to create one.`);
|
|
1593
|
+
var writeNewStore = (exec, seed) => {
|
|
1594
|
+
exec.transaction(() => {
|
|
1595
|
+
exec.exec(RECORD_SCHEMA_SQL);
|
|
1596
|
+
exec.exec(FTS5_SCHEMA_SQL);
|
|
1597
|
+
exec.run("INSERT INTO format (id, version) VALUES (1, ?)", [FORMAT_VERSION]);
|
|
1598
|
+
new SharedSqlRecordLogic({ exec }).writeImported(seed.config, seed.history);
|
|
1599
|
+
});
|
|
1600
|
+
return seed.config.content;
|
|
1601
|
+
};
|
|
1602
|
+
var readStoredConfig = (exec, where) => {
|
|
1603
|
+
const hasFormat = exec.get(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'format'`) !== void 0;
|
|
1604
|
+
if (!hasFormat)
|
|
1605
|
+
return null;
|
|
1606
|
+
const version = exec.get("SELECT version FROM format WHERE id = 1")?.version;
|
|
1607
|
+
if (version !== FORMAT_VERSION)
|
|
1608
|
+
throw new FormatMismatchError(FORMAT_VERSION, version, where);
|
|
1609
|
+
const { content } = exec.get(CONFIG_ROW_SQL);
|
|
1610
|
+
return JSON.parse(content);
|
|
1611
|
+
};
|
|
1612
|
+
var openStore = async (exec, opts) => {
|
|
1613
|
+
const stored = readStoredConfig(exec, opts.where);
|
|
1614
|
+
if (!stored && opts.mode === "never")
|
|
1615
|
+
throw missingStoreError(opts.where);
|
|
1616
|
+
if (stored && (opts.mode === "exclusive" || opts.seed)) {
|
|
1617
|
+
throw new Error(`Cannot create: database already exists at ${opts.where}. Pass create: 'never' or 'ifMissing' without a seed to open it.`);
|
|
1618
|
+
}
|
|
1619
|
+
applyConnectionPragmas(exec, { wal: opts.wal });
|
|
1620
|
+
if (stored) {
|
|
1621
|
+
const expected = typeof opts.ownerEntityId === "string" ? opts.ownerEntityId : void 0;
|
|
1622
|
+
if (expected !== void 0 && expected !== stored.ownerEntityId) {
|
|
1623
|
+
throw new OwnerMismatchError(expected, stored.ownerEntityId, opts.where);
|
|
1624
|
+
}
|
|
1625
|
+
return stored;
|
|
1626
|
+
}
|
|
1627
|
+
if (opts.seed)
|
|
1628
|
+
return writeNewStore(exec, opts.seed);
|
|
1629
|
+
const owner = typeof opts.ownerEntityId === "function" ? await opts.ownerEntityId() : opts.ownerEntityId;
|
|
1630
|
+
if (!owner) {
|
|
1631
|
+
throw new Error(`Cannot create: no ownerEntityId given for the new database at ${opts.where}.`);
|
|
1632
|
+
}
|
|
1633
|
+
return writeNewStore(exec, newStoreSeed({ ownerEntityId: owner }));
|
|
1634
|
+
};
|
|
1451
1635
|
|
|
1452
1636
|
// ../sqlite-shared/dist/record-adapter.js
|
|
1453
1637
|
var SQLITE_RECORD_CAPABILITIES = {
|
|
1454
1638
|
filter: {
|
|
1455
1639
|
content: "path",
|
|
1456
1640
|
contentPresent: true,
|
|
1457
|
-
|
|
1458
|
-
// docs/spec/adapters.md § Adapter capabilities.
|
|
1459
|
-
contentByType: 64,
|
|
1641
|
+
contentByType: true,
|
|
1460
1642
|
search: true
|
|
1461
1643
|
},
|
|
1462
1644
|
sort: {
|
|
1463
1645
|
fields: ["createdAt", "updatedAt", "version"],
|
|
1464
1646
|
contentField: true,
|
|
1465
|
-
contentFieldByType:
|
|
1647
|
+
contentFieldByType: true
|
|
1466
1648
|
},
|
|
1467
1649
|
limits: {
|
|
1468
1650
|
attachmentBytes: null,
|
|
@@ -1472,12 +1654,10 @@ var SQLITE_RECORD_CAPABILITIES = {
|
|
|
1472
1654
|
var SharedSqlRecordAdapter = class {
|
|
1473
1655
|
capabilities = SQLITE_RECORD_CAPABILITIES;
|
|
1474
1656
|
ownerEntityId;
|
|
1475
|
-
timezone;
|
|
1476
1657
|
record;
|
|
1477
1658
|
constructor(exec, config) {
|
|
1478
1659
|
this.record = new SharedSqlRecordLogic({ exec });
|
|
1479
|
-
this.ownerEntityId = config.
|
|
1480
|
-
this.timezone = config.timezone;
|
|
1660
|
+
this.ownerEntityId = config.ownerEntityId;
|
|
1481
1661
|
}
|
|
1482
1662
|
// -------------------------------------------------------
|
|
1483
1663
|
// Records
|
|
@@ -1485,11 +1665,20 @@ var SharedSqlRecordAdapter = class {
|
|
|
1485
1665
|
createRecord(record, opts) {
|
|
1486
1666
|
return this.record.createRecord(record, opts);
|
|
1487
1667
|
}
|
|
1668
|
+
importRecord(record, history) {
|
|
1669
|
+
return this.record.importRecord(record, history);
|
|
1670
|
+
}
|
|
1488
1671
|
getRecord(id) {
|
|
1489
1672
|
return this.record.getRecord(id);
|
|
1490
1673
|
}
|
|
1491
|
-
|
|
1492
|
-
return this.record.
|
|
1674
|
+
patchContent(id, patch, opts) {
|
|
1675
|
+
return this.record.patchContent(id, patch, opts);
|
|
1676
|
+
}
|
|
1677
|
+
setParent(id, parentId, opts) {
|
|
1678
|
+
return this.record.setParent(id, parentId, opts);
|
|
1679
|
+
}
|
|
1680
|
+
setUnlisted(id, unlisted, opts) {
|
|
1681
|
+
return this.record.setUnlisted(id, unlisted, opts);
|
|
1493
1682
|
}
|
|
1494
1683
|
deleteRecord(id, opts) {
|
|
1495
1684
|
return this.record.deleteRecord(id, opts);
|
|
@@ -1506,8 +1695,11 @@ var SharedSqlRecordAdapter = class {
|
|
|
1506
1695
|
queryRecords(query) {
|
|
1507
1696
|
return this.record.queryRecords(query);
|
|
1508
1697
|
}
|
|
1509
|
-
|
|
1510
|
-
return this.record.
|
|
1698
|
+
queryReadable(query, reach) {
|
|
1699
|
+
return this.record.queryReadable(query, reach);
|
|
1700
|
+
}
|
|
1701
|
+
deleteUnreferencedAttachmentRecords(fileId, metadataBaseId) {
|
|
1702
|
+
return this.record.deleteUnreferencedAttachmentRecords(fileId, metadataBaseId);
|
|
1511
1703
|
}
|
|
1512
1704
|
// -------------------------------------------------------
|
|
1513
1705
|
// Versions
|
|
@@ -1518,9 +1710,6 @@ var SharedSqlRecordAdapter = class {
|
|
|
1518
1710
|
getVersion(id, version) {
|
|
1519
1711
|
return this.record.getVersion(id, version);
|
|
1520
1712
|
}
|
|
1521
|
-
saveVersion(id, version) {
|
|
1522
|
-
return this.record.saveVersion(id, version);
|
|
1523
|
-
}
|
|
1524
1713
|
// -------------------------------------------------------
|
|
1525
1714
|
// Change journal
|
|
1526
1715
|
// -------------------------------------------------------
|
|
@@ -1540,10 +1729,19 @@ var SharedSqlRecordAdapter = class {
|
|
|
1540
1729
|
return this.record.listTypes();
|
|
1541
1730
|
}
|
|
1542
1731
|
// -------------------------------------------------------
|
|
1543
|
-
// Associations
|
|
1732
|
+
// Associations and permissions
|
|
1544
1733
|
// -------------------------------------------------------
|
|
1545
|
-
|
|
1546
|
-
return this.record.
|
|
1734
|
+
editAssociations(id, edits, opts) {
|
|
1735
|
+
return this.record.editAssociations(id, edits, opts);
|
|
1736
|
+
}
|
|
1737
|
+
setAssociations(id, associations, opts) {
|
|
1738
|
+
return this.record.setAssociations(id, associations, opts);
|
|
1739
|
+
}
|
|
1740
|
+
editAccess(id, edits, opts) {
|
|
1741
|
+
return this.record.editAccess(id, edits, opts);
|
|
1742
|
+
}
|
|
1743
|
+
setAccess(id, permissions, opts) {
|
|
1744
|
+
return this.record.setAccess(id, permissions, opts);
|
|
1547
1745
|
}
|
|
1548
1746
|
};
|
|
1549
1747
|
|
|
@@ -1564,8 +1762,8 @@ var DurableObjectSqliteExecutor = class {
|
|
|
1564
1762
|
return cursor.rowsWritten;
|
|
1565
1763
|
}
|
|
1566
1764
|
get(sql, params = []) {
|
|
1567
|
-
const
|
|
1568
|
-
return
|
|
1765
|
+
const rows2 = this.sql.exec(sql, ...params).toArray();
|
|
1766
|
+
return rows2[0];
|
|
1569
1767
|
}
|
|
1570
1768
|
all(sql, params = []) {
|
|
1571
1769
|
return this.sql.exec(sql, ...params).toArray();
|
|
@@ -1583,9 +1781,9 @@ var DoSQLiteRecordAdapter = class _DoSQLiteRecordAdapter extends SharedSqlRecord
|
|
|
1583
1781
|
/**
|
|
1584
1782
|
* Open the adapter for a DO instance, creating its storage on first use.
|
|
1585
1783
|
* A DO id either already has a config record (reattach) or it doesn't
|
|
1586
|
-
* (first call — opts.ownerEntityId
|
|
1587
|
-
* is no `create` option.
|
|
1588
|
-
*
|
|
1784
|
+
* (first call — opts.ownerEntityId becomes the config), so there
|
|
1785
|
+
* is no `create` option. A new store's tables are written in the same
|
|
1786
|
+
* transaction as its config record, so storage holds a whole store or none.
|
|
1589
1787
|
*
|
|
1590
1788
|
* DO SQLite manages its own durability and rejects PRAGMA journal_mode
|
|
1591
1789
|
* outright ("not authorized") — verified against the real runtime, not
|
|
@@ -1593,20 +1791,12 @@ var DoSQLiteRecordAdapter = class _DoSQLiteRecordAdapter extends SharedSqlRecord
|
|
|
1593
1791
|
*/
|
|
1594
1792
|
static async open(storage, opts) {
|
|
1595
1793
|
const exec = new DurableObjectSqliteExecutor(storage);
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
config.entityId,
|
|
1603
|
-
"the Durable Object storage"
|
|
1604
|
-
);
|
|
1605
|
-
}
|
|
1606
|
-
} else {
|
|
1607
|
-
const ownerEntityId = typeof opts.ownerEntityId === "function" ? await opts.ownerEntityId() : opts.ownerEntityId;
|
|
1608
|
-
config = insertConfigRecord(exec, ownerEntityId, opts.timezone);
|
|
1609
|
-
}
|
|
1794
|
+
const config = await openStore(exec, {
|
|
1795
|
+
...opts,
|
|
1796
|
+
mode: "ifMissing",
|
|
1797
|
+
where: "the Durable Object storage",
|
|
1798
|
+
wal: false
|
|
1799
|
+
});
|
|
1610
1800
|
return new _DoSQLiteRecordAdapter(exec, config);
|
|
1611
1801
|
}
|
|
1612
1802
|
// -------------------------------------------------------
|