@haverstack/record-adapter-sqlite 0.34.0 → 0.35.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 +13 -7
- package/dist/index.js +543 -427
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
2
|
-
import { StackConflictError, applyMergePatch, StackNotFoundError,
|
|
3
|
-
import { OwnerMismatchError,
|
|
2
|
+
import { StackConflictError, applyMergePatch, StackNotFoundError, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
3
|
+
import { OwnerMismatchError, softDeletedRefusal, sameAssociationSet, ElementSetMovedError, contentSortEntry, constantSortEntry, contentSortKey, parseContentFilterKey } from '@haverstack/core/adapter';
|
|
4
4
|
import { randomBytes, createHash } from 'crypto';
|
|
5
5
|
|
|
6
6
|
// src/node-sqlite.ts
|
|
@@ -27,34 +27,43 @@ var RECORD_SCHEMA_SQL = `
|
|
|
27
27
|
unlisted_at INTEGER
|
|
28
28
|
) STRICT;
|
|
29
29
|
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
-- over it, partitioned by kind.
|
|
34
|
-
-- See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
30
|
+
-- A record's data associations: tags, attachments and relationships.
|
|
31
|
+
-- Its permissions are in their own table below, so no predicate over this
|
|
32
|
+
-- one can match a grantee as a relationship target.
|
|
35
33
|
CREATE TABLE IF NOT EXISTS associations (
|
|
36
34
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
37
|
-
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship'
|
|
35
|
+
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship')),
|
|
38
36
|
label TEXT NOT NULL,
|
|
39
37
|
file_id TEXT NOT NULL DEFAULT '',
|
|
40
38
|
-- 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'
|
|
39
|
+
-- related_ns (a foreign namespace) or related_stack (another stack).
|
|
40
|
+
-- All are in the key, so targets differing only by a qualifier are
|
|
41
|
+
-- distinct associations.
|
|
42
|
+
related_scope TEXT NOT NULL DEFAULT '' CHECK (related_scope IN ('', 'record', 'entity', 'external')),
|
|
45
43
|
related_id TEXT NOT NULL DEFAULT '',
|
|
46
44
|
related_ns TEXT NOT NULL DEFAULT '',
|
|
47
45
|
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
46
|
-- Out of the key: it annotates a reference rather than naming one, so
|
|
52
47
|
-- re-pointing it updates the existing row. See docs/spec/attachments.md
|
|
53
48
|
-- \xA7 Naming the upload a reference came from.
|
|
54
49
|
attachment_record_id TEXT NOT NULL DEFAULT '',
|
|
55
50
|
-- Leads with record_id, so its implicit index serves every by-record
|
|
56
51
|
-- 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
|
|
52
|
+
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack)
|
|
53
|
+
) STRICT;
|
|
54
|
+
|
|
55
|
+
-- Who reaches a record, one row per element. An 'anyone' row names no
|
|
56
|
+
-- grantee; a group grantee's role is in the key, because member and
|
|
57
|
+
-- admin name two different sets of people.
|
|
58
|
+
-- See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
59
|
+
CREATE TABLE IF NOT EXISTS permissions (
|
|
60
|
+
record_id TEXT NOT NULL REFERENCES records(id),
|
|
61
|
+
kind TEXT NOT NULL CHECK (kind IN ('permission', 'anyone')),
|
|
62
|
+
label TEXT NOT NULL CHECK (label IN ('read', 'write')),
|
|
63
|
+
grantee_kind TEXT NOT NULL DEFAULT '' CHECK (grantee_kind IN ('', 'entity', 'group')),
|
|
64
|
+
grantee_id TEXT NOT NULL DEFAULT '',
|
|
65
|
+
role TEXT NOT NULL DEFAULT '' CHECK (role IN ('', 'member', 'admin')),
|
|
66
|
+
PRIMARY KEY (record_id, kind, label, grantee_kind, grantee_id, role)
|
|
58
67
|
) STRICT;
|
|
59
68
|
|
|
60
69
|
-- Content and the type it is read under. No parent_id, unlisted_at or
|
|
@@ -156,6 +165,11 @@ var RECORD_SCHEMA_SQL = `
|
|
|
156
165
|
CREATE INDEX IF NOT EXISTS idx_assoc_related ON associations(kind, related_scope, related_ns, related_id, record_id);
|
|
157
166
|
CREATE INDEX IF NOT EXISTS idx_types_base_id ON types(base_id, version);
|
|
158
167
|
|
|
168
|
+
-- The permissions key with record_id moved last, so a scoped query reads
|
|
169
|
+
-- the records carrying an element straight out of the index.
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_permissions_element
|
|
171
|
+
ON permissions(kind, label, grantee_kind, grantee_id, role, record_id);
|
|
172
|
+
|
|
159
173
|
-- The whole content ordering, in index order, for one field at a time:
|
|
160
174
|
-- a content-sorted page is an ordered walk of a slice of this index.
|
|
161
175
|
CREATE INDEX IF NOT EXISTS idx_content_index_sort
|
|
@@ -346,24 +360,22 @@ var decodeCursor = (cursor) => {
|
|
|
346
360
|
throw malformed();
|
|
347
361
|
};
|
|
348
362
|
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
363
|
var getSortColumn = (field) => field === "createdAt" ? "created_at" : field === "updatedAt" ? "updated_at" : "version";
|
|
351
|
-
var makeCursor = (record, sort,
|
|
352
|
-
const byType = sort
|
|
364
|
+
var makeCursor = (record, sort, kindOf2) => {
|
|
365
|
+
const byType = sort.contentFieldByType;
|
|
353
366
|
if (byType) {
|
|
354
|
-
const
|
|
367
|
+
const field2 = byTypeDescriptor(byType);
|
|
355
368
|
const key = Object.hasOwn(byType, record.typeId) ? byType[record.typeId] : void 0;
|
|
356
|
-
const kind2 = key?.field === void 0 ? void 0 :
|
|
369
|
+
const kind2 = key?.field === void 0 ? void 0 : kindOf2?.(record, key.field);
|
|
357
370
|
const entry2 = key?.field !== void 0 ? kind2 && contentSortEntry(kind2, record.content[key.field]) : key && constantSortEntry(key.value ?? null);
|
|
358
|
-
return encodeEntry(
|
|
371
|
+
return encodeEntry(field2, entry2 || null, record.id);
|
|
359
372
|
}
|
|
360
|
-
const contentField = sort
|
|
361
|
-
if (
|
|
362
|
-
const field = getSortField({ sort });
|
|
373
|
+
const { contentField, field } = sort;
|
|
374
|
+
if (field !== void 0) {
|
|
363
375
|
const value = field === "updatedAt" ? record.updatedAt.getTime() : field === "version" ? record.version : record.createdAt.getTime();
|
|
364
376
|
return encodeCursor({ kind: "native", field, value, id: record.id });
|
|
365
377
|
}
|
|
366
|
-
const kind =
|
|
378
|
+
const kind = kindOf2?.(record, contentField);
|
|
367
379
|
const entry = kind ? contentSortEntry(kind, record.content[contentField]) : null;
|
|
368
380
|
return encodeEntry(contentField, entry, record.id);
|
|
369
381
|
};
|
|
@@ -373,38 +385,182 @@ var encodeEntry = (field, entry, id) => {
|
|
|
373
385
|
return entry.kind === "num" ? encodeCursor({ kind: "num", field, value: entry.num, id }) : encodeCursor({ kind: "text", field, value: entry.text, id });
|
|
374
386
|
};
|
|
375
387
|
|
|
388
|
+
// ../sqlite-shared/dist/mappers.js
|
|
389
|
+
var toMs = (d) => d.getTime();
|
|
390
|
+
var actorFromColumns = (subject, principal) => subject == null ? void 0 : {
|
|
391
|
+
subjectId: subject,
|
|
392
|
+
...principal != null && { principalId: principal }
|
|
393
|
+
};
|
|
394
|
+
var fromMs = (ms) => new Date(ms);
|
|
395
|
+
var rowToRecord = (row, associations, permissions) => {
|
|
396
|
+
const record = {
|
|
397
|
+
id: row.id,
|
|
398
|
+
typeId: row.type_id,
|
|
399
|
+
createdAt: fromMs(row.created_at),
|
|
400
|
+
updatedAt: fromMs(row.updated_at),
|
|
401
|
+
content: JSON.parse(row.content),
|
|
402
|
+
version: row.version
|
|
403
|
+
};
|
|
404
|
+
if (row.parent_id != null)
|
|
405
|
+
record.parentId = row.parent_id;
|
|
406
|
+
if (row.app_id != null)
|
|
407
|
+
record.appId = row.app_id;
|
|
408
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
409
|
+
if (createdBy)
|
|
410
|
+
record.createdBy = createdBy;
|
|
411
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
412
|
+
if (updatedBy)
|
|
413
|
+
record.updatedBy = updatedBy;
|
|
414
|
+
if (row.deleted_at != null)
|
|
415
|
+
record.deletedAt = fromMs(row.deleted_at);
|
|
416
|
+
if (row.unlisted_at != null)
|
|
417
|
+
record.unlistedAt = fromMs(row.unlisted_at);
|
|
418
|
+
if (associations.length)
|
|
419
|
+
record.associations = associations;
|
|
420
|
+
if (permissions.length)
|
|
421
|
+
record.permissions = permissions;
|
|
422
|
+
return record;
|
|
423
|
+
};
|
|
424
|
+
var rowToAssociation = (row) => {
|
|
425
|
+
if (row.kind === "tag") {
|
|
426
|
+
return { kind: "tag", label: row.label };
|
|
427
|
+
}
|
|
428
|
+
if (row.kind === "attachment") {
|
|
429
|
+
const attachmentRecordId = row.attachment_record_id;
|
|
430
|
+
return {
|
|
431
|
+
kind: "attachment",
|
|
432
|
+
label: row.label,
|
|
433
|
+
fileId: row.file_id,
|
|
434
|
+
// Stored as '' when absent, like every other optional column here —
|
|
435
|
+
// the field is omitted rather than served empty, so a round trip
|
|
436
|
+
// produces the association the caller wrote.
|
|
437
|
+
...attachmentRecordId && { attachmentRecordId }
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
kind: "relationship",
|
|
442
|
+
label: row.label,
|
|
443
|
+
target: rowToTarget(row)
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
var associationKeyColumns = (a) => {
|
|
447
|
+
if (a.kind === "attachment")
|
|
448
|
+
return [a.fileId, "", "", "", ""];
|
|
449
|
+
if (a.kind !== "relationship")
|
|
450
|
+
return ["", "", "", "", ""];
|
|
451
|
+
const t = a.target;
|
|
452
|
+
if (t.kind === "entity")
|
|
453
|
+
return ["", "entity", t.entityId, "", ""];
|
|
454
|
+
if (t.kind === "external")
|
|
455
|
+
return ["", "external", t.id, t.ns, ""];
|
|
456
|
+
return ["", "record", t.recordId, "", t.stackUrl ?? ""];
|
|
457
|
+
};
|
|
458
|
+
var permissionKeyColumns = (p) => {
|
|
459
|
+
if (p.kind === "anyone")
|
|
460
|
+
return ["", "", ""];
|
|
461
|
+
const g = p.grantee;
|
|
462
|
+
return g.kind === "entity" ? ["entity", g.entityId, ""] : ["group", g.groupId, g.role];
|
|
463
|
+
};
|
|
464
|
+
var rowToPermission = (row) => {
|
|
465
|
+
if (row.kind === "anyone")
|
|
466
|
+
return { kind: "anyone", label: "read" };
|
|
467
|
+
const id = row.grantee_id;
|
|
468
|
+
return {
|
|
469
|
+
kind: "permission",
|
|
470
|
+
label: row.label,
|
|
471
|
+
grantee: row.grantee_kind === "group" ? { kind: "group", groupId: id, role: row.role } : { kind: "entity", entityId: id }
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
var rowToTarget = (row) => {
|
|
475
|
+
const id = row.related_id;
|
|
476
|
+
if (row.related_scope === "entity")
|
|
477
|
+
return { kind: "entity", entityId: id };
|
|
478
|
+
if (row.related_scope === "external") {
|
|
479
|
+
return { kind: "external", ns: row.related_ns, id };
|
|
480
|
+
}
|
|
481
|
+
const stackUrl = row.related_stack;
|
|
482
|
+
return { kind: "record", recordId: id, ...stackUrl && { stackUrl } };
|
|
483
|
+
};
|
|
484
|
+
var rowToType = (row) => {
|
|
485
|
+
const t = {
|
|
486
|
+
id: row.id,
|
|
487
|
+
baseId: row.base_id,
|
|
488
|
+
version: row.version,
|
|
489
|
+
name: row.name,
|
|
490
|
+
schema: JSON.parse(row.schema),
|
|
491
|
+
schemaHash: row.schema_hash,
|
|
492
|
+
createdAt: fromMs(row.created_at)
|
|
493
|
+
};
|
|
494
|
+
if (row.migrates_from)
|
|
495
|
+
t.migratesFrom = row.migrates_from;
|
|
496
|
+
return t;
|
|
497
|
+
};
|
|
498
|
+
var rowToVersion = (row) => {
|
|
499
|
+
const v = {
|
|
500
|
+
version: row.version,
|
|
501
|
+
typeId: row.type_id,
|
|
502
|
+
content: JSON.parse(row.content),
|
|
503
|
+
updatedAt: fromMs(row.updated_at)
|
|
504
|
+
};
|
|
505
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
506
|
+
if (createdBy)
|
|
507
|
+
v.createdBy = createdBy;
|
|
508
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
509
|
+
if (updatedBy)
|
|
510
|
+
v.updatedBy = updatedBy;
|
|
511
|
+
return v;
|
|
512
|
+
};
|
|
513
|
+
var rowToJournalEntry = (row) => {
|
|
514
|
+
const e = {
|
|
515
|
+
seq: row.seq,
|
|
516
|
+
at: fromMs(row.at),
|
|
517
|
+
kind: row.kind,
|
|
518
|
+
ops: JSON.parse(row.ops),
|
|
519
|
+
version: row.version,
|
|
520
|
+
typeId: row.type_id
|
|
521
|
+
};
|
|
522
|
+
if (row.parent_id != null)
|
|
523
|
+
e.parentId = row.parent_id;
|
|
524
|
+
if (row.previous_parent_id != null) {
|
|
525
|
+
e.previousParentId = row.previous_parent_id === "" ? null : row.previous_parent_id;
|
|
526
|
+
}
|
|
527
|
+
if (row.actor != null)
|
|
528
|
+
e.actor = JSON.parse(row.actor);
|
|
529
|
+
if (row.associations != null)
|
|
530
|
+
e.associations = JSON.parse(row.associations);
|
|
531
|
+
return e;
|
|
532
|
+
};
|
|
533
|
+
|
|
376
534
|
// ../sqlite-shared/dist/query.js
|
|
377
|
-
var atBudget = (page,
|
|
535
|
+
var atBudget = (page, rows2) => ({
|
|
378
536
|
sql: page.sql,
|
|
379
|
-
params: [...page.params,
|
|
537
|
+
params: [...page.params, rows2]
|
|
380
538
|
});
|
|
381
|
-
var sqlDirection = (query) =>
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
539
|
+
var sqlDirection = (query) => query.sort.direction === "asc" ? "ASC" : "DESC";
|
|
540
|
+
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`;
|
|
541
|
+
var members = (alias) => `CASE WHEN ${alias}.t = 'object' THEN ${alias}.v ELSE '{}' END`;
|
|
542
|
+
var emptyStatement = () => ({
|
|
543
|
+
tables: [],
|
|
544
|
+
tableParams: [],
|
|
545
|
+
conditions: ["r.id != '_config'"],
|
|
546
|
+
params: []
|
|
547
|
+
});
|
|
548
|
+
var rows = (s, columns, tuples) => {
|
|
549
|
+
const name = `t${s.tables.length}`;
|
|
550
|
+
s.tables.push(`${name} AS MATERIALIZED ${rowsTable(columns)}`);
|
|
551
|
+
s.tableParams.push(JSON.stringify(tuples));
|
|
552
|
+
return name;
|
|
553
|
+
};
|
|
554
|
+
var rowsTable = (columns) => {
|
|
555
|
+
const named = columns.map((column, i) => `json_extract(value, '$[${i}]') AS ${column}`);
|
|
556
|
+
return `(SELECT ${named.join(", ")} FROM json_each(?))`;
|
|
387
557
|
};
|
|
388
|
-
var
|
|
389
|
-
var
|
|
390
|
-
var
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const from = [`json_each(r.content) AS ${key}`, `json_each(${spread(key)}) AS ${value}`];
|
|
394
|
-
const where = [`${key}.key = ?`];
|
|
395
|
-
params.push(segments[0]);
|
|
396
|
-
for (const segment of segments.slice(1)) {
|
|
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 ")})`;
|
|
558
|
+
var CONDITION_COLUMNS = ["scope", "path", "op", "val", "kind"];
|
|
559
|
+
var kindOf = (type) => `CASE ${type} WHEN 'integer' THEN 'number' WHEN 'real' THEN 'number' ELSE ${type} END`;
|
|
560
|
+
var holds = (c, r) => {
|
|
561
|
+
const depth = `json_array_length(${c}.path)`;
|
|
562
|
+
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
563
|
};
|
|
407
|
-
var equalsLeaf = (alias) => `${alias}.type NOT IN ('object', 'array') AND ${alias}.value = ?`;
|
|
408
564
|
var SORT_ALIAS = "cs";
|
|
409
565
|
var cursorMismatch = (cursor, field) => new StackBadRequestError(`Cursor sort field "${cursor.field}" does not match query sort field "${field}"`);
|
|
410
566
|
var nativeCursor = (cursor, field) => {
|
|
@@ -417,37 +573,77 @@ var contentCursor = (cursor, field) => {
|
|
|
417
573
|
throw cursorMismatch(cursor, field);
|
|
418
574
|
return cursor;
|
|
419
575
|
};
|
|
420
|
-
var
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
576
|
+
var valueKind = (value) => {
|
|
577
|
+
if (typeof value === "boolean")
|
|
578
|
+
return `${value}`;
|
|
579
|
+
if (typeof value === "number")
|
|
580
|
+
return "number";
|
|
581
|
+
return typeof value === "string" ? "text" : null;
|
|
582
|
+
};
|
|
583
|
+
var contentRows = (scope, predicate) => {
|
|
584
|
+
const row = (key, op, value = null) => {
|
|
585
|
+
const path = parseContentFilterKey(key);
|
|
586
|
+
return [scope, path, op, value, valueKind(value)];
|
|
587
|
+
};
|
|
588
|
+
return [
|
|
589
|
+
...Object.entries(predicate.content ?? {}).map(([key, value]) => (
|
|
590
|
+
// undefined is the same absence null names — JSON drops it on the way
|
|
591
|
+
// over the wire, so treating it as a value would make an in-process
|
|
592
|
+
// query and its wire equivalent disagree.
|
|
593
|
+
value === null || value === void 0 ? row(key, "isNull") : row(key, "equals", value)
|
|
594
|
+
)),
|
|
595
|
+
...(predicate.contentPresent ?? []).map((key) => row(key, "present"))
|
|
596
|
+
];
|
|
597
|
+
};
|
|
598
|
+
var inList = (s, column, values) => {
|
|
599
|
+
if (values.length === 1) {
|
|
600
|
+
s.params.push(values[0]);
|
|
601
|
+
return `${column} = ?`;
|
|
433
602
|
}
|
|
434
|
-
|
|
435
|
-
|
|
603
|
+
const table = rows(s, ["value"], values.map((value) => [value]));
|
|
604
|
+
return `${column} IN (SELECT value FROM ${table})`;
|
|
605
|
+
};
|
|
606
|
+
var familyRange = (baseId) => [`${baseId}@`, `${baseId}A`];
|
|
607
|
+
var withinFamily = (column, lo, hi) => `(${column} >= ${lo} AND ${column} < ${hi})`;
|
|
608
|
+
var inFamily = (column, baseId) => ({
|
|
609
|
+
sql: withinFamily(column, "?", "?"),
|
|
610
|
+
params: familyRange(baseId)
|
|
611
|
+
});
|
|
612
|
+
var inFamilies = (s, record, baseIds) => {
|
|
613
|
+
if (baseIds.length === 1) {
|
|
614
|
+
const one = inFamily(`${record}.type_id`, baseIds[0]);
|
|
615
|
+
s.params.push(...one.params);
|
|
616
|
+
return one.sql;
|
|
617
|
+
}
|
|
618
|
+
const table = rows(s, ["lo", "hi"], baseIds.map(familyRange));
|
|
619
|
+
return `${record}.id IN (SELECT fr.id FROM ${table} f JOIN records fr ON ${withinFamily("fr.type_id", "f.lo", "f.hi")})`;
|
|
620
|
+
};
|
|
621
|
+
var carriesPermissions = (s, sets) => {
|
|
622
|
+
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)])));
|
|
623
|
+
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))`;
|
|
624
|
+
};
|
|
625
|
+
var reachConditions = (s, reach) => {
|
|
626
|
+
if (reach.fence)
|
|
627
|
+
s.conditions.push(inFamilies(s, "r", reach.fence));
|
|
628
|
+
const within = reach.within;
|
|
629
|
+
if (within) {
|
|
630
|
+
const arms = [inFamilies(s, "r", within.baseIds)];
|
|
631
|
+
if (within.authored) {
|
|
632
|
+
s.params.push(within.authored.subjectId);
|
|
633
|
+
arms.push(`(r.entity_id = ? AND ${inFamilies(s, "r", within.authored.baseIds)})`);
|
|
634
|
+
}
|
|
635
|
+
arms.push(carriesPermissions(s, within.permissions));
|
|
636
|
+
s.conditions.push(`(${arms.join(" OR ")})`);
|
|
436
637
|
}
|
|
437
|
-
return conditions;
|
|
438
638
|
};
|
|
439
|
-
var recordConditions = (query) => {
|
|
440
|
-
const
|
|
441
|
-
const params =
|
|
639
|
+
var recordConditions = (query, reach) => {
|
|
640
|
+
const s = emptyStatement();
|
|
641
|
+
const { conditions, params } = s;
|
|
442
642
|
const f = query.filter ?? {};
|
|
443
|
-
let aliasSeq = 0;
|
|
444
|
-
const nextContentAlias = () => `cp${aliasSeq++}`;
|
|
445
643
|
const inFilter = (column, value) => {
|
|
446
644
|
if (value === void 0)
|
|
447
645
|
return;
|
|
448
|
-
|
|
449
|
-
conditions.push(`r.${column} IN (${values.map(() => "?").join(",")})`);
|
|
450
|
-
params.push(...values);
|
|
646
|
+
conditions.push(inList(s, `r.${column}`, Array.isArray(value) ? value : [value]));
|
|
451
647
|
};
|
|
452
648
|
const rangeFilter = (column, range) => {
|
|
453
649
|
if (range?.after) {
|
|
@@ -465,7 +661,12 @@ var recordConditions = (query) => {
|
|
|
465
661
|
if (!f.includeUnlisted) {
|
|
466
662
|
conditions.push("r.unlisted_at IS NULL");
|
|
467
663
|
}
|
|
664
|
+
if (reach)
|
|
665
|
+
reachConditions(s, reach);
|
|
468
666
|
inFilter("type_id", f.typeId);
|
|
667
|
+
if (f.baseId !== void 0) {
|
|
668
|
+
conditions.push(inFamilies(s, "r", Array.isArray(f.baseId) ? f.baseId : [f.baseId]));
|
|
669
|
+
}
|
|
469
670
|
if (f.parentId !== void 0) {
|
|
470
671
|
if (f.parentId === null) {
|
|
471
672
|
conditions.push("r.parent_id IS NULL");
|
|
@@ -481,10 +682,10 @@ var recordConditions = (query) => {
|
|
|
481
682
|
rangeFilter("updated_at", f.updatedAt);
|
|
482
683
|
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
684
|
if (f.tags?.length) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
685
|
+
const tags = [...new Set(f.tags)];
|
|
686
|
+
const label = inList(s, "a.label", tags);
|
|
687
|
+
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) = ?)`);
|
|
688
|
+
params.push(tags.length);
|
|
488
689
|
}
|
|
489
690
|
if (f.attachment) {
|
|
490
691
|
const clauses = [];
|
|
@@ -530,15 +731,17 @@ var recordConditions = (query) => {
|
|
|
530
731
|
}
|
|
531
732
|
conditions.push(hasAssociation("relationship", clauses));
|
|
532
733
|
}
|
|
533
|
-
|
|
534
|
-
conditions.push(...contentConditions(f, params, nextContentAlias));
|
|
535
|
-
}
|
|
734
|
+
const content = contentRows(null, f);
|
|
536
735
|
if (f.contentByType) {
|
|
537
|
-
const arms = Object.entries(f.contentByType)
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
736
|
+
const arms = Object.entries(f.contentByType);
|
|
737
|
+
const typeIds = arms.map(([typeId]) => typeId);
|
|
738
|
+
conditions.push(inList(s, "r.type_id", typeIds));
|
|
739
|
+
for (const [typeId, arm] of arms)
|
|
740
|
+
content.push(...contentRows(typeId, arm));
|
|
741
|
+
}
|
|
742
|
+
if (content.length > 0) {
|
|
743
|
+
const table = rows(s, CONDITION_COLUMNS, content);
|
|
744
|
+
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
745
|
}
|
|
543
746
|
if (f.search) {
|
|
544
747
|
const sanitized = sanitizeFts5Query(f.search);
|
|
@@ -549,7 +752,7 @@ var recordConditions = (query) => {
|
|
|
549
752
|
conditions.push("0");
|
|
550
753
|
}
|
|
551
754
|
}
|
|
552
|
-
return
|
|
755
|
+
return s;
|
|
553
756
|
};
|
|
554
757
|
var presentCursorCondition = (cursor, op, params) => {
|
|
555
758
|
if (cursor.kind === "num") {
|
|
@@ -560,56 +763,54 @@ var presentCursorCondition = (cursor, op, params) => {
|
|
|
560
763
|
params.push(key, key, cursor.value, cursor.value, cursor.id);
|
|
561
764
|
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
765
|
};
|
|
563
|
-
var statement = (
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
|
|
766
|
+
var statement = (s, from, tail) => {
|
|
767
|
+
const select = `SELECT r.* FROM ${from} WHERE ${s.conditions.join(" AND ")}${tail}`;
|
|
768
|
+
const sql = s.tables.length === 0 ? select : `WITH ${s.tables.join(", ")} ${select}`;
|
|
769
|
+
return { sql, params: [...s.tableParams, ...s.params] };
|
|
770
|
+
};
|
|
771
|
+
var buildQueryPlan = (query, reach) => {
|
|
568
772
|
const dir = sqlDirection(query);
|
|
569
773
|
const op = dir === "ASC" ? ">" : "<";
|
|
570
774
|
const decoded = query.cursor ? decodeCursor(query.cursor) : null;
|
|
571
|
-
if (query.sort
|
|
572
|
-
return byTypePlan(query, query.sort.contentFieldByType, dir, op, decoded);
|
|
775
|
+
if (query.sort.contentFieldByType) {
|
|
776
|
+
return byTypePlan(query, reach, query.sort.contentFieldByType, dir, op, decoded);
|
|
573
777
|
}
|
|
574
|
-
const contentField = query.sort
|
|
575
|
-
if (
|
|
576
|
-
const
|
|
577
|
-
const field = getSortField(query);
|
|
778
|
+
const { contentField, field } = query.sort;
|
|
779
|
+
if (field !== void 0) {
|
|
780
|
+
const where = recordConditions(query, reach);
|
|
578
781
|
const col = getSortColumn(field);
|
|
579
782
|
if (decoded) {
|
|
580
783
|
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);
|
|
784
|
+
where.conditions.push(`(r.${col} ${op} ? OR (r.${col} = ? AND r.id ${op} ?))`);
|
|
785
|
+
where.params.push(cursor2.value, cursor2.value, cursor2.id);
|
|
583
786
|
}
|
|
584
|
-
return [
|
|
585
|
-
statement("r.*", "records r", conditions, params, ` ORDER BY r.${col} ${dir}, r.id ${dir} LIMIT ?`)
|
|
586
|
-
];
|
|
787
|
+
return [statement(where, "records r", ` ORDER BY r.${col} ${dir}, r.id ${dir} LIMIT ?`)];
|
|
587
788
|
}
|
|
588
789
|
const cursor = decoded ? contentCursor(decoded, contentField) : null;
|
|
589
790
|
const pages = [];
|
|
590
791
|
if (cursor?.kind !== "absent") {
|
|
591
|
-
const
|
|
792
|
+
const where = recordConditions(query, reach);
|
|
592
793
|
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);
|
|
794
|
+
where.conditions.push(`${SORT_ALIAS}.field = ?`);
|
|
795
|
+
where.params.push(contentField);
|
|
595
796
|
if (cursor)
|
|
596
|
-
conditions.push(presentCursorCondition(cursor, op, params));
|
|
797
|
+
where.conditions.push(presentCursorCondition(cursor, op, where.params));
|
|
597
798
|
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(
|
|
799
|
+
pages.push(statement(where, from, ` ORDER BY ${order} LIMIT ?`));
|
|
599
800
|
}
|
|
600
801
|
{
|
|
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);
|
|
802
|
+
const where = recordConditions(query, reach);
|
|
803
|
+
where.conditions.push(`NOT EXISTS (SELECT 1 FROM content_index ${SORT_ALIAS} WHERE ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = ?)`);
|
|
804
|
+
where.params.push(contentField);
|
|
604
805
|
if (cursor?.kind === "absent") {
|
|
605
|
-
conditions.push(`r.id ${op} ?`);
|
|
606
|
-
params.push(cursor.id);
|
|
806
|
+
where.conditions.push(`r.id ${op} ?`);
|
|
807
|
+
where.params.push(cursor.id);
|
|
607
808
|
}
|
|
608
|
-
pages.push(statement(
|
|
809
|
+
pages.push(statement(where, "records r", ` ORDER BY r.id ${dir} LIMIT ?`));
|
|
609
810
|
}
|
|
610
811
|
return pages;
|
|
611
812
|
};
|
|
612
|
-
var byTypePlan = (query, byType, dir, op, decoded) => {
|
|
813
|
+
var byTypePlan = (query, reach, byType, dir, op, decoded) => {
|
|
613
814
|
const cursor = decoded ? contentCursor(decoded, byTypeDescriptor(byType)) : null;
|
|
614
815
|
const fields = [];
|
|
615
816
|
const constants = [];
|
|
@@ -623,42 +824,38 @@ var byTypePlan = (query, byType, dir, op, decoded) => {
|
|
|
623
824
|
constants.push([typeId, entry]);
|
|
624
825
|
}
|
|
625
826
|
const pages = [];
|
|
626
|
-
const fieldOf = (alias, record) => fields.map(() => `(${record}.type_id = ? AND ${alias}.field = ?)`).join(" OR ");
|
|
627
827
|
if (cursor?.kind !== "absent" && fields.length + constants.length > 0) {
|
|
828
|
+
const where = recordConditions(query, reach);
|
|
628
829
|
const sources = [];
|
|
629
|
-
const sourceParams = [];
|
|
630
830
|
if (fields.length > 0) {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
sourceParams.push(typeId, field);
|
|
831
|
+
const byField = rows(where, ["type_id", "field"], fields);
|
|
832
|
+
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
833
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
834
|
+
if (constants.length > 0) {
|
|
835
|
+
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]));
|
|
836
|
+
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
837
|
}
|
|
639
|
-
const { conditions, params } = recordConditions(query);
|
|
640
838
|
if (cursor)
|
|
641
|
-
conditions.push(presentCursorCondition(cursor, op, params));
|
|
839
|
+
where.conditions.push(presentCursorCondition(cursor, op, where.params));
|
|
642
840
|
const from = `(${sources.join(" UNION ALL ")}) ${SORT_ALIAS} JOIN records r ON r.id = ${SORT_ALIAS}.record_id`;
|
|
643
841
|
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(
|
|
842
|
+
pages.push(statement(where, from, ` ORDER BY ${order} LIMIT ?`));
|
|
645
843
|
}
|
|
646
844
|
{
|
|
647
|
-
const
|
|
845
|
+
const where = recordConditions(query, reach);
|
|
648
846
|
if (fields.length > 0) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
params.push(typeId, field);
|
|
847
|
+
const byField = rows(where, ["type_id", "field"], fields);
|
|
848
|
+
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
849
|
}
|
|
653
850
|
if (constants.length > 0) {
|
|
654
|
-
|
|
655
|
-
|
|
851
|
+
const typeIds = constants.map(([typeId]) => typeId);
|
|
852
|
+
where.conditions.push(`NOT (${inList(where, "r.type_id", typeIds)})`);
|
|
656
853
|
}
|
|
657
854
|
if (cursor?.kind === "absent") {
|
|
658
|
-
conditions.push(`r.id ${op} ?`);
|
|
659
|
-
params.push(cursor.id);
|
|
855
|
+
where.conditions.push(`r.id ${op} ?`);
|
|
856
|
+
where.params.push(cursor.id);
|
|
660
857
|
}
|
|
661
|
-
pages.push(statement(
|
|
858
|
+
pages.push(statement(where, "records r", ` ORDER BY r.id ${dir} LIMIT ?`));
|
|
662
859
|
}
|
|
663
860
|
return pages;
|
|
664
861
|
};
|
|
@@ -751,154 +948,6 @@ var ContentIndex = class {
|
|
|
751
948
|
return fields;
|
|
752
949
|
}
|
|
753
950
|
};
|
|
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
951
|
|
|
903
952
|
// ../sqlite-shared/dist/record-logic.js
|
|
904
953
|
var asStackBadRequestError = (query, run) => {
|
|
@@ -957,6 +1006,18 @@ var SharedSqlRecordLogic = class {
|
|
|
957
1006
|
throw recordNotFound(id);
|
|
958
1007
|
throw versionConflict(id, expectedVersion, version);
|
|
959
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* An aspect write's record, inside its transaction: missing is not found,
|
|
1011
|
+
* a tombstone is refused, and anything else answers its version.
|
|
1012
|
+
*/
|
|
1013
|
+
liveVersion(id) {
|
|
1014
|
+
const row = this.exec.get("SELECT version, deleted_at FROM records WHERE id = ?", [id]);
|
|
1015
|
+
if (!row)
|
|
1016
|
+
throw recordNotFound(id);
|
|
1017
|
+
if (row.deleted_at !== null)
|
|
1018
|
+
throw softDeletedRefusal(id);
|
|
1019
|
+
return row.version;
|
|
1020
|
+
}
|
|
960
1021
|
currentVersion(id) {
|
|
961
1022
|
return this.exec.get("SELECT version FROM records WHERE id = ?", [id])?.version;
|
|
962
1023
|
}
|
|
@@ -1040,9 +1101,10 @@ var SharedSqlRecordLogic = class {
|
|
|
1040
1101
|
}
|
|
1041
1102
|
throw err;
|
|
1042
1103
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1104
|
+
if (record.associations?.length)
|
|
1105
|
+
this.insertAssociations(record.id, record.associations);
|
|
1106
|
+
if (record.permissions?.length)
|
|
1107
|
+
this.insertPermissions(record.id, record.permissions);
|
|
1046
1108
|
this.searchIndex.insert(record.id, JSON.stringify(record.content));
|
|
1047
1109
|
this.contentIndex.sync(record.id, record.typeId, record.content);
|
|
1048
1110
|
this.appendJournal(record.id, opts.journal);
|
|
@@ -1060,56 +1122,55 @@ var SharedSqlRecordLogic = class {
|
|
|
1060
1122
|
*/
|
|
1061
1123
|
readRecord(id) {
|
|
1062
1124
|
const row = this.exec.get("SELECT * FROM records WHERE id = ?", [id]);
|
|
1063
|
-
return row ? this.hydrate(row) : null;
|
|
1125
|
+
return row ? this.hydrate([row])[0] : null;
|
|
1064
1126
|
}
|
|
1065
|
-
/**
|
|
1066
|
-
|
|
1067
|
-
|
|
1127
|
+
/**
|
|
1128
|
+
* Records rows with their associations and permissions attached: one
|
|
1129
|
+
* statement per element table for the whole list, so a page costs three
|
|
1130
|
+
* reads however many records it holds.
|
|
1131
|
+
*/
|
|
1132
|
+
hydrate(rows2) {
|
|
1133
|
+
const ids = rows2.map((row) => row.id);
|
|
1134
|
+
const associations = this.elementsByRecord("associations", ids, rowToAssociation);
|
|
1135
|
+
const permissions = this.elementsByRecord("permissions", ids, rowToPermission);
|
|
1136
|
+
return rows2.map((row) => rowToRecord(row, associations.get(row.id) ?? [], permissions.get(row.id) ?? []));
|
|
1068
1137
|
}
|
|
1069
1138
|
/**
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1139
|
+
* The patch applies to the content as it stands when the write lands,
|
|
1140
|
+
* read inside the same transaction.
|
|
1072
1141
|
*/
|
|
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
|
-
}
|
|
1142
|
+
async patchContent(id, patch, opts = {}) {
|
|
1085
1143
|
this.exec.transaction(() => {
|
|
1086
1144
|
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
1145
|
if (opts.snapshot)
|
|
1101
1146
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1147
|
+
this.rewriteContent(id, current.typeId, applyMergePatch(current.content, patch), opts);
|
|
1148
|
+
this.appendJournal(id, opts.journal);
|
|
1149
|
+
});
|
|
1150
|
+
return this.reread(id, "patchContent");
|
|
1151
|
+
}
|
|
1152
|
+
async setParent(id, parentId, opts = {}) {
|
|
1153
|
+
return this.aspectUpdate(id, "parent_id = ?", parentId, opts, "setParent");
|
|
1154
|
+
}
|
|
1155
|
+
/** Stamped fresh: `Stack` hands this only a transition. */
|
|
1156
|
+
async setUnlisted(id, unlisted, opts = {}) {
|
|
1157
|
+
return this.aspectUpdate(id, "unlisted_at = ?", unlisted ? toMs(/* @__PURE__ */ new Date()) : null, opts, "setUnlisted");
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* One records column that never bumps. The tombstone refusal and the
|
|
1161
|
+
* `ifVersion` guard ride in the WHERE clause, as on a versioned update.
|
|
1162
|
+
*/
|
|
1163
|
+
aspectUpdate(id, set, value, opts, verb) {
|
|
1164
|
+
const guarded = opts.ifVersion !== void 0;
|
|
1165
|
+
this.exec.transaction(() => {
|
|
1166
|
+
const changed = this.exec.run(`UPDATE records SET ${set}
|
|
1167
|
+
WHERE id = ? AND deleted_at IS NULL${guarded ? " AND version = ?" : ""}`, [value, id, ...guarded ? [opts.ifVersion] : []]);
|
|
1168
|
+
if (changed === 0) {
|
|
1169
|
+
throw versionConflict(id, opts.ifVersion, this.liveVersion(id));
|
|
1109
1170
|
}
|
|
1110
1171
|
this.appendJournal(id, opts.journal);
|
|
1111
1172
|
});
|
|
1112
|
-
return this.reread(id,
|
|
1173
|
+
return this.reread(id, verb);
|
|
1113
1174
|
}
|
|
1114
1175
|
async deleteRecord(id, opts = {}) {
|
|
1115
1176
|
if (opts.purge) {
|
|
@@ -1139,6 +1200,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1139
1200
|
this.checkExpectedVersion(id, purged.version, ifVersion);
|
|
1140
1201
|
this.searchIndex.remove(id);
|
|
1141
1202
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
1203
|
+
this.exec.run("DELETE FROM permissions WHERE record_id = ?", [id]);
|
|
1142
1204
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
1143
1205
|
this.exec.run("DELETE FROM journal WHERE record_id = ?", [id]);
|
|
1144
1206
|
this.contentIndex.remove(id);
|
|
@@ -1189,20 +1251,27 @@ var SharedSqlRecordLogic = class {
|
|
|
1189
1251
|
this.searchIndex.insert(id, json);
|
|
1190
1252
|
this.contentIndex.sync(id, typeId, content);
|
|
1191
1253
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1254
|
+
queryRecords(query) {
|
|
1255
|
+
return this.page(query);
|
|
1256
|
+
}
|
|
1257
|
+
queryReadable(query, reach) {
|
|
1258
|
+
return this.page(query, reach);
|
|
1259
|
+
}
|
|
1260
|
+
/** `reach`, when given, is one more condition of every statement in the plan. */
|
|
1261
|
+
async page(query, reach) {
|
|
1262
|
+
const { limit } = query;
|
|
1194
1263
|
const fetch = limit + 1;
|
|
1195
|
-
const
|
|
1196
|
-
for (const page2 of buildQueryPlan(query)) {
|
|
1197
|
-
const budget = fetch -
|
|
1264
|
+
const rows2 = [];
|
|
1265
|
+
for (const page2 of buildQueryPlan(query, reach)) {
|
|
1266
|
+
const budget = fetch - rows2.length;
|
|
1198
1267
|
if (budget <= 0)
|
|
1199
1268
|
break;
|
|
1200
1269
|
const statement2 = atBudget(page2, budget);
|
|
1201
|
-
|
|
1270
|
+
rows2.push(...asStackBadRequestError(query, () => this.exec.all(statement2.sql, statement2.params)));
|
|
1202
1271
|
}
|
|
1203
|
-
const hasMore =
|
|
1204
|
-
const page = hasMore ?
|
|
1205
|
-
const records =
|
|
1272
|
+
const hasMore = rows2.length > limit;
|
|
1273
|
+
const page = hasMore ? rows2.slice(0, limit) : rows2;
|
|
1274
|
+
const records = this.hydrate(page);
|
|
1206
1275
|
const lastRecord = records[records.length - 1];
|
|
1207
1276
|
const cursor = hasMore && lastRecord ? makeCursor(lastRecord, query.sort, (record, field) => this.contentIndex.kindOf(record.typeId, field)) : null;
|
|
1208
1277
|
return { records, cursor };
|
|
@@ -1211,7 +1280,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1211
1280
|
* The reference check and the purges share one synchronous transaction,
|
|
1212
1281
|
* so nothing can add a reference between the two.
|
|
1213
1282
|
*/
|
|
1214
|
-
async deleteUnreferencedAttachmentRecords(fileId,
|
|
1283
|
+
async deleteUnreferencedAttachmentRecords(fileId, metadataBaseId) {
|
|
1215
1284
|
return this.exec.transaction(() => {
|
|
1216
1285
|
const referenced = this.exec.all(`SELECT 1 as found FROM associations WHERE kind = 'attachment' AND file_id = ?
|
|
1217
1286
|
UNION ALL
|
|
@@ -1220,11 +1289,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1220
1289
|
if (referenced.length) {
|
|
1221
1290
|
throw new StackConflictError("Attachment is still referenced by one or more records");
|
|
1222
1291
|
}
|
|
1223
|
-
|
|
1224
|
-
return [];
|
|
1225
|
-
const placeholders = metadataTypeIds.map(() => "?").join(", ");
|
|
1292
|
+
const family = inFamily("type_id", metadataBaseId);
|
|
1226
1293
|
const metaRows = this.exec.all(`SELECT id FROM records
|
|
1227
|
-
WHERE
|
|
1294
|
+
WHERE ${family.sql} AND json_extract(content, '$.fileId') = ?`, [...family.params, fileId]);
|
|
1228
1295
|
const deleted = [];
|
|
1229
1296
|
for (const row of metaRows) {
|
|
1230
1297
|
const purged = this.purgeRecord(row.id);
|
|
@@ -1244,8 +1311,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1244
1311
|
conditions.push("version < ?");
|
|
1245
1312
|
values.push(query.beforeVersion);
|
|
1246
1313
|
}
|
|
1247
|
-
const
|
|
1248
|
-
return
|
|
1314
|
+
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);
|
|
1315
|
+
return rows2.map(rowToVersion);
|
|
1249
1316
|
}
|
|
1250
1317
|
async getVersion(id, version) {
|
|
1251
1318
|
return this.readVersion(id, version);
|
|
@@ -1355,8 +1422,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1355
1422
|
conditions.push("seq > ?");
|
|
1356
1423
|
values.push(query.afterSeq);
|
|
1357
1424
|
}
|
|
1358
|
-
const
|
|
1359
|
-
return
|
|
1425
|
+
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);
|
|
1426
|
+
return rows2.map(rowToJournalEntry);
|
|
1360
1427
|
}
|
|
1361
1428
|
// -------------------------------------------------------
|
|
1362
1429
|
// Types
|
|
@@ -1381,66 +1448,71 @@ var SharedSqlRecordLogic = class {
|
|
|
1381
1448
|
return row ? rowToType(row) : null;
|
|
1382
1449
|
}
|
|
1383
1450
|
async listTypes() {
|
|
1384
|
-
const
|
|
1385
|
-
return
|
|
1451
|
+
const rows2 = this.exec.all("SELECT * FROM types ORDER BY base_id, version");
|
|
1452
|
+
return rows2.map(rowToType);
|
|
1386
1453
|
}
|
|
1387
1454
|
// -------------------------------------------------------
|
|
1388
1455
|
// Associations
|
|
1389
1456
|
// -------------------------------------------------------
|
|
1390
1457
|
/**
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1458
|
+
* A write to one of a record's two element tables, inside a transaction
|
|
1459
|
+
* that first checks the record exists, is not soft-deleted, is at
|
|
1460
|
+
* `ifVersion`, and that the table holds `expectSet`. Never bumps and
|
|
1461
|
+
* never snapshots.
|
|
1394
1462
|
*/
|
|
1395
|
-
|
|
1396
|
-
assertOneSurface(changes);
|
|
1463
|
+
elementWrite(id, table, opts, write, verb) {
|
|
1397
1464
|
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)]);
|
|
1465
|
+
this.checkExpectedVersion(id, this.liveVersion(id), opts.ifVersion);
|
|
1466
|
+
if (opts.expectSet) {
|
|
1467
|
+
const map = table === "associations" ? rowToAssociation : rowToPermission;
|
|
1468
|
+
const stored = this.elementsByRecord(table, [id], map).get(id) ?? [];
|
|
1469
|
+
if (!sameAssociationSet(stored, opts.expectSet))
|
|
1470
|
+
throw new ElementSetMovedError(id);
|
|
1414
1471
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
this.insertAssociations(recordId, adds);
|
|
1418
|
-
this.appendJournal(recordId, opts.journal);
|
|
1472
|
+
write();
|
|
1473
|
+
this.appendJournal(id, opts.journal);
|
|
1419
1474
|
});
|
|
1420
|
-
return this.reread(
|
|
1475
|
+
return this.reread(id, verb);
|
|
1421
1476
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1477
|
+
async editAssociations(id, edits, opts = {}) {
|
|
1478
|
+
return this.elementWrite(id, "associations", opts, () => {
|
|
1479
|
+
for (const edit of edits) {
|
|
1480
|
+
if (edit.op !== "remove")
|
|
1481
|
+
continue;
|
|
1482
|
+
const association = edit.association;
|
|
1483
|
+
this.exec.run(`DELETE FROM associations
|
|
1484
|
+
WHERE record_id = ? AND kind = ? AND label = ?
|
|
1485
|
+
AND file_id = ? AND related_scope = ? AND related_id = ?
|
|
1486
|
+
AND related_ns = ? AND related_stack = ?`, [id, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1487
|
+
}
|
|
1488
|
+
this.insertAssociations(id, addsOf(edits));
|
|
1489
|
+
}, "editAssociations");
|
|
1490
|
+
}
|
|
1491
|
+
async setAssociations(id, associations, opts = {}) {
|
|
1492
|
+
return this.elementWrite(id, "associations", opts, () => {
|
|
1493
|
+
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
1494
|
+
this.insertAssociations(id, associations);
|
|
1495
|
+
}, "setAssociations");
|
|
1496
|
+
}
|
|
1497
|
+
/** editAssociations() over the permissions table. */
|
|
1498
|
+
async editAccess(id, edits, opts = {}) {
|
|
1499
|
+
return this.elementWrite(id, "permissions", opts, () => {
|
|
1500
|
+
for (const edit of edits) {
|
|
1501
|
+
if (edit.op !== "remove")
|
|
1502
|
+
continue;
|
|
1503
|
+
const permission = edit.association;
|
|
1504
|
+
this.exec.run(`DELETE FROM permissions
|
|
1505
|
+
WHERE record_id = ? AND kind = ? AND label = ?
|
|
1506
|
+
AND grantee_kind = ? AND grantee_id = ? AND role = ?`, [id, permission.kind, permission.label, ...permissionKeyColumns(permission)]);
|
|
1507
|
+
}
|
|
1508
|
+
this.insertPermissions(id, addsOf(edits));
|
|
1509
|
+
}, "editAccess");
|
|
1434
1510
|
}
|
|
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);
|
|
1511
|
+
async setAccess(id, permissions, opts = {}) {
|
|
1512
|
+
return this.elementWrite(id, "permissions", opts, () => {
|
|
1513
|
+
this.exec.run("DELETE FROM permissions WHERE record_id = ?", [id]);
|
|
1514
|
+
this.insertPermissions(id, permissions);
|
|
1515
|
+
}, "setAccess");
|
|
1444
1516
|
}
|
|
1445
1517
|
/**
|
|
1446
1518
|
* Upserts on association identity, so re-pointing `attachment_record_id`
|
|
@@ -1449,46 +1521,72 @@ var SharedSqlRecordLogic = class {
|
|
|
1449
1521
|
*/
|
|
1450
1522
|
insertAssociations(recordId, associations) {
|
|
1451
1523
|
for (const assoc of associations) {
|
|
1452
|
-
|
|
1453
|
-
this.exec.run(`INSERT INTO associations
|
|
1524
|
+
this.insertRow(recordId, () => this.exec.run(`INSERT INTO associations
|
|
1454
1525
|
(record_id, kind, label, file_id, related_scope, related_id, related_ns,
|
|
1455
|
-
related_stack,
|
|
1456
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
1526
|
+
related_stack, attachment_record_id)
|
|
1527
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1457
1528
|
ON CONFLICT DO UPDATE SET attachment_record_id = excluded.attachment_record_id`, [
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
} catch (err) {
|
|
1465
|
-
if (isForeignKeyViolation(err)) {
|
|
1466
|
-
throw recordNotFound(recordId);
|
|
1467
|
-
}
|
|
1468
|
-
throw err;
|
|
1469
|
-
}
|
|
1529
|
+
recordId,
|
|
1530
|
+
assoc.kind,
|
|
1531
|
+
assoc.label,
|
|
1532
|
+
...associationKeyColumns(assoc),
|
|
1533
|
+
assoc.kind === "attachment" ? assoc.attachmentRecordId ?? "" : ""
|
|
1534
|
+
]));
|
|
1470
1535
|
}
|
|
1471
1536
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1537
|
+
/** Every column is in the key, so a repeated element is the same row. */
|
|
1538
|
+
insertPermissions(recordId, permissions) {
|
|
1539
|
+
for (const p of permissions) {
|
|
1540
|
+
this.insertRow(recordId, () => this.exec.run(`INSERT INTO permissions (record_id, kind, label, grantee_kind, grantee_id, role)
|
|
1541
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1542
|
+
ON CONFLICT DO NOTHING`, [recordId, p.kind, p.label, ...permissionKeyColumns(p)]));
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
insertRow(recordId, insert) {
|
|
1546
|
+
try {
|
|
1547
|
+
insert();
|
|
1548
|
+
} catch (err) {
|
|
1549
|
+
if (isForeignKeyViolation(err))
|
|
1550
|
+
throw recordNotFound(recordId);
|
|
1551
|
+
throw err;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* One element table's rows for every record in `ids`, in primary-key
|
|
1556
|
+
* order — the order a single record's read has always had.
|
|
1557
|
+
*/
|
|
1558
|
+
elementsByRecord(table, ids, map) {
|
|
1559
|
+
const byRecord = /* @__PURE__ */ new Map();
|
|
1560
|
+
const rows2 = this.exec.all(`WITH ids AS MATERIALIZED ${rowsTable(["id"])}
|
|
1561
|
+
SELECT * FROM ${table} WHERE record_id IN (SELECT id FROM ids)
|
|
1562
|
+
ORDER BY ${table === "associations" ? ASSOCIATION_KEY : PERMISSION_KEY}`, [JSON.stringify(ids.map((id) => [id]))]);
|
|
1563
|
+
for (const row of rows2) {
|
|
1564
|
+
const id = row.record_id;
|
|
1565
|
+
const list = byRecord.get(id);
|
|
1566
|
+
if (list)
|
|
1567
|
+
list.push(map(row));
|
|
1568
|
+
else
|
|
1569
|
+
byRecord.set(id, [map(row)]);
|
|
1570
|
+
}
|
|
1571
|
+
return byRecord;
|
|
1475
1572
|
}
|
|
1476
1573
|
};
|
|
1574
|
+
var ASSOCIATION_KEY = "record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack";
|
|
1575
|
+
var PERMISSION_KEY = "record_id, kind, label, grantee_kind, grantee_id, role";
|
|
1576
|
+
var addsOf = (edits) => edits.flatMap((e) => e.op === "add" ? [e.association] : []);
|
|
1477
1577
|
|
|
1478
1578
|
// ../sqlite-shared/dist/record-adapter.js
|
|
1479
1579
|
var SQLITE_RECORD_CAPABILITIES = {
|
|
1480
1580
|
filter: {
|
|
1481
1581
|
content: "path",
|
|
1482
1582
|
contentPresent: true,
|
|
1483
|
-
|
|
1484
|
-
// docs/spec/adapters.md § Adapter capabilities.
|
|
1485
|
-
contentByType: 64,
|
|
1583
|
+
contentByType: true,
|
|
1486
1584
|
search: true
|
|
1487
1585
|
},
|
|
1488
1586
|
sort: {
|
|
1489
1587
|
fields: ["createdAt", "updatedAt", "version"],
|
|
1490
1588
|
contentField: true,
|
|
1491
|
-
contentFieldByType:
|
|
1589
|
+
contentFieldByType: true
|
|
1492
1590
|
},
|
|
1493
1591
|
limits: {
|
|
1494
1592
|
attachmentBytes: null,
|
|
@@ -1514,8 +1612,14 @@ var SharedSqlRecordAdapter = class {
|
|
|
1514
1612
|
getRecord(id) {
|
|
1515
1613
|
return this.record.getRecord(id);
|
|
1516
1614
|
}
|
|
1517
|
-
|
|
1518
|
-
return this.record.
|
|
1615
|
+
patchContent(id, patch, opts) {
|
|
1616
|
+
return this.record.patchContent(id, patch, opts);
|
|
1617
|
+
}
|
|
1618
|
+
setParent(id, parentId, opts) {
|
|
1619
|
+
return this.record.setParent(id, parentId, opts);
|
|
1620
|
+
}
|
|
1621
|
+
setUnlisted(id, unlisted, opts) {
|
|
1622
|
+
return this.record.setUnlisted(id, unlisted, opts);
|
|
1519
1623
|
}
|
|
1520
1624
|
deleteRecord(id, opts) {
|
|
1521
1625
|
return this.record.deleteRecord(id, opts);
|
|
@@ -1532,8 +1636,11 @@ var SharedSqlRecordAdapter = class {
|
|
|
1532
1636
|
queryRecords(query) {
|
|
1533
1637
|
return this.record.queryRecords(query);
|
|
1534
1638
|
}
|
|
1535
|
-
|
|
1536
|
-
return this.record.
|
|
1639
|
+
queryReadable(query, reach) {
|
|
1640
|
+
return this.record.queryReadable(query, reach);
|
|
1641
|
+
}
|
|
1642
|
+
deleteUnreferencedAttachmentRecords(fileId, metadataBaseId) {
|
|
1643
|
+
return this.record.deleteUnreferencedAttachmentRecords(fileId, metadataBaseId);
|
|
1537
1644
|
}
|
|
1538
1645
|
// -------------------------------------------------------
|
|
1539
1646
|
// Versions
|
|
@@ -1566,10 +1673,19 @@ var SharedSqlRecordAdapter = class {
|
|
|
1566
1673
|
return this.record.listTypes();
|
|
1567
1674
|
}
|
|
1568
1675
|
// -------------------------------------------------------
|
|
1569
|
-
// Associations
|
|
1676
|
+
// Associations and permissions
|
|
1570
1677
|
// -------------------------------------------------------
|
|
1571
|
-
|
|
1572
|
-
return this.record.
|
|
1678
|
+
editAssociations(id, edits, opts) {
|
|
1679
|
+
return this.record.editAssociations(id, edits, opts);
|
|
1680
|
+
}
|
|
1681
|
+
setAssociations(id, associations, opts) {
|
|
1682
|
+
return this.record.setAssociations(id, associations, opts);
|
|
1683
|
+
}
|
|
1684
|
+
editAccess(id, edits, opts) {
|
|
1685
|
+
return this.record.editAccess(id, edits, opts);
|
|
1686
|
+
}
|
|
1687
|
+
setAccess(id, permissions, opts) {
|
|
1688
|
+
return this.record.setAccess(id, permissions, opts);
|
|
1573
1689
|
}
|
|
1574
1690
|
};
|
|
1575
1691
|
var lockPathFor = (dbPath) => `${dbPath}.lock`;
|
|
@@ -1641,8 +1757,8 @@ var SharedTokenLogic = class {
|
|
|
1641
1757
|
return { principalId: row.principal_id, subjectId: row.subject_id };
|
|
1642
1758
|
}
|
|
1643
1759
|
async listTokens() {
|
|
1644
|
-
const
|
|
1645
|
-
return
|
|
1760
|
+
const rows2 = this.exec.all("SELECT id, principal_id, subject_id, label, created_at, expires_at FROM tokens ORDER BY created_at DESC");
|
|
1761
|
+
return rows2.map((row) => ({
|
|
1646
1762
|
id: row.id,
|
|
1647
1763
|
principalId: row.principal_id,
|
|
1648
1764
|
subjectId: row.subject_id,
|