@haverstack/record-adapter-do-sqlite 0.27.0 → 0.28.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 +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { StackConflictError, applyMergePatch, StackNotFoundError,
|
|
2
|
-
import { OwnerMismatchError,
|
|
1
|
+
import { StackConflictError, applyMergePatch, StackNotFoundError, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
2
|
+
import { OwnerMismatchError, softDeletedRefusal, sameAssociationSet, ElementSetMovedError, contentSortEntry, constantSortEntry, contentSortKey, parseContentFilterKey } from '@haverstack/core/adapter';
|
|
3
3
|
|
|
4
4
|
// ../sqlite-shared/dist/schema.js
|
|
5
5
|
var RECORD_SCHEMA_SQL = `
|
|
@@ -20,34 +20,43 @@ var RECORD_SCHEMA_SQL = `
|
|
|
20
20
|
unlisted_at INTEGER
|
|
21
21
|
) STRICT;
|
|
22
22
|
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
-- over it, partitioned by kind.
|
|
27
|
-
-- See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
23
|
+
-- A record's data associations: tags, attachments and relationships.
|
|
24
|
+
-- Its permissions are in their own table below, so no predicate over this
|
|
25
|
+
-- one can match a grantee as a relationship target.
|
|
28
26
|
CREATE TABLE IF NOT EXISTS associations (
|
|
29
27
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
30
|
-
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship'
|
|
28
|
+
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship')),
|
|
31
29
|
label TEXT NOT NULL,
|
|
32
30
|
file_id TEXT NOT NULL DEFAULT '',
|
|
33
31
|
-- 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'
|
|
32
|
+
-- related_ns (a foreign namespace) or related_stack (another stack).
|
|
33
|
+
-- All are in the key, so targets differing only by a qualifier are
|
|
34
|
+
-- distinct associations.
|
|
35
|
+
related_scope TEXT NOT NULL DEFAULT '' CHECK (related_scope IN ('', 'record', 'entity', 'external')),
|
|
38
36
|
related_id TEXT NOT NULL DEFAULT '',
|
|
39
37
|
related_ns TEXT NOT NULL DEFAULT '',
|
|
40
38
|
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
39
|
-- Out of the key: it annotates a reference rather than naming one, so
|
|
45
40
|
-- re-pointing it updates the existing row. See docs/spec/attachments.md
|
|
46
41
|
-- \xA7 Naming the upload a reference came from.
|
|
47
42
|
attachment_record_id TEXT NOT NULL DEFAULT '',
|
|
48
43
|
-- Leads with record_id, so its implicit index serves every by-record
|
|
49
44
|
-- 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
|
|
45
|
+
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack)
|
|
46
|
+
) STRICT;
|
|
47
|
+
|
|
48
|
+
-- Who reaches a record, one row per element. An 'anyone' row names no
|
|
49
|
+
-- grantee; a group grantee's role is in the key, because member and
|
|
50
|
+
-- admin name two different sets of people.
|
|
51
|
+
-- See docs/spec/access-control.md \xA7 Record-level permissions.
|
|
52
|
+
CREATE TABLE IF NOT EXISTS permissions (
|
|
53
|
+
record_id TEXT NOT NULL REFERENCES records(id),
|
|
54
|
+
kind TEXT NOT NULL CHECK (kind IN ('permission', 'anyone')),
|
|
55
|
+
label TEXT NOT NULL CHECK (label IN ('read', 'write')),
|
|
56
|
+
grantee_kind TEXT NOT NULL DEFAULT '' CHECK (grantee_kind IN ('', 'entity', 'group')),
|
|
57
|
+
grantee_id TEXT NOT NULL DEFAULT '',
|
|
58
|
+
role TEXT NOT NULL DEFAULT '' CHECK (role IN ('', 'member', 'admin')),
|
|
59
|
+
PRIMARY KEY (record_id, kind, label, grantee_kind, grantee_id, role)
|
|
51
60
|
) STRICT;
|
|
52
61
|
|
|
53
62
|
-- Content and the type it is read under. No parent_id, unlisted_at or
|
|
@@ -149,6 +158,11 @@ var RECORD_SCHEMA_SQL = `
|
|
|
149
158
|
CREATE INDEX IF NOT EXISTS idx_assoc_related ON associations(kind, related_scope, related_ns, related_id, record_id);
|
|
150
159
|
CREATE INDEX IF NOT EXISTS idx_types_base_id ON types(base_id, version);
|
|
151
160
|
|
|
161
|
+
-- The permissions key with record_id moved last, so a scoped query reads
|
|
162
|
+
-- the records carrying an element straight out of the index.
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_permissions_element
|
|
164
|
+
ON permissions(kind, label, grantee_kind, grantee_id, role, record_id);
|
|
165
|
+
|
|
152
166
|
-- The whole content ordering, in index order, for one field at a time:
|
|
153
167
|
-- a content-sorted page is an ordered walk of a slice of this index.
|
|
154
168
|
CREATE INDEX IF NOT EXISTS idx_content_index_sort
|
|
@@ -320,24 +334,22 @@ var decodeCursor = (cursor) => {
|
|
|
320
334
|
throw malformed();
|
|
321
335
|
};
|
|
322
336
|
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
337
|
var getSortColumn = (field) => field === "createdAt" ? "created_at" : field === "updatedAt" ? "updated_at" : "version";
|
|
325
|
-
var makeCursor = (record, sort,
|
|
326
|
-
const byType = sort
|
|
338
|
+
var makeCursor = (record, sort, kindOf2) => {
|
|
339
|
+
const byType = sort.contentFieldByType;
|
|
327
340
|
if (byType) {
|
|
328
|
-
const
|
|
341
|
+
const field2 = byTypeDescriptor(byType);
|
|
329
342
|
const key = Object.hasOwn(byType, record.typeId) ? byType[record.typeId] : void 0;
|
|
330
|
-
const kind2 = key?.field === void 0 ? void 0 :
|
|
343
|
+
const kind2 = key?.field === void 0 ? void 0 : kindOf2?.(record, key.field);
|
|
331
344
|
const entry2 = key?.field !== void 0 ? kind2 && contentSortEntry(kind2, record.content[key.field]) : key && constantSortEntry(key.value ?? null);
|
|
332
|
-
return encodeEntry(
|
|
345
|
+
return encodeEntry(field2, entry2 || null, record.id);
|
|
333
346
|
}
|
|
334
|
-
const contentField = sort
|
|
335
|
-
if (
|
|
336
|
-
const field = getSortField({ sort });
|
|
347
|
+
const { contentField, field } = sort;
|
|
348
|
+
if (field !== void 0) {
|
|
337
349
|
const value = field === "updatedAt" ? record.updatedAt.getTime() : field === "version" ? record.version : record.createdAt.getTime();
|
|
338
350
|
return encodeCursor({ kind: "native", field, value, id: record.id });
|
|
339
351
|
}
|
|
340
|
-
const kind =
|
|
352
|
+
const kind = kindOf2?.(record, contentField);
|
|
341
353
|
const entry = kind ? contentSortEntry(kind, record.content[contentField]) : null;
|
|
342
354
|
return encodeEntry(contentField, entry, record.id);
|
|
343
355
|
};
|
|
@@ -347,38 +359,182 @@ var encodeEntry = (field, entry, id) => {
|
|
|
347
359
|
return entry.kind === "num" ? encodeCursor({ kind: "num", field, value: entry.num, id }) : encodeCursor({ kind: "text", field, value: entry.text, id });
|
|
348
360
|
};
|
|
349
361
|
|
|
362
|
+
// ../sqlite-shared/dist/mappers.js
|
|
363
|
+
var toMs = (d) => d.getTime();
|
|
364
|
+
var actorFromColumns = (subject, principal) => subject == null ? void 0 : {
|
|
365
|
+
subjectId: subject,
|
|
366
|
+
...principal != null && { principalId: principal }
|
|
367
|
+
};
|
|
368
|
+
var fromMs = (ms) => new Date(ms);
|
|
369
|
+
var rowToRecord = (row, associations, permissions) => {
|
|
370
|
+
const record = {
|
|
371
|
+
id: row.id,
|
|
372
|
+
typeId: row.type_id,
|
|
373
|
+
createdAt: fromMs(row.created_at),
|
|
374
|
+
updatedAt: fromMs(row.updated_at),
|
|
375
|
+
content: JSON.parse(row.content),
|
|
376
|
+
version: row.version
|
|
377
|
+
};
|
|
378
|
+
if (row.parent_id != null)
|
|
379
|
+
record.parentId = row.parent_id;
|
|
380
|
+
if (row.app_id != null)
|
|
381
|
+
record.appId = row.app_id;
|
|
382
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
383
|
+
if (createdBy)
|
|
384
|
+
record.createdBy = createdBy;
|
|
385
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
386
|
+
if (updatedBy)
|
|
387
|
+
record.updatedBy = updatedBy;
|
|
388
|
+
if (row.deleted_at != null)
|
|
389
|
+
record.deletedAt = fromMs(row.deleted_at);
|
|
390
|
+
if (row.unlisted_at != null)
|
|
391
|
+
record.unlistedAt = fromMs(row.unlisted_at);
|
|
392
|
+
if (associations.length)
|
|
393
|
+
record.associations = associations;
|
|
394
|
+
if (permissions.length)
|
|
395
|
+
record.permissions = permissions;
|
|
396
|
+
return record;
|
|
397
|
+
};
|
|
398
|
+
var rowToAssociation = (row) => {
|
|
399
|
+
if (row.kind === "tag") {
|
|
400
|
+
return { kind: "tag", label: row.label };
|
|
401
|
+
}
|
|
402
|
+
if (row.kind === "attachment") {
|
|
403
|
+
const attachmentRecordId = row.attachment_record_id;
|
|
404
|
+
return {
|
|
405
|
+
kind: "attachment",
|
|
406
|
+
label: row.label,
|
|
407
|
+
fileId: row.file_id,
|
|
408
|
+
// Stored as '' when absent, like every other optional column here —
|
|
409
|
+
// the field is omitted rather than served empty, so a round trip
|
|
410
|
+
// produces the association the caller wrote.
|
|
411
|
+
...attachmentRecordId && { attachmentRecordId }
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
kind: "relationship",
|
|
416
|
+
label: row.label,
|
|
417
|
+
target: rowToTarget(row)
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
var associationKeyColumns = (a) => {
|
|
421
|
+
if (a.kind === "attachment")
|
|
422
|
+
return [a.fileId, "", "", "", ""];
|
|
423
|
+
if (a.kind !== "relationship")
|
|
424
|
+
return ["", "", "", "", ""];
|
|
425
|
+
const t = a.target;
|
|
426
|
+
if (t.kind === "entity")
|
|
427
|
+
return ["", "entity", t.entityId, "", ""];
|
|
428
|
+
if (t.kind === "external")
|
|
429
|
+
return ["", "external", t.id, t.ns, ""];
|
|
430
|
+
return ["", "record", t.recordId, "", t.stackUrl ?? ""];
|
|
431
|
+
};
|
|
432
|
+
var permissionKeyColumns = (p) => {
|
|
433
|
+
if (p.kind === "anyone")
|
|
434
|
+
return ["", "", ""];
|
|
435
|
+
const g = p.grantee;
|
|
436
|
+
return g.kind === "entity" ? ["entity", g.entityId, ""] : ["group", g.groupId, g.role];
|
|
437
|
+
};
|
|
438
|
+
var rowToPermission = (row) => {
|
|
439
|
+
if (row.kind === "anyone")
|
|
440
|
+
return { kind: "anyone", label: "read" };
|
|
441
|
+
const id = row.grantee_id;
|
|
442
|
+
return {
|
|
443
|
+
kind: "permission",
|
|
444
|
+
label: row.label,
|
|
445
|
+
grantee: row.grantee_kind === "group" ? { kind: "group", groupId: id, role: row.role } : { kind: "entity", entityId: id }
|
|
446
|
+
};
|
|
447
|
+
};
|
|
448
|
+
var rowToTarget = (row) => {
|
|
449
|
+
const id = row.related_id;
|
|
450
|
+
if (row.related_scope === "entity")
|
|
451
|
+
return { kind: "entity", entityId: id };
|
|
452
|
+
if (row.related_scope === "external") {
|
|
453
|
+
return { kind: "external", ns: row.related_ns, id };
|
|
454
|
+
}
|
|
455
|
+
const stackUrl = row.related_stack;
|
|
456
|
+
return { kind: "record", recordId: id, ...stackUrl && { stackUrl } };
|
|
457
|
+
};
|
|
458
|
+
var rowToType = (row) => {
|
|
459
|
+
const t = {
|
|
460
|
+
id: row.id,
|
|
461
|
+
baseId: row.base_id,
|
|
462
|
+
version: row.version,
|
|
463
|
+
name: row.name,
|
|
464
|
+
schema: JSON.parse(row.schema),
|
|
465
|
+
schemaHash: row.schema_hash,
|
|
466
|
+
createdAt: fromMs(row.created_at)
|
|
467
|
+
};
|
|
468
|
+
if (row.migrates_from)
|
|
469
|
+
t.migratesFrom = row.migrates_from;
|
|
470
|
+
return t;
|
|
471
|
+
};
|
|
472
|
+
var rowToVersion = (row) => {
|
|
473
|
+
const v = {
|
|
474
|
+
version: row.version,
|
|
475
|
+
typeId: row.type_id,
|
|
476
|
+
content: JSON.parse(row.content),
|
|
477
|
+
updatedAt: fromMs(row.updated_at)
|
|
478
|
+
};
|
|
479
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
480
|
+
if (createdBy)
|
|
481
|
+
v.createdBy = createdBy;
|
|
482
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
483
|
+
if (updatedBy)
|
|
484
|
+
v.updatedBy = updatedBy;
|
|
485
|
+
return v;
|
|
486
|
+
};
|
|
487
|
+
var rowToJournalEntry = (row) => {
|
|
488
|
+
const e = {
|
|
489
|
+
seq: row.seq,
|
|
490
|
+
at: fromMs(row.at),
|
|
491
|
+
kind: row.kind,
|
|
492
|
+
ops: JSON.parse(row.ops),
|
|
493
|
+
version: row.version,
|
|
494
|
+
typeId: row.type_id
|
|
495
|
+
};
|
|
496
|
+
if (row.parent_id != null)
|
|
497
|
+
e.parentId = row.parent_id;
|
|
498
|
+
if (row.previous_parent_id != null) {
|
|
499
|
+
e.previousParentId = row.previous_parent_id === "" ? null : row.previous_parent_id;
|
|
500
|
+
}
|
|
501
|
+
if (row.actor != null)
|
|
502
|
+
e.actor = JSON.parse(row.actor);
|
|
503
|
+
if (row.associations != null)
|
|
504
|
+
e.associations = JSON.parse(row.associations);
|
|
505
|
+
return e;
|
|
506
|
+
};
|
|
507
|
+
|
|
350
508
|
// ../sqlite-shared/dist/query.js
|
|
351
|
-
var atBudget = (page,
|
|
509
|
+
var atBudget = (page, rows2) => ({
|
|
352
510
|
sql: page.sql,
|
|
353
|
-
params: [...page.params,
|
|
511
|
+
params: [...page.params, rows2]
|
|
354
512
|
});
|
|
355
|
-
var sqlDirection = (query) =>
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
513
|
+
var sqlDirection = (query) => query.sort.direction === "asc" ? "ASC" : "DESC";
|
|
514
|
+
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`;
|
|
515
|
+
var members = (alias) => `CASE WHEN ${alias}.t = 'object' THEN ${alias}.v ELSE '{}' END`;
|
|
516
|
+
var emptyStatement = () => ({
|
|
517
|
+
tables: [],
|
|
518
|
+
tableParams: [],
|
|
519
|
+
conditions: ["r.id != '_config'"],
|
|
520
|
+
params: []
|
|
521
|
+
});
|
|
522
|
+
var rows = (s, columns, tuples) => {
|
|
523
|
+
const name = `t${s.tables.length}`;
|
|
524
|
+
s.tables.push(`${name} AS MATERIALIZED ${rowsTable(columns)}`);
|
|
525
|
+
s.tableParams.push(JSON.stringify(tuples));
|
|
526
|
+
return name;
|
|
527
|
+
};
|
|
528
|
+
var rowsTable = (columns) => {
|
|
529
|
+
const named = columns.map((column, i) => `json_extract(value, '$[${i}]') AS ${column}`);
|
|
530
|
+
return `(SELECT ${named.join(", ")} FROM json_each(?))`;
|
|
361
531
|
};
|
|
362
|
-
var
|
|
363
|
-
var
|
|
364
|
-
var
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const from = [`json_each(r.content) AS ${key}`, `json_each(${spread(key)}) AS ${value}`];
|
|
368
|
-
const where = [`${key}.key = ?`];
|
|
369
|
-
params.push(segments[0]);
|
|
370
|
-
for (const segment of segments.slice(1)) {
|
|
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 ")})`;
|
|
532
|
+
var CONDITION_COLUMNS = ["scope", "path", "op", "val", "kind"];
|
|
533
|
+
var kindOf = (type) => `CASE ${type} WHEN 'integer' THEN 'number' WHEN 'real' THEN 'number' ELSE ${type} END`;
|
|
534
|
+
var holds = (c, r) => {
|
|
535
|
+
const depth = `json_array_length(${c}.path)`;
|
|
536
|
+
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
537
|
};
|
|
381
|
-
var equalsLeaf = (alias) => `${alias}.type NOT IN ('object', 'array') AND ${alias}.value = ?`;
|
|
382
538
|
var SORT_ALIAS = "cs";
|
|
383
539
|
var cursorMismatch = (cursor, field) => new StackBadRequestError(`Cursor sort field "${cursor.field}" does not match query sort field "${field}"`);
|
|
384
540
|
var nativeCursor = (cursor, field) => {
|
|
@@ -391,37 +547,77 @@ var contentCursor = (cursor, field) => {
|
|
|
391
547
|
throw cursorMismatch(cursor, field);
|
|
392
548
|
return cursor;
|
|
393
549
|
};
|
|
394
|
-
var
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
550
|
+
var valueKind = (value) => {
|
|
551
|
+
if (typeof value === "boolean")
|
|
552
|
+
return `${value}`;
|
|
553
|
+
if (typeof value === "number")
|
|
554
|
+
return "number";
|
|
555
|
+
return typeof value === "string" ? "text" : null;
|
|
556
|
+
};
|
|
557
|
+
var contentRows = (scope, predicate) => {
|
|
558
|
+
const row = (key, op, value = null) => {
|
|
559
|
+
const path = parseContentFilterKey(key);
|
|
560
|
+
return [scope, path, op, value, valueKind(value)];
|
|
561
|
+
};
|
|
562
|
+
return [
|
|
563
|
+
...Object.entries(predicate.content ?? {}).map(([key, value]) => (
|
|
564
|
+
// undefined is the same absence null names — JSON drops it on the way
|
|
565
|
+
// over the wire, so treating it as a value would make an in-process
|
|
566
|
+
// query and its wire equivalent disagree.
|
|
567
|
+
value === null || value === void 0 ? row(key, "isNull") : row(key, "equals", value)
|
|
568
|
+
)),
|
|
569
|
+
...(predicate.contentPresent ?? []).map((key) => row(key, "present"))
|
|
570
|
+
];
|
|
571
|
+
};
|
|
572
|
+
var inList = (s, column, values) => {
|
|
573
|
+
if (values.length === 1) {
|
|
574
|
+
s.params.push(values[0]);
|
|
575
|
+
return `${column} = ?`;
|
|
407
576
|
}
|
|
408
|
-
|
|
409
|
-
|
|
577
|
+
const table = rows(s, ["value"], values.map((value) => [value]));
|
|
578
|
+
return `${column} IN (SELECT value FROM ${table})`;
|
|
579
|
+
};
|
|
580
|
+
var familyRange = (baseId) => [`${baseId}@`, `${baseId}A`];
|
|
581
|
+
var withinFamily = (column, lo, hi) => `(${column} >= ${lo} AND ${column} < ${hi})`;
|
|
582
|
+
var inFamily = (column, baseId) => ({
|
|
583
|
+
sql: withinFamily(column, "?", "?"),
|
|
584
|
+
params: familyRange(baseId)
|
|
585
|
+
});
|
|
586
|
+
var inFamilies = (s, record, baseIds) => {
|
|
587
|
+
if (baseIds.length === 1) {
|
|
588
|
+
const one = inFamily(`${record}.type_id`, baseIds[0]);
|
|
589
|
+
s.params.push(...one.params);
|
|
590
|
+
return one.sql;
|
|
591
|
+
}
|
|
592
|
+
const table = rows(s, ["lo", "hi"], baseIds.map(familyRange));
|
|
593
|
+
return `${record}.id IN (SELECT fr.id FROM ${table} f JOIN records fr ON ${withinFamily("fr.type_id", "f.lo", "f.hi")})`;
|
|
594
|
+
};
|
|
595
|
+
var carriesPermissions = (s, sets) => {
|
|
596
|
+
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)])));
|
|
597
|
+
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))`;
|
|
598
|
+
};
|
|
599
|
+
var reachConditions = (s, reach) => {
|
|
600
|
+
if (reach.fence)
|
|
601
|
+
s.conditions.push(inFamilies(s, "r", reach.fence));
|
|
602
|
+
const within = reach.within;
|
|
603
|
+
if (within) {
|
|
604
|
+
const arms = [inFamilies(s, "r", within.baseIds)];
|
|
605
|
+
if (within.authored) {
|
|
606
|
+
s.params.push(within.authored.subjectId);
|
|
607
|
+
arms.push(`(r.entity_id = ? AND ${inFamilies(s, "r", within.authored.baseIds)})`);
|
|
608
|
+
}
|
|
609
|
+
arms.push(carriesPermissions(s, within.permissions));
|
|
610
|
+
s.conditions.push(`(${arms.join(" OR ")})`);
|
|
410
611
|
}
|
|
411
|
-
return conditions;
|
|
412
612
|
};
|
|
413
|
-
var recordConditions = (query) => {
|
|
414
|
-
const
|
|
415
|
-
const params =
|
|
613
|
+
var recordConditions = (query, reach) => {
|
|
614
|
+
const s = emptyStatement();
|
|
615
|
+
const { conditions, params } = s;
|
|
416
616
|
const f = query.filter ?? {};
|
|
417
|
-
let aliasSeq = 0;
|
|
418
|
-
const nextContentAlias = () => `cp${aliasSeq++}`;
|
|
419
617
|
const inFilter = (column, value) => {
|
|
420
618
|
if (value === void 0)
|
|
421
619
|
return;
|
|
422
|
-
|
|
423
|
-
conditions.push(`r.${column} IN (${values.map(() => "?").join(",")})`);
|
|
424
|
-
params.push(...values);
|
|
620
|
+
conditions.push(inList(s, `r.${column}`, Array.isArray(value) ? value : [value]));
|
|
425
621
|
};
|
|
426
622
|
const rangeFilter = (column, range) => {
|
|
427
623
|
if (range?.after) {
|
|
@@ -439,7 +635,12 @@ var recordConditions = (query) => {
|
|
|
439
635
|
if (!f.includeUnlisted) {
|
|
440
636
|
conditions.push("r.unlisted_at IS NULL");
|
|
441
637
|
}
|
|
638
|
+
if (reach)
|
|
639
|
+
reachConditions(s, reach);
|
|
442
640
|
inFilter("type_id", f.typeId);
|
|
641
|
+
if (f.baseId !== void 0) {
|
|
642
|
+
conditions.push(inFamilies(s, "r", Array.isArray(f.baseId) ? f.baseId : [f.baseId]));
|
|
643
|
+
}
|
|
443
644
|
if (f.parentId !== void 0) {
|
|
444
645
|
if (f.parentId === null) {
|
|
445
646
|
conditions.push("r.parent_id IS NULL");
|
|
@@ -455,10 +656,10 @@ var recordConditions = (query) => {
|
|
|
455
656
|
rangeFilter("updated_at", f.updatedAt);
|
|
456
657
|
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
658
|
if (f.tags?.length) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
659
|
+
const tags = [...new Set(f.tags)];
|
|
660
|
+
const label = inList(s, "a.label", tags);
|
|
661
|
+
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) = ?)`);
|
|
662
|
+
params.push(tags.length);
|
|
462
663
|
}
|
|
463
664
|
if (f.attachment) {
|
|
464
665
|
const clauses = [];
|
|
@@ -504,15 +705,17 @@ var recordConditions = (query) => {
|
|
|
504
705
|
}
|
|
505
706
|
conditions.push(hasAssociation("relationship", clauses));
|
|
506
707
|
}
|
|
507
|
-
|
|
508
|
-
conditions.push(...contentConditions(f, params, nextContentAlias));
|
|
509
|
-
}
|
|
708
|
+
const content = contentRows(null, f);
|
|
510
709
|
if (f.contentByType) {
|
|
511
|
-
const arms = Object.entries(f.contentByType)
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
710
|
+
const arms = Object.entries(f.contentByType);
|
|
711
|
+
const typeIds = arms.map(([typeId]) => typeId);
|
|
712
|
+
conditions.push(inList(s, "r.type_id", typeIds));
|
|
713
|
+
for (const [typeId, arm] of arms)
|
|
714
|
+
content.push(...contentRows(typeId, arm));
|
|
715
|
+
}
|
|
716
|
+
if (content.length > 0) {
|
|
717
|
+
const table = rows(s, CONDITION_COLUMNS, content);
|
|
718
|
+
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
719
|
}
|
|
517
720
|
if (f.search) {
|
|
518
721
|
const sanitized = sanitizeFts5Query(f.search);
|
|
@@ -523,7 +726,7 @@ var recordConditions = (query) => {
|
|
|
523
726
|
conditions.push("0");
|
|
524
727
|
}
|
|
525
728
|
}
|
|
526
|
-
return
|
|
729
|
+
return s;
|
|
527
730
|
};
|
|
528
731
|
var presentCursorCondition = (cursor, op, params) => {
|
|
529
732
|
if (cursor.kind === "num") {
|
|
@@ -534,56 +737,54 @@ var presentCursorCondition = (cursor, op, params) => {
|
|
|
534
737
|
params.push(key, key, cursor.value, cursor.value, cursor.id);
|
|
535
738
|
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
739
|
};
|
|
537
|
-
var statement = (
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
740
|
+
var statement = (s, from, tail) => {
|
|
741
|
+
const select = `SELECT r.* FROM ${from} WHERE ${s.conditions.join(" AND ")}${tail}`;
|
|
742
|
+
const sql = s.tables.length === 0 ? select : `WITH ${s.tables.join(", ")} ${select}`;
|
|
743
|
+
return { sql, params: [...s.tableParams, ...s.params] };
|
|
744
|
+
};
|
|
745
|
+
var buildQueryPlan = (query, reach) => {
|
|
542
746
|
const dir = sqlDirection(query);
|
|
543
747
|
const op = dir === "ASC" ? ">" : "<";
|
|
544
748
|
const decoded = query.cursor ? decodeCursor(query.cursor) : null;
|
|
545
|
-
if (query.sort
|
|
546
|
-
return byTypePlan(query, query.sort.contentFieldByType, dir, op, decoded);
|
|
749
|
+
if (query.sort.contentFieldByType) {
|
|
750
|
+
return byTypePlan(query, reach, query.sort.contentFieldByType, dir, op, decoded);
|
|
547
751
|
}
|
|
548
|
-
const contentField = query.sort
|
|
549
|
-
if (
|
|
550
|
-
const
|
|
551
|
-
const field = getSortField(query);
|
|
752
|
+
const { contentField, field } = query.sort;
|
|
753
|
+
if (field !== void 0) {
|
|
754
|
+
const where = recordConditions(query, reach);
|
|
552
755
|
const col = getSortColumn(field);
|
|
553
756
|
if (decoded) {
|
|
554
757
|
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);
|
|
758
|
+
where.conditions.push(`(r.${col} ${op} ? OR (r.${col} = ? AND r.id ${op} ?))`);
|
|
759
|
+
where.params.push(cursor2.value, cursor2.value, cursor2.id);
|
|
557
760
|
}
|
|
558
|
-
return [
|
|
559
|
-
statement("r.*", "records r", conditions, params, ` ORDER BY r.${col} ${dir}, r.id ${dir} LIMIT ?`)
|
|
560
|
-
];
|
|
761
|
+
return [statement(where, "records r", ` ORDER BY r.${col} ${dir}, r.id ${dir} LIMIT ?`)];
|
|
561
762
|
}
|
|
562
763
|
const cursor = decoded ? contentCursor(decoded, contentField) : null;
|
|
563
764
|
const pages = [];
|
|
564
765
|
if (cursor?.kind !== "absent") {
|
|
565
|
-
const
|
|
766
|
+
const where = recordConditions(query, reach);
|
|
566
767
|
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);
|
|
768
|
+
where.conditions.push(`${SORT_ALIAS}.field = ?`);
|
|
769
|
+
where.params.push(contentField);
|
|
569
770
|
if (cursor)
|
|
570
|
-
conditions.push(presentCursorCondition(cursor, op, params));
|
|
771
|
+
where.conditions.push(presentCursorCondition(cursor, op, where.params));
|
|
571
772
|
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(
|
|
773
|
+
pages.push(statement(where, from, ` ORDER BY ${order} LIMIT ?`));
|
|
573
774
|
}
|
|
574
775
|
{
|
|
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);
|
|
776
|
+
const where = recordConditions(query, reach);
|
|
777
|
+
where.conditions.push(`NOT EXISTS (SELECT 1 FROM content_index ${SORT_ALIAS} WHERE ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = ?)`);
|
|
778
|
+
where.params.push(contentField);
|
|
578
779
|
if (cursor?.kind === "absent") {
|
|
579
|
-
conditions.push(`r.id ${op} ?`);
|
|
580
|
-
params.push(cursor.id);
|
|
780
|
+
where.conditions.push(`r.id ${op} ?`);
|
|
781
|
+
where.params.push(cursor.id);
|
|
581
782
|
}
|
|
582
|
-
pages.push(statement(
|
|
783
|
+
pages.push(statement(where, "records r", ` ORDER BY r.id ${dir} LIMIT ?`));
|
|
583
784
|
}
|
|
584
785
|
return pages;
|
|
585
786
|
};
|
|
586
|
-
var byTypePlan = (query, byType, dir, op, decoded) => {
|
|
787
|
+
var byTypePlan = (query, reach, byType, dir, op, decoded) => {
|
|
587
788
|
const cursor = decoded ? contentCursor(decoded, byTypeDescriptor(byType)) : null;
|
|
588
789
|
const fields = [];
|
|
589
790
|
const constants = [];
|
|
@@ -597,42 +798,38 @@ var byTypePlan = (query, byType, dir, op, decoded) => {
|
|
|
597
798
|
constants.push([typeId, entry]);
|
|
598
799
|
}
|
|
599
800
|
const pages = [];
|
|
600
|
-
const fieldOf = (alias, record) => fields.map(() => `(${record}.type_id = ? AND ${alias}.field = ?)`).join(" OR ");
|
|
601
801
|
if (cursor?.kind !== "absent" && fields.length + constants.length > 0) {
|
|
802
|
+
const where = recordConditions(query, reach);
|
|
602
803
|
const sources = [];
|
|
603
|
-
const sourceParams = [];
|
|
604
804
|
if (fields.length > 0) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
sourceParams.push(typeId, field);
|
|
805
|
+
const byField = rows(where, ["type_id", "field"], fields);
|
|
806
|
+
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
807
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
808
|
+
if (constants.length > 0) {
|
|
809
|
+
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]));
|
|
810
|
+
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
811
|
}
|
|
613
|
-
const { conditions, params } = recordConditions(query);
|
|
614
812
|
if (cursor)
|
|
615
|
-
conditions.push(presentCursorCondition(cursor, op, params));
|
|
813
|
+
where.conditions.push(presentCursorCondition(cursor, op, where.params));
|
|
616
814
|
const from = `(${sources.join(" UNION ALL ")}) ${SORT_ALIAS} JOIN records r ON r.id = ${SORT_ALIAS}.record_id`;
|
|
617
815
|
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(
|
|
816
|
+
pages.push(statement(where, from, ` ORDER BY ${order} LIMIT ?`));
|
|
619
817
|
}
|
|
620
818
|
{
|
|
621
|
-
const
|
|
819
|
+
const where = recordConditions(query, reach);
|
|
622
820
|
if (fields.length > 0) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
params.push(typeId, field);
|
|
821
|
+
const byField = rows(where, ["type_id", "field"], fields);
|
|
822
|
+
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
823
|
}
|
|
627
824
|
if (constants.length > 0) {
|
|
628
|
-
|
|
629
|
-
|
|
825
|
+
const typeIds = constants.map(([typeId]) => typeId);
|
|
826
|
+
where.conditions.push(`NOT (${inList(where, "r.type_id", typeIds)})`);
|
|
630
827
|
}
|
|
631
828
|
if (cursor?.kind === "absent") {
|
|
632
|
-
conditions.push(`r.id ${op} ?`);
|
|
633
|
-
params.push(cursor.id);
|
|
829
|
+
where.conditions.push(`r.id ${op} ?`);
|
|
830
|
+
where.params.push(cursor.id);
|
|
634
831
|
}
|
|
635
|
-
pages.push(statement(
|
|
832
|
+
pages.push(statement(where, "records r", ` ORDER BY r.id ${dir} LIMIT ?`));
|
|
636
833
|
}
|
|
637
834
|
return pages;
|
|
638
835
|
};
|
|
@@ -725,154 +922,6 @@ var ContentIndex = class {
|
|
|
725
922
|
return fields;
|
|
726
923
|
}
|
|
727
924
|
};
|
|
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
925
|
|
|
877
926
|
// ../sqlite-shared/dist/record-logic.js
|
|
878
927
|
var asStackBadRequestError = (query, run) => {
|
|
@@ -931,6 +980,18 @@ var SharedSqlRecordLogic = class {
|
|
|
931
980
|
throw recordNotFound(id);
|
|
932
981
|
throw versionConflict(id, expectedVersion, version);
|
|
933
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* An aspect write's record, inside its transaction: missing is not found,
|
|
985
|
+
* a tombstone is refused, and anything else answers its version.
|
|
986
|
+
*/
|
|
987
|
+
liveVersion(id) {
|
|
988
|
+
const row = this.exec.get("SELECT version, deleted_at FROM records WHERE id = ?", [id]);
|
|
989
|
+
if (!row)
|
|
990
|
+
throw recordNotFound(id);
|
|
991
|
+
if (row.deleted_at !== null)
|
|
992
|
+
throw softDeletedRefusal(id);
|
|
993
|
+
return row.version;
|
|
994
|
+
}
|
|
934
995
|
currentVersion(id) {
|
|
935
996
|
return this.exec.get("SELECT version FROM records WHERE id = ?", [id])?.version;
|
|
936
997
|
}
|
|
@@ -1014,9 +1075,10 @@ var SharedSqlRecordLogic = class {
|
|
|
1014
1075
|
}
|
|
1015
1076
|
throw err;
|
|
1016
1077
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1078
|
+
if (record.associations?.length)
|
|
1079
|
+
this.insertAssociations(record.id, record.associations);
|
|
1080
|
+
if (record.permissions?.length)
|
|
1081
|
+
this.insertPermissions(record.id, record.permissions);
|
|
1020
1082
|
this.searchIndex.insert(record.id, JSON.stringify(record.content));
|
|
1021
1083
|
this.contentIndex.sync(record.id, record.typeId, record.content);
|
|
1022
1084
|
this.appendJournal(record.id, opts.journal);
|
|
@@ -1034,56 +1096,55 @@ var SharedSqlRecordLogic = class {
|
|
|
1034
1096
|
*/
|
|
1035
1097
|
readRecord(id) {
|
|
1036
1098
|
const row = this.exec.get("SELECT * FROM records WHERE id = ?", [id]);
|
|
1037
|
-
return row ? this.hydrate(row) : null;
|
|
1099
|
+
return row ? this.hydrate([row])[0] : null;
|
|
1038
1100
|
}
|
|
1039
|
-
/**
|
|
1040
|
-
|
|
1041
|
-
|
|
1101
|
+
/**
|
|
1102
|
+
* Records rows with their associations and permissions attached: one
|
|
1103
|
+
* statement per element table for the whole list, so a page costs three
|
|
1104
|
+
* reads however many records it holds.
|
|
1105
|
+
*/
|
|
1106
|
+
hydrate(rows2) {
|
|
1107
|
+
const ids = rows2.map((row) => row.id);
|
|
1108
|
+
const associations = this.elementsByRecord("associations", ids, rowToAssociation);
|
|
1109
|
+
const permissions = this.elementsByRecord("permissions", ids, rowToPermission);
|
|
1110
|
+
return rows2.map((row) => rowToRecord(row, associations.get(row.id) ?? [], permissions.get(row.id) ?? []));
|
|
1042
1111
|
}
|
|
1043
1112
|
/**
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1113
|
+
* The patch applies to the content as it stands when the write lands,
|
|
1114
|
+
* read inside the same transaction.
|
|
1046
1115
|
*/
|
|
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
|
-
}
|
|
1116
|
+
async patchContent(id, patch, opts = {}) {
|
|
1059
1117
|
this.exec.transaction(() => {
|
|
1060
1118
|
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
1119
|
if (opts.snapshot)
|
|
1075
1120
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1121
|
+
this.rewriteContent(id, current.typeId, applyMergePatch(current.content, patch), opts);
|
|
1122
|
+
this.appendJournal(id, opts.journal);
|
|
1123
|
+
});
|
|
1124
|
+
return this.reread(id, "patchContent");
|
|
1125
|
+
}
|
|
1126
|
+
async setParent(id, parentId, opts = {}) {
|
|
1127
|
+
return this.aspectUpdate(id, "parent_id = ?", parentId, opts, "setParent");
|
|
1128
|
+
}
|
|
1129
|
+
/** Stamped fresh: `Stack` hands this only a transition. */
|
|
1130
|
+
async setUnlisted(id, unlisted, opts = {}) {
|
|
1131
|
+
return this.aspectUpdate(id, "unlisted_at = ?", unlisted ? toMs(/* @__PURE__ */ new Date()) : null, opts, "setUnlisted");
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* One records column that never bumps. The tombstone refusal and the
|
|
1135
|
+
* `ifVersion` guard ride in the WHERE clause, as on a versioned update.
|
|
1136
|
+
*/
|
|
1137
|
+
aspectUpdate(id, set, value, opts, verb) {
|
|
1138
|
+
const guarded = opts.ifVersion !== void 0;
|
|
1139
|
+
this.exec.transaction(() => {
|
|
1140
|
+
const changed = this.exec.run(`UPDATE records SET ${set}
|
|
1141
|
+
WHERE id = ? AND deleted_at IS NULL${guarded ? " AND version = ?" : ""}`, [value, id, ...guarded ? [opts.ifVersion] : []]);
|
|
1142
|
+
if (changed === 0) {
|
|
1143
|
+
throw versionConflict(id, opts.ifVersion, this.liveVersion(id));
|
|
1083
1144
|
}
|
|
1084
1145
|
this.appendJournal(id, opts.journal);
|
|
1085
1146
|
});
|
|
1086
|
-
return this.reread(id,
|
|
1147
|
+
return this.reread(id, verb);
|
|
1087
1148
|
}
|
|
1088
1149
|
async deleteRecord(id, opts = {}) {
|
|
1089
1150
|
if (opts.purge) {
|
|
@@ -1113,6 +1174,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1113
1174
|
this.checkExpectedVersion(id, purged.version, ifVersion);
|
|
1114
1175
|
this.searchIndex.remove(id);
|
|
1115
1176
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
1177
|
+
this.exec.run("DELETE FROM permissions WHERE record_id = ?", [id]);
|
|
1116
1178
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
1117
1179
|
this.exec.run("DELETE FROM journal WHERE record_id = ?", [id]);
|
|
1118
1180
|
this.contentIndex.remove(id);
|
|
@@ -1163,20 +1225,27 @@ var SharedSqlRecordLogic = class {
|
|
|
1163
1225
|
this.searchIndex.insert(id, json);
|
|
1164
1226
|
this.contentIndex.sync(id, typeId, content);
|
|
1165
1227
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1228
|
+
queryRecords(query) {
|
|
1229
|
+
return this.page(query);
|
|
1230
|
+
}
|
|
1231
|
+
queryReadable(query, reach) {
|
|
1232
|
+
return this.page(query, reach);
|
|
1233
|
+
}
|
|
1234
|
+
/** `reach`, when given, is one more condition of every statement in the plan. */
|
|
1235
|
+
async page(query, reach) {
|
|
1236
|
+
const { limit } = query;
|
|
1168
1237
|
const fetch = limit + 1;
|
|
1169
|
-
const
|
|
1170
|
-
for (const page2 of buildQueryPlan(query)) {
|
|
1171
|
-
const budget = fetch -
|
|
1238
|
+
const rows2 = [];
|
|
1239
|
+
for (const page2 of buildQueryPlan(query, reach)) {
|
|
1240
|
+
const budget = fetch - rows2.length;
|
|
1172
1241
|
if (budget <= 0)
|
|
1173
1242
|
break;
|
|
1174
1243
|
const statement2 = atBudget(page2, budget);
|
|
1175
|
-
|
|
1244
|
+
rows2.push(...asStackBadRequestError(query, () => this.exec.all(statement2.sql, statement2.params)));
|
|
1176
1245
|
}
|
|
1177
|
-
const hasMore =
|
|
1178
|
-
const page = hasMore ?
|
|
1179
|
-
const records =
|
|
1246
|
+
const hasMore = rows2.length > limit;
|
|
1247
|
+
const page = hasMore ? rows2.slice(0, limit) : rows2;
|
|
1248
|
+
const records = this.hydrate(page);
|
|
1180
1249
|
const lastRecord = records[records.length - 1];
|
|
1181
1250
|
const cursor = hasMore && lastRecord ? makeCursor(lastRecord, query.sort, (record, field) => this.contentIndex.kindOf(record.typeId, field)) : null;
|
|
1182
1251
|
return { records, cursor };
|
|
@@ -1185,7 +1254,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1185
1254
|
* The reference check and the purges share one synchronous transaction,
|
|
1186
1255
|
* so nothing can add a reference between the two.
|
|
1187
1256
|
*/
|
|
1188
|
-
async deleteUnreferencedAttachmentRecords(fileId,
|
|
1257
|
+
async deleteUnreferencedAttachmentRecords(fileId, metadataBaseId) {
|
|
1189
1258
|
return this.exec.transaction(() => {
|
|
1190
1259
|
const referenced = this.exec.all(`SELECT 1 as found FROM associations WHERE kind = 'attachment' AND file_id = ?
|
|
1191
1260
|
UNION ALL
|
|
@@ -1194,11 +1263,9 @@ var SharedSqlRecordLogic = class {
|
|
|
1194
1263
|
if (referenced.length) {
|
|
1195
1264
|
throw new StackConflictError("Attachment is still referenced by one or more records");
|
|
1196
1265
|
}
|
|
1197
|
-
|
|
1198
|
-
return [];
|
|
1199
|
-
const placeholders = metadataTypeIds.map(() => "?").join(", ");
|
|
1266
|
+
const family = inFamily("type_id", metadataBaseId);
|
|
1200
1267
|
const metaRows = this.exec.all(`SELECT id FROM records
|
|
1201
|
-
WHERE
|
|
1268
|
+
WHERE ${family.sql} AND json_extract(content, '$.fileId') = ?`, [...family.params, fileId]);
|
|
1202
1269
|
const deleted = [];
|
|
1203
1270
|
for (const row of metaRows) {
|
|
1204
1271
|
const purged = this.purgeRecord(row.id);
|
|
@@ -1218,8 +1285,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1218
1285
|
conditions.push("version < ?");
|
|
1219
1286
|
values.push(query.beforeVersion);
|
|
1220
1287
|
}
|
|
1221
|
-
const
|
|
1222
|
-
return
|
|
1288
|
+
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);
|
|
1289
|
+
return rows2.map(rowToVersion);
|
|
1223
1290
|
}
|
|
1224
1291
|
async getVersion(id, version) {
|
|
1225
1292
|
return this.readVersion(id, version);
|
|
@@ -1329,8 +1396,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1329
1396
|
conditions.push("seq > ?");
|
|
1330
1397
|
values.push(query.afterSeq);
|
|
1331
1398
|
}
|
|
1332
|
-
const
|
|
1333
|
-
return
|
|
1399
|
+
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);
|
|
1400
|
+
return rows2.map(rowToJournalEntry);
|
|
1334
1401
|
}
|
|
1335
1402
|
// -------------------------------------------------------
|
|
1336
1403
|
// Types
|
|
@@ -1355,66 +1422,71 @@ var SharedSqlRecordLogic = class {
|
|
|
1355
1422
|
return row ? rowToType(row) : null;
|
|
1356
1423
|
}
|
|
1357
1424
|
async listTypes() {
|
|
1358
|
-
const
|
|
1359
|
-
return
|
|
1425
|
+
const rows2 = this.exec.all("SELECT * FROM types ORDER BY base_id, version");
|
|
1426
|
+
return rows2.map(rowToType);
|
|
1360
1427
|
}
|
|
1361
1428
|
// -------------------------------------------------------
|
|
1362
1429
|
// Associations
|
|
1363
1430
|
// -------------------------------------------------------
|
|
1364
1431
|
/**
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1432
|
+
* A write to one of a record's two element tables, inside a transaction
|
|
1433
|
+
* that first checks the record exists, is not soft-deleted, is at
|
|
1434
|
+
* `ifVersion`, and that the table holds `expectSet`. Never bumps and
|
|
1435
|
+
* never snapshots.
|
|
1368
1436
|
*/
|
|
1369
|
-
|
|
1370
|
-
assertOneSurface(changes);
|
|
1437
|
+
elementWrite(id, table, opts, write, verb) {
|
|
1371
1438
|
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)]);
|
|
1439
|
+
this.checkExpectedVersion(id, this.liveVersion(id), opts.ifVersion);
|
|
1440
|
+
if (opts.expectSet) {
|
|
1441
|
+
const map = table === "associations" ? rowToAssociation : rowToPermission;
|
|
1442
|
+
const stored = this.elementsByRecord(table, [id], map).get(id) ?? [];
|
|
1443
|
+
if (!sameAssociationSet(stored, opts.expectSet))
|
|
1444
|
+
throw new ElementSetMovedError(id);
|
|
1388
1445
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
this.insertAssociations(recordId, adds);
|
|
1392
|
-
this.appendJournal(recordId, opts.journal);
|
|
1446
|
+
write();
|
|
1447
|
+
this.appendJournal(id, opts.journal);
|
|
1393
1448
|
});
|
|
1394
|
-
return this.reread(
|
|
1449
|
+
return this.reread(id, verb);
|
|
1395
1450
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1451
|
+
async editAssociations(id, edits, opts = {}) {
|
|
1452
|
+
return this.elementWrite(id, "associations", opts, () => {
|
|
1453
|
+
for (const edit of edits) {
|
|
1454
|
+
if (edit.op !== "remove")
|
|
1455
|
+
continue;
|
|
1456
|
+
const association = edit.association;
|
|
1457
|
+
this.exec.run(`DELETE FROM associations
|
|
1458
|
+
WHERE record_id = ? AND kind = ? AND label = ?
|
|
1459
|
+
AND file_id = ? AND related_scope = ? AND related_id = ?
|
|
1460
|
+
AND related_ns = ? AND related_stack = ?`, [id, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1461
|
+
}
|
|
1462
|
+
this.insertAssociations(id, addsOf(edits));
|
|
1463
|
+
}, "editAssociations");
|
|
1464
|
+
}
|
|
1465
|
+
async setAssociations(id, associations, opts = {}) {
|
|
1466
|
+
return this.elementWrite(id, "associations", opts, () => {
|
|
1467
|
+
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
1468
|
+
this.insertAssociations(id, associations);
|
|
1469
|
+
}, "setAssociations");
|
|
1470
|
+
}
|
|
1471
|
+
/** editAssociations() over the permissions table. */
|
|
1472
|
+
async editAccess(id, edits, opts = {}) {
|
|
1473
|
+
return this.elementWrite(id, "permissions", opts, () => {
|
|
1474
|
+
for (const edit of edits) {
|
|
1475
|
+
if (edit.op !== "remove")
|
|
1476
|
+
continue;
|
|
1477
|
+
const permission = edit.association;
|
|
1478
|
+
this.exec.run(`DELETE FROM permissions
|
|
1479
|
+
WHERE record_id = ? AND kind = ? AND label = ?
|
|
1480
|
+
AND grantee_kind = ? AND grantee_id = ? AND role = ?`, [id, permission.kind, permission.label, ...permissionKeyColumns(permission)]);
|
|
1481
|
+
}
|
|
1482
|
+
this.insertPermissions(id, addsOf(edits));
|
|
1483
|
+
}, "editAccess");
|
|
1408
1484
|
}
|
|
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);
|
|
1485
|
+
async setAccess(id, permissions, opts = {}) {
|
|
1486
|
+
return this.elementWrite(id, "permissions", opts, () => {
|
|
1487
|
+
this.exec.run("DELETE FROM permissions WHERE record_id = ?", [id]);
|
|
1488
|
+
this.insertPermissions(id, permissions);
|
|
1489
|
+
}, "setAccess");
|
|
1418
1490
|
}
|
|
1419
1491
|
/**
|
|
1420
1492
|
* Upserts on association identity, so re-pointing `attachment_record_id`
|
|
@@ -1423,46 +1495,72 @@ var SharedSqlRecordLogic = class {
|
|
|
1423
1495
|
*/
|
|
1424
1496
|
insertAssociations(recordId, associations) {
|
|
1425
1497
|
for (const assoc of associations) {
|
|
1426
|
-
|
|
1427
|
-
this.exec.run(`INSERT INTO associations
|
|
1498
|
+
this.insertRow(recordId, () => this.exec.run(`INSERT INTO associations
|
|
1428
1499
|
(record_id, kind, label, file_id, related_scope, related_id, related_ns,
|
|
1429
|
-
related_stack,
|
|
1430
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
1500
|
+
related_stack, attachment_record_id)
|
|
1501
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1431
1502
|
ON CONFLICT DO UPDATE SET attachment_record_id = excluded.attachment_record_id`, [
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
} catch (err) {
|
|
1439
|
-
if (isForeignKeyViolation(err)) {
|
|
1440
|
-
throw recordNotFound(recordId);
|
|
1441
|
-
}
|
|
1442
|
-
throw err;
|
|
1443
|
-
}
|
|
1503
|
+
recordId,
|
|
1504
|
+
assoc.kind,
|
|
1505
|
+
assoc.label,
|
|
1506
|
+
...associationKeyColumns(assoc),
|
|
1507
|
+
assoc.kind === "attachment" ? assoc.attachmentRecordId ?? "" : ""
|
|
1508
|
+
]));
|
|
1444
1509
|
}
|
|
1445
1510
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1511
|
+
/** Every column is in the key, so a repeated element is the same row. */
|
|
1512
|
+
insertPermissions(recordId, permissions) {
|
|
1513
|
+
for (const p of permissions) {
|
|
1514
|
+
this.insertRow(recordId, () => this.exec.run(`INSERT INTO permissions (record_id, kind, label, grantee_kind, grantee_id, role)
|
|
1515
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1516
|
+
ON CONFLICT DO NOTHING`, [recordId, p.kind, p.label, ...permissionKeyColumns(p)]));
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
insertRow(recordId, insert) {
|
|
1520
|
+
try {
|
|
1521
|
+
insert();
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
if (isForeignKeyViolation(err))
|
|
1524
|
+
throw recordNotFound(recordId);
|
|
1525
|
+
throw err;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* One element table's rows for every record in `ids`, in primary-key
|
|
1530
|
+
* order — the order a single record's read has always had.
|
|
1531
|
+
*/
|
|
1532
|
+
elementsByRecord(table, ids, map) {
|
|
1533
|
+
const byRecord = /* @__PURE__ */ new Map();
|
|
1534
|
+
const rows2 = this.exec.all(`WITH ids AS MATERIALIZED ${rowsTable(["id"])}
|
|
1535
|
+
SELECT * FROM ${table} WHERE record_id IN (SELECT id FROM ids)
|
|
1536
|
+
ORDER BY ${table === "associations" ? ASSOCIATION_KEY : PERMISSION_KEY}`, [JSON.stringify(ids.map((id) => [id]))]);
|
|
1537
|
+
for (const row of rows2) {
|
|
1538
|
+
const id = row.record_id;
|
|
1539
|
+
const list = byRecord.get(id);
|
|
1540
|
+
if (list)
|
|
1541
|
+
list.push(map(row));
|
|
1542
|
+
else
|
|
1543
|
+
byRecord.set(id, [map(row)]);
|
|
1544
|
+
}
|
|
1545
|
+
return byRecord;
|
|
1449
1546
|
}
|
|
1450
1547
|
};
|
|
1548
|
+
var ASSOCIATION_KEY = "record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack";
|
|
1549
|
+
var PERMISSION_KEY = "record_id, kind, label, grantee_kind, grantee_id, role";
|
|
1550
|
+
var addsOf = (edits) => edits.flatMap((e) => e.op === "add" ? [e.association] : []);
|
|
1451
1551
|
|
|
1452
1552
|
// ../sqlite-shared/dist/record-adapter.js
|
|
1453
1553
|
var SQLITE_RECORD_CAPABILITIES = {
|
|
1454
1554
|
filter: {
|
|
1455
1555
|
content: "path",
|
|
1456
1556
|
contentPresent: true,
|
|
1457
|
-
|
|
1458
|
-
// docs/spec/adapters.md § Adapter capabilities.
|
|
1459
|
-
contentByType: 64,
|
|
1557
|
+
contentByType: true,
|
|
1460
1558
|
search: true
|
|
1461
1559
|
},
|
|
1462
1560
|
sort: {
|
|
1463
1561
|
fields: ["createdAt", "updatedAt", "version"],
|
|
1464
1562
|
contentField: true,
|
|
1465
|
-
contentFieldByType:
|
|
1563
|
+
contentFieldByType: true
|
|
1466
1564
|
},
|
|
1467
1565
|
limits: {
|
|
1468
1566
|
attachmentBytes: null,
|
|
@@ -1488,8 +1586,14 @@ var SharedSqlRecordAdapter = class {
|
|
|
1488
1586
|
getRecord(id) {
|
|
1489
1587
|
return this.record.getRecord(id);
|
|
1490
1588
|
}
|
|
1491
|
-
|
|
1492
|
-
return this.record.
|
|
1589
|
+
patchContent(id, patch, opts) {
|
|
1590
|
+
return this.record.patchContent(id, patch, opts);
|
|
1591
|
+
}
|
|
1592
|
+
setParent(id, parentId, opts) {
|
|
1593
|
+
return this.record.setParent(id, parentId, opts);
|
|
1594
|
+
}
|
|
1595
|
+
setUnlisted(id, unlisted, opts) {
|
|
1596
|
+
return this.record.setUnlisted(id, unlisted, opts);
|
|
1493
1597
|
}
|
|
1494
1598
|
deleteRecord(id, opts) {
|
|
1495
1599
|
return this.record.deleteRecord(id, opts);
|
|
@@ -1506,8 +1610,11 @@ var SharedSqlRecordAdapter = class {
|
|
|
1506
1610
|
queryRecords(query) {
|
|
1507
1611
|
return this.record.queryRecords(query);
|
|
1508
1612
|
}
|
|
1509
|
-
|
|
1510
|
-
return this.record.
|
|
1613
|
+
queryReadable(query, reach) {
|
|
1614
|
+
return this.record.queryReadable(query, reach);
|
|
1615
|
+
}
|
|
1616
|
+
deleteUnreferencedAttachmentRecords(fileId, metadataBaseId) {
|
|
1617
|
+
return this.record.deleteUnreferencedAttachmentRecords(fileId, metadataBaseId);
|
|
1511
1618
|
}
|
|
1512
1619
|
// -------------------------------------------------------
|
|
1513
1620
|
// Versions
|
|
@@ -1540,10 +1647,19 @@ var SharedSqlRecordAdapter = class {
|
|
|
1540
1647
|
return this.record.listTypes();
|
|
1541
1648
|
}
|
|
1542
1649
|
// -------------------------------------------------------
|
|
1543
|
-
// Associations
|
|
1650
|
+
// Associations and permissions
|
|
1544
1651
|
// -------------------------------------------------------
|
|
1545
|
-
|
|
1546
|
-
return this.record.
|
|
1652
|
+
editAssociations(id, edits, opts) {
|
|
1653
|
+
return this.record.editAssociations(id, edits, opts);
|
|
1654
|
+
}
|
|
1655
|
+
setAssociations(id, associations, opts) {
|
|
1656
|
+
return this.record.setAssociations(id, associations, opts);
|
|
1657
|
+
}
|
|
1658
|
+
editAccess(id, edits, opts) {
|
|
1659
|
+
return this.record.editAccess(id, edits, opts);
|
|
1660
|
+
}
|
|
1661
|
+
setAccess(id, permissions, opts) {
|
|
1662
|
+
return this.record.setAccess(id, permissions, opts);
|
|
1547
1663
|
}
|
|
1548
1664
|
};
|
|
1549
1665
|
|
|
@@ -1564,8 +1680,8 @@ var DurableObjectSqliteExecutor = class {
|
|
|
1564
1680
|
return cursor.rowsWritten;
|
|
1565
1681
|
}
|
|
1566
1682
|
get(sql, params = []) {
|
|
1567
|
-
const
|
|
1568
|
-
return
|
|
1683
|
+
const rows2 = this.sql.exec(sql, ...params).toArray();
|
|
1684
|
+
return rows2[0];
|
|
1569
1685
|
}
|
|
1570
1686
|
all(sql, params = []) {
|
|
1571
1687
|
return this.sql.exec(sql, ...params).toArray();
|