@haverstack/record-adapter-sqlite 0.18.0 → 0.20.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 +4 -0
- package/dist/index.js +236 -205
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -32,6 +32,10 @@ var RECORD_SCHEMA_SQL = `
|
|
|
32
32
|
-- qualifier: related_stack for a record in another stack, related_ns for
|
|
33
33
|
-- a foreign namespace. All four are in the primary key, so two targets
|
|
34
34
|
-- differing only by namespace are two associations.
|
|
35
|
+
--
|
|
36
|
+
-- The primary key leads with record_id, so its implicit index already
|
|
37
|
+
-- serves every by-record read and delete; the indexes below exist only
|
|
38
|
+
-- for the filter directions it cannot answer.
|
|
35
39
|
CREATE TABLE IF NOT EXISTS associations (
|
|
36
40
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
37
41
|
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship')),
|
|
@@ -44,6 +48,9 @@ var RECORD_SCHEMA_SQL = `
|
|
|
44
48
|
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack)
|
|
45
49
|
) STRICT;
|
|
46
50
|
|
|
51
|
+
-- parent_id holds the container the record sat in, NULL for the root --
|
|
52
|
+
-- the same encoding records.parent_id uses, since a snapshot states
|
|
53
|
+
-- where the record was rather than an instruction to apply.
|
|
47
54
|
CREATE TABLE IF NOT EXISTS versions (
|
|
48
55
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
49
56
|
version INTEGER NOT NULL,
|
|
@@ -53,6 +60,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
53
60
|
entity_id TEXT,
|
|
54
61
|
updated_by TEXT,
|
|
55
62
|
updated_via TEXT,
|
|
63
|
+
parent_id TEXT,
|
|
56
64
|
associations TEXT CHECK (associations IS NULL OR json_valid(associations)),
|
|
57
65
|
permissions TEXT CHECK (permissions IS NULL OR json_valid(permissions)),
|
|
58
66
|
PRIMARY KEY (record_id, version)
|
|
@@ -69,51 +77,58 @@ var RECORD_SCHEMA_SQL = `
|
|
|
69
77
|
created_at INTEGER NOT NULL
|
|
70
78
|
) STRICT;
|
|
71
79
|
|
|
72
|
-
-- One row per top-level
|
|
73
|
-
--
|
|
74
|
-
--
|
|
75
|
-
--
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
) STRICT;
|
|
82
|
-
|
|
83
|
-
-- One row per top-level scalar content field on a record, rewritten
|
|
84
|
-
-- beside file_refs on every content/typeId write. Exactly one of
|
|
85
|
-
-- num_value / text_key+text_value is set, by the field's declared kind:
|
|
86
|
-
-- what a field orders as must not vary with the value a given record
|
|
87
|
-
-- happens to hold. text_key is the folded form ordering compares
|
|
88
|
-
-- (@haverstack/core's contentSortKey), text_value the stored one, which
|
|
89
|
-
-- breaks a tie between two values that fold together.
|
|
90
|
-
CREATE TABLE IF NOT EXISTS content_sort (
|
|
80
|
+
-- One row per top-level scalar content field a record holds a value at:
|
|
81
|
+
-- what it orders as, and the file it references. See
|
|
82
|
+
-- docs/spec/adapters.md \xA7 Adapter backends and
|
|
83
|
+
-- docs/spec/data-model.md \xA7 Sorting by a content field.
|
|
84
|
+
--
|
|
85
|
+
-- value_rank is 0 for a numeric value and 1 for a text one \u2014 the same
|
|
86
|
+
-- split as "num_value IS NULL", but non-null, so one index can carry
|
|
87
|
+
-- the whole ordering. file_id is set only for a 'file-ref' field.
|
|
88
|
+
CREATE TABLE IF NOT EXISTS content_index (
|
|
91
89
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
92
90
|
field TEXT NOT NULL,
|
|
91
|
+
value_rank INTEGER NOT NULL CHECK (value_rank IN (0, 1)),
|
|
93
92
|
num_value REAL,
|
|
94
93
|
text_key TEXT,
|
|
95
94
|
text_value TEXT,
|
|
95
|
+
file_id TEXT,
|
|
96
96
|
PRIMARY KEY (record_id, field)
|
|
97
97
|
) STRICT;
|
|
98
98
|
|
|
99
|
-
-- Indexes
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
CREATE INDEX IF NOT EXISTS
|
|
108
|
-
CREATE INDEX IF NOT EXISTS
|
|
109
|
-
CREATE INDEX IF NOT EXISTS
|
|
110
|
-
CREATE INDEX IF NOT EXISTS
|
|
111
|
-
CREATE INDEX IF NOT EXISTS
|
|
112
|
-
CREATE INDEX IF NOT EXISTS
|
|
113
|
-
CREATE INDEX IF NOT EXISTS
|
|
114
|
-
CREATE INDEX IF NOT EXISTS
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
-- Indexes. Each filter column continues into (created_at, id) \u2014 the
|
|
100
|
+
-- default ordering and its tiebreak \u2014 because these columns are
|
|
101
|
+
-- low-cardinality by design, and a narrow index on one leaves the
|
|
102
|
+
-- planner sorting the whole matching set before it can honor LIMIT.
|
|
103
|
+
--
|
|
104
|
+
-- deleted_at and unlisted_at deliberately have none: nothing searches
|
|
105
|
+
-- for the rows they mark, and an index on a column every query tests
|
|
106
|
+
-- for IS NULL only lures the planner off the one answering ORDER BY.
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_records_type_id ON records(type_id, created_at, id);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_records_parent_id ON records(parent_id, created_at, id);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_records_entity_id ON records(entity_id, created_at, id);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_records_app_id ON records(app_id, created_at, id);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_records_principal_id ON records(principal_id, created_at, id);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_records_created_at ON records(created_at, id);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_records_updated_at ON records(updated_at, id);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_records_version ON records(version, id);
|
|
115
|
+
|
|
116
|
+
-- Each association index ends with record_id so the semi-joins in
|
|
117
|
+
-- query.ts read the matching record ids out of the index alone.
|
|
118
|
+
-- idx_assoc_related carries related_ns ahead of related_id because a
|
|
119
|
+
-- record- or entity-scoped target always stores '' there, which
|
|
120
|
+
-- buildQueryPlan states explicitly to keep the prefix contiguous.
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_assoc_kind_label ON associations(kind, label, record_id);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_assoc_kind_file_id ON associations(kind, file_id, record_id);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_assoc_related ON associations(kind, related_scope, related_ns, related_id, record_id);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_types_base_id ON types(base_id, version);
|
|
125
|
+
|
|
126
|
+
-- The whole content ordering, in index order, for one field at a time:
|
|
127
|
+
-- a content-sorted page is an ordered walk of a slice of this index.
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_content_index_sort
|
|
129
|
+
ON content_index(field, value_rank, num_value, text_key, text_value, record_id);
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_content_index_file
|
|
131
|
+
ON content_index(file_id, record_id) WHERE file_id IS NOT NULL;
|
|
117
132
|
`;
|
|
118
133
|
var TOKENS_SCHEMA_SQL = `
|
|
119
134
|
CREATE TABLE IF NOT EXISTS tokens (
|
|
@@ -125,14 +140,13 @@ var TOKENS_SCHEMA_SQL = `
|
|
|
125
140
|
created_at INTEGER NOT NULL,
|
|
126
141
|
expires_at INTEGER
|
|
127
142
|
) STRICT;
|
|
128
|
-
|
|
129
|
-
CREATE INDEX IF NOT EXISTS idx_tokens_hash ON tokens(token_hash);
|
|
130
143
|
`;
|
|
131
144
|
var FTS5_SCHEMA_SQL = `
|
|
132
145
|
CREATE VIRTUAL TABLE IF NOT EXISTS records_fts USING fts5(
|
|
133
146
|
content,
|
|
134
147
|
content='records',
|
|
135
|
-
content_rowid='rowid'
|
|
148
|
+
content_rowid='rowid',
|
|
149
|
+
columnsize=0
|
|
136
150
|
);
|
|
137
151
|
`;
|
|
138
152
|
var PRAGMA_FOREIGN_KEYS_ON = `PRAGMA foreign_keys = ON;`;
|
|
@@ -205,14 +219,16 @@ var makeCursor = (record, sort, kindOf) => {
|
|
|
205
219
|
|
|
206
220
|
// ../sqlite-shared/dist/fts5.js
|
|
207
221
|
var OUTSIDE_PHRASE_DISALLOWED = /[^\p{L}\p{N}\p{M}_\s()"]/gu;
|
|
222
|
+
var collapseWhitespace = (query) => query.replace(/\s+/g, " ");
|
|
208
223
|
var sanitizeFts5Query = (query, maxDepth = 2) => {
|
|
209
224
|
if (!query)
|
|
210
225
|
return "";
|
|
211
226
|
let clean = query.replace(/[\p{Cc}\p{Cf}]/gu, " ");
|
|
212
227
|
clean = clean.replace(/\*/g, "");
|
|
213
|
-
clean = clean.replace(/\bNEAR\s*\(
|
|
228
|
+
clean = clean.replace(/\bNEAR\s*\(([^,()]*)(?:,[^()]*)?\)/gi, "$1");
|
|
214
229
|
clean = clean.replace(/\bNEAR\s*\(/gi, "(");
|
|
215
230
|
clean = clean.replace(/"[^"]*"|[^"]+/g, (span) => span.startsWith('"') ? span : span.replace(OUTSIDE_PHRASE_DISALLOWED, " "));
|
|
231
|
+
clean = collapseWhitespace(clean);
|
|
216
232
|
clean = clean.replace(/(?:^|\(\s*)NOT\s+/gi, (m) => m.replace(/NOT\s+/i, ""));
|
|
217
233
|
let currentDepth = 0;
|
|
218
234
|
let inPhrase = false;
|
|
@@ -246,12 +262,11 @@ var sanitizeFts5Query = (query, maxDepth = 2) => {
|
|
|
246
262
|
let prev;
|
|
247
263
|
do {
|
|
248
264
|
prev = result;
|
|
249
|
-
result = result.replace(/""
|
|
250
|
-
result = result.replace(/\(\s*\)/g, " ");
|
|
265
|
+
result = result.replace(/(?:""|\(\s*\))+/g, " ");
|
|
251
266
|
result = dropDanglingOperators(result);
|
|
252
267
|
result = restoreImplicitAnd(result);
|
|
253
268
|
} while (result !== prev);
|
|
254
|
-
return result
|
|
269
|
+
return collapseWhitespace(result).replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").trim();
|
|
255
270
|
};
|
|
256
271
|
var OPERATOR = String.raw`(?:AND|OR|NOT)`;
|
|
257
272
|
var PHRASE_HOLE = "\uE000";
|
|
@@ -262,7 +277,7 @@ var dropDanglingOperators = (query) => withPhrasesMasked(query, (masked) => {
|
|
|
262
277
|
prev = out;
|
|
263
278
|
out = out.replace(new RegExp(String.raw`\b(${OPERATOR})\b(?:\s+\b${OPERATOR}\b)+`, "gi"), "$1");
|
|
264
279
|
out = out.replace(new RegExp(String.raw`(^|\()\s*\b${OPERATOR}\b\s*`, "gi"), "$1");
|
|
265
|
-
out = out.replace(new RegExp(String.raw`\
|
|
280
|
+
out = out.replace(new RegExp(String.raw`\b${OPERATOR}\b\s*($|\))`, "gi"), "$1");
|
|
266
281
|
} while (out !== prev);
|
|
267
282
|
return out;
|
|
268
283
|
});
|
|
@@ -280,7 +295,7 @@ var isOperator = (word) => /^(?:AND|OR|NOT)$/i.test(word);
|
|
|
280
295
|
var restoreImplicitAnd = (query) => withPhrasesMasked(query, (masked) => masked.replace(new RegExp(String.raw`\)\s*(?=${OPERAND_CHAR}|\()`, "gu"), (match, offset, whole) => {
|
|
281
296
|
const next = /^\s*([\p{L}\p{N}_]+)/u.exec(whole.slice(offset + match.length));
|
|
282
297
|
return next && isOperator(next[1]) ? match : ") AND ";
|
|
283
|
-
}).replace(new RegExp(String.raw`(${OPERAND_CHAR}+)\s*(?=\()`, "gu"), (match, word) => isOperator(word) ? match : `${word} AND `));
|
|
298
|
+
}).replace(new RegExp(String.raw`(?<!${OPERAND_CHAR})(${OPERAND_CHAR}+)\s*(?=\()`, "gu"), (match, word) => isOperator(word) ? match : `${word} AND `));
|
|
284
299
|
var fts5Strategy = {
|
|
285
300
|
/** Index a record that has no FTS entry yet (used right after an INSERT). */
|
|
286
301
|
insert(exec, recordId, content) {
|
|
@@ -305,6 +320,10 @@ var fts5Strategy = {
|
|
|
305
320
|
};
|
|
306
321
|
|
|
307
322
|
// ../sqlite-shared/dist/query.js
|
|
323
|
+
var atBudget = (page, rows) => ({
|
|
324
|
+
sql: page.sql,
|
|
325
|
+
params: [...page.params, rows]
|
|
326
|
+
});
|
|
308
327
|
var sqlDirection = (query) => {
|
|
309
328
|
const dir = query.sort?.direction ?? "desc";
|
|
310
329
|
if (dir !== "asc" && dir !== "desc") {
|
|
@@ -333,49 +352,18 @@ var contentPathExists = (segments, leaf, params, nextAlias) => {
|
|
|
333
352
|
};
|
|
334
353
|
var equalsLeaf = (alias) => `${alias}.type NOT IN ('object', 'array') AND ${alias}.value = ?`;
|
|
335
354
|
var SORT_ALIAS = "cs";
|
|
336
|
-
var
|
|
337
|
-
|
|
338
|
-
if (field
|
|
339
|
-
|
|
340
|
-
return
|
|
341
|
-
sql: `records r LEFT JOIN content_sort ${SORT_ALIAS} ON ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = ?`,
|
|
342
|
-
params: [field]
|
|
343
|
-
};
|
|
344
|
-
};
|
|
345
|
-
var assertCursorMatchesSort = (cursor, query) => {
|
|
346
|
-
const contentField = query.sort?.contentField;
|
|
347
|
-
if (cursor.kind === "native") {
|
|
348
|
-
const sortField = getSortField(query);
|
|
349
|
-
if (contentField !== void 0 || cursor.field !== sortField) {
|
|
350
|
-
throw new StackQueryError(`Cursor sort field "${cursor.field}" does not match query sort field "${contentField ?? sortField}"`);
|
|
351
|
-
}
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
if (contentField === void 0 || cursor.field !== contentField) {
|
|
355
|
-
throw new StackQueryError(`Cursor sort field "${cursor.field}" does not match query sort field "${contentField ?? getSortField(query)}"`);
|
|
356
|
-
}
|
|
355
|
+
var cursorMismatch = (cursor, field) => new StackQueryError(`Cursor sort field "${cursor.field}" does not match query sort field "${field}"`);
|
|
356
|
+
var nativeCursor = (cursor, field) => {
|
|
357
|
+
if (cursor.kind !== "native" || cursor.field !== field)
|
|
358
|
+
throw cursorMismatch(cursor, field);
|
|
359
|
+
return cursor;
|
|
357
360
|
};
|
|
358
|
-
var
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const rank = `CASE WHEN ${SORT_ALIAS}.record_id IS NULL THEN 2 WHEN ${SORT_ALIAS}.num_value IS NULL THEN ${textRank} ELSE ${numRank} END`;
|
|
363
|
-
const cursorRank = cursor.kind === "absent" ? 2 : cursor.kind === "num" ? numRank : textRank;
|
|
364
|
-
let tail;
|
|
365
|
-
if (cursor.kind === "num") {
|
|
366
|
-
tail = `(${SORT_ALIAS}.num_value ${op} ? OR (${SORT_ALIAS}.num_value = ? AND r.id ${op} ?))`;
|
|
367
|
-
params.push(cursor.value, cursor.value, cursor.id);
|
|
368
|
-
} else if (cursor.kind === "text") {
|
|
369
|
-
const key = contentSortKey(cursor.value);
|
|
370
|
-
tail = `(${SORT_ALIAS}.text_key ${op} ? OR (${SORT_ALIAS}.text_key = ? AND (${SORT_ALIAS}.text_value ${op} ? OR (${SORT_ALIAS}.text_value = ? AND r.id ${op} ?))))`;
|
|
371
|
-
params.push(key, key, cursor.value, cursor.value, cursor.id);
|
|
372
|
-
} else {
|
|
373
|
-
tail = `r.id ${op} ?`;
|
|
374
|
-
params.push(cursor.id);
|
|
375
|
-
}
|
|
376
|
-
return `((${rank}) > ${cursorRank} OR ((${rank}) = ${cursorRank} AND ${tail}))`;
|
|
361
|
+
var contentCursor = (cursor, field) => {
|
|
362
|
+
if (cursor.kind === "native" || cursor.field !== field)
|
|
363
|
+
throw cursorMismatch(cursor, field);
|
|
364
|
+
return cursor;
|
|
377
365
|
};
|
|
378
|
-
var
|
|
366
|
+
var recordConditions = (query) => {
|
|
379
367
|
const conditions = ["r.id != '_config'"];
|
|
380
368
|
const params = [];
|
|
381
369
|
const f = query.filter ?? {};
|
|
@@ -441,7 +429,7 @@ var buildWhereClause = (query) => {
|
|
|
441
429
|
}
|
|
442
430
|
if (f.attachmentFileId) {
|
|
443
431
|
conditions.push(`(r.id IN (SELECT a.record_id FROM associations a WHERE a.kind = 'attachment' AND a.file_id = ?)
|
|
444
|
-
OR r.id IN (SELECT
|
|
432
|
+
OR r.id IN (SELECT ci.record_id FROM content_index ci WHERE ci.file_id = ?))`);
|
|
445
433
|
params.push(f.attachmentFileId, f.attachmentFileId);
|
|
446
434
|
}
|
|
447
435
|
if (f.relatedTo) {
|
|
@@ -455,11 +443,11 @@ var buildWhereClause = (query) => {
|
|
|
455
443
|
clauses.push("a.related_scope = ?");
|
|
456
444
|
params.push(target.scope);
|
|
457
445
|
if (target.scope === "record") {
|
|
458
|
-
clauses.push("a.related_id = ?", "a.related_stack = ?");
|
|
459
|
-
params.push(target.recordId, target.stackUrl ?? "");
|
|
446
|
+
clauses.push("a.related_ns = ?", "a.related_id = ?", "a.related_stack = ?");
|
|
447
|
+
params.push("", target.recordId, target.stackUrl ?? "");
|
|
460
448
|
} else if (target.scope === "entity") {
|
|
461
|
-
clauses.push("a.related_id = ?");
|
|
462
|
-
params.push(target.entityId);
|
|
449
|
+
clauses.push("a.related_ns = ?", "a.related_id = ?");
|
|
450
|
+
params.push("", target.entityId);
|
|
463
451
|
} else {
|
|
464
452
|
clauses.push("a.related_ns = ?");
|
|
465
453
|
params.push(target.ns);
|
|
@@ -503,29 +491,61 @@ var buildWhereClause = (query) => {
|
|
|
503
491
|
conditions.push("0");
|
|
504
492
|
}
|
|
505
493
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
conditions.push(`(r.${col} ${op} ? OR (r.${col} = ? AND r.id ${op} ?))`);
|
|
513
|
-
params.push(cursor.value, cursor.value, cursor.id);
|
|
514
|
-
} else {
|
|
515
|
-
conditions.push(contentCursorCondition(cursor, op, params));
|
|
516
|
-
}
|
|
494
|
+
return { conditions, params };
|
|
495
|
+
};
|
|
496
|
+
var presentCursorCondition = (cursor, op, params) => {
|
|
497
|
+
if (cursor.kind === "num") {
|
|
498
|
+
params.push(cursor.value, cursor.value, cursor.id);
|
|
499
|
+
return `(${SORT_ALIAS}.value_rank ${op} 0 OR (${SORT_ALIAS}.value_rank = 0 AND (${SORT_ALIAS}.num_value ${op} ? OR (${SORT_ALIAS}.num_value = ? AND ${SORT_ALIAS}.record_id ${op} ?))))`;
|
|
517
500
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
};
|
|
501
|
+
const key = contentSortKey(cursor.value);
|
|
502
|
+
params.push(key, key, cursor.value, cursor.value, cursor.id);
|
|
503
|
+
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} ?))))))`;
|
|
522
504
|
};
|
|
523
|
-
var
|
|
505
|
+
var statement = (select, from, conditions, params, tail = "") => ({
|
|
506
|
+
sql: `SELECT ${select} FROM ${from} WHERE ${conditions.join(" AND ")}${tail}`,
|
|
507
|
+
params
|
|
508
|
+
});
|
|
509
|
+
var buildQueryPlan = (query) => {
|
|
524
510
|
const dir = sqlDirection(query);
|
|
525
|
-
|
|
526
|
-
|
|
511
|
+
const op = dir === "ASC" ? ">" : "<";
|
|
512
|
+
const decoded = query.cursor ? decodeCursor(query.cursor) : null;
|
|
513
|
+
const contentField = query.sort?.contentField;
|
|
514
|
+
if (contentField === void 0) {
|
|
515
|
+
const { conditions, params } = recordConditions(query);
|
|
516
|
+
const col = getSortColumn(getSortField(query));
|
|
517
|
+
if (decoded) {
|
|
518
|
+
const cursor2 = nativeCursor(decoded, getSortField(query));
|
|
519
|
+
conditions.push(`(r.${col} ${op} ? OR (r.${col} = ? AND r.id ${op} ?))`);
|
|
520
|
+
params.push(cursor2.value, cursor2.value, cursor2.id);
|
|
521
|
+
}
|
|
522
|
+
return [
|
|
523
|
+
statement("r.*", "records r", conditions, params, ` ORDER BY r.${col} ${dir}, r.id ${dir} LIMIT ?`)
|
|
524
|
+
];
|
|
525
|
+
}
|
|
526
|
+
const cursor = decoded ? contentCursor(decoded, contentField) : null;
|
|
527
|
+
const pages = [];
|
|
528
|
+
if (cursor?.kind !== "absent") {
|
|
529
|
+
const { conditions, params } = recordConditions(query);
|
|
530
|
+
const from = `content_index ${SORT_ALIAS} JOIN records r ON r.id = ${SORT_ALIAS}.record_id`;
|
|
531
|
+
conditions.push(`${SORT_ALIAS}.field = ?`);
|
|
532
|
+
params.push(contentField);
|
|
533
|
+
if (cursor)
|
|
534
|
+
conditions.push(presentCursorCondition(cursor, op, params));
|
|
535
|
+
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}`;
|
|
536
|
+
pages.push(statement("r.*", from, conditions, params, ` ORDER BY ${order} LIMIT ?`));
|
|
537
|
+
}
|
|
538
|
+
{
|
|
539
|
+
const { conditions, params } = recordConditions(query);
|
|
540
|
+
conditions.push(`NOT EXISTS (SELECT 1 FROM content_index ${SORT_ALIAS} WHERE ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = ?)`);
|
|
541
|
+
params.push(contentField);
|
|
542
|
+
if (cursor?.kind === "absent") {
|
|
543
|
+
conditions.push(`r.id ${op} ?`);
|
|
544
|
+
params.push(cursor.id);
|
|
545
|
+
}
|
|
546
|
+
pages.push(statement("r.*", "records r", conditions, params, ` ORDER BY r.id ${dir} LIMIT ?`));
|
|
527
547
|
}
|
|
528
|
-
return
|
|
548
|
+
return pages;
|
|
529
549
|
};
|
|
530
550
|
|
|
531
551
|
// ../sqlite-shared/dist/mappers.js
|
|
@@ -540,23 +560,23 @@ var rowToRecord = (row, associations) => {
|
|
|
540
560
|
content: JSON.parse(row.content),
|
|
541
561
|
version: row.version
|
|
542
562
|
};
|
|
543
|
-
if (row.parent_id)
|
|
563
|
+
if (row.parent_id != null)
|
|
544
564
|
record.parentId = row.parent_id;
|
|
545
|
-
if (row.entity_id)
|
|
565
|
+
if (row.entity_id != null)
|
|
546
566
|
record.entityId = row.entity_id;
|
|
547
|
-
if (row.app_id)
|
|
567
|
+
if (row.app_id != null)
|
|
548
568
|
record.appId = row.app_id;
|
|
549
|
-
if (row.principal_id)
|
|
569
|
+
if (row.principal_id != null)
|
|
550
570
|
record.principalId = row.principal_id;
|
|
551
|
-
if (row.updated_by)
|
|
571
|
+
if (row.updated_by != null)
|
|
552
572
|
record.updatedBy = row.updated_by;
|
|
553
|
-
if (row.updated_via)
|
|
573
|
+
if (row.updated_via != null)
|
|
554
574
|
record.updatedVia = row.updated_via;
|
|
555
|
-
if (row.deleted_at)
|
|
575
|
+
if (row.deleted_at != null)
|
|
556
576
|
record.deletedAt = fromMs(row.deleted_at);
|
|
557
|
-
if (row.unlisted_at)
|
|
577
|
+
if (row.unlisted_at != null)
|
|
558
578
|
record.unlistedAt = fromMs(row.unlisted_at);
|
|
559
|
-
if (row.permissions)
|
|
579
|
+
if (row.permissions != null)
|
|
560
580
|
record.permissions = JSON.parse(row.permissions);
|
|
561
581
|
if (associations.length)
|
|
562
582
|
record.associations = associations;
|
|
@@ -622,15 +642,17 @@ var rowToVersion = (row) => {
|
|
|
622
642
|
content: JSON.parse(row.content),
|
|
623
643
|
updatedAt: fromMs(row.updated_at)
|
|
624
644
|
};
|
|
625
|
-
if (row.entity_id)
|
|
645
|
+
if (row.entity_id != null)
|
|
626
646
|
v.entityId = row.entity_id;
|
|
627
|
-
if (row.updated_by)
|
|
647
|
+
if (row.updated_by != null)
|
|
628
648
|
v.updatedBy = row.updated_by;
|
|
629
|
-
if (row.updated_via)
|
|
649
|
+
if (row.updated_via != null)
|
|
630
650
|
v.updatedVia = row.updated_via;
|
|
631
|
-
if (row.
|
|
651
|
+
if (row.parent_id != null)
|
|
652
|
+
v.parentId = row.parent_id;
|
|
653
|
+
if (row.associations != null)
|
|
632
654
|
v.associations = JSON.parse(row.associations);
|
|
633
|
-
if (row.permissions)
|
|
655
|
+
if (row.permissions != null)
|
|
634
656
|
v.permissions = JSON.parse(row.permissions);
|
|
635
657
|
return v;
|
|
636
658
|
};
|
|
@@ -701,8 +723,7 @@ var asStackQueryError = (query, run) => {
|
|
|
701
723
|
throw new StackQueryError(`Search text could not be parsed: ${err instanceof Error ? err.message : String(err)}`);
|
|
702
724
|
}
|
|
703
725
|
};
|
|
704
|
-
var
|
|
705
|
-
var sortableFieldKinds = (schema) => {
|
|
726
|
+
var indexedFieldKinds = (schema) => {
|
|
706
727
|
const kinds = /* @__PURE__ */ new Map();
|
|
707
728
|
for (const [field, def] of Object.entries(schema)) {
|
|
708
729
|
if (!def.kind || def.kind === "array" || def.kind === "object")
|
|
@@ -714,13 +735,12 @@ var sortableFieldKinds = (schema) => {
|
|
|
714
735
|
var SharedSqlRecordLogic = class {
|
|
715
736
|
deps;
|
|
716
737
|
/**
|
|
717
|
-
* Per-typeId cache of
|
|
718
|
-
* hit the `types` table and
|
|
719
|
-
* Populated eagerly in saveType()
|
|
738
|
+
* Per-typeId cache of the fields content_index covers and their declared
|
|
739
|
+
* kinds, so syncContentIndex() doesn't hit the `types` table and
|
|
740
|
+
* re-parse schema JSON on every write. Populated eagerly in saveType()
|
|
741
|
+
* and lazily via getIndexedFields().
|
|
720
742
|
*/
|
|
721
|
-
|
|
722
|
-
/** Per-typeId cache of sortable field kinds, as fileRefFieldsByType is. */
|
|
723
|
-
sortFieldsByType = /* @__PURE__ */ new Map();
|
|
743
|
+
indexedFieldsByType = /* @__PURE__ */ new Map();
|
|
724
744
|
constructor(deps) {
|
|
725
745
|
this.deps = deps;
|
|
726
746
|
}
|
|
@@ -801,8 +821,7 @@ var SharedSqlRecordLogic = class {
|
|
|
801
821
|
this.insertAssociations(record.id, record.associations);
|
|
802
822
|
}
|
|
803
823
|
fts5Strategy.insert(this.exec, record.id, JSON.stringify(record.content));
|
|
804
|
-
this.
|
|
805
|
-
this.syncContentSort(record.id, record.typeId, record.content);
|
|
824
|
+
this.syncContentIndex(record.id, record.typeId, record.content);
|
|
806
825
|
return record;
|
|
807
826
|
}
|
|
808
827
|
async getRecord(id) {
|
|
@@ -847,8 +866,7 @@ var SharedSqlRecordLogic = class {
|
|
|
847
866
|
id
|
|
848
867
|
]);
|
|
849
868
|
fts5Strategy.insert(this.exec, id, JSON.stringify(merged));
|
|
850
|
-
this.
|
|
851
|
-
this.syncContentSort(id, existing.typeId, merged);
|
|
869
|
+
this.syncContentIndex(id, existing.typeId, merged);
|
|
852
870
|
});
|
|
853
871
|
const updated = await this.getRecord(id);
|
|
854
872
|
if (!updated)
|
|
@@ -903,8 +921,7 @@ var SharedSqlRecordLogic = class {
|
|
|
903
921
|
fts5Strategy.remove(this.exec, id);
|
|
904
922
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
905
923
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
906
|
-
this.exec.run("DELETE FROM
|
|
907
|
-
this.exec.run("DELETE FROM content_sort WHERE record_id = ?", [id]);
|
|
924
|
+
this.exec.run("DELETE FROM content_index WHERE record_id = ?", [id]);
|
|
908
925
|
this.exec.run("DELETE FROM records WHERE id = ?", [id]);
|
|
909
926
|
return purged;
|
|
910
927
|
}
|
|
@@ -965,6 +982,27 @@ var SharedSqlRecordLogic = class {
|
|
|
965
982
|
throw new Error(`Record not found after setUnlisted: "${id}"`);
|
|
966
983
|
return updated;
|
|
967
984
|
}
|
|
985
|
+
async setParent(id, parentId, opts = {}) {
|
|
986
|
+
this.exec.transaction(() => {
|
|
987
|
+
if (opts.snapshot)
|
|
988
|
+
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
989
|
+
const { clause, params: verParams } = this.versionGuard(opts.expectedVersion);
|
|
990
|
+
const changed = this.exec.run(`UPDATE records SET parent_id = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?${clause}`, [
|
|
991
|
+
parentId,
|
|
992
|
+
toMs(/* @__PURE__ */ new Date()),
|
|
993
|
+
opts.updatedBy ?? null,
|
|
994
|
+
opts.updatedVia ?? null,
|
|
995
|
+
id,
|
|
996
|
+
...verParams
|
|
997
|
+
]);
|
|
998
|
+
if (changed === 0)
|
|
999
|
+
this.throwVersionConflict(id, opts.expectedVersion);
|
|
1000
|
+
});
|
|
1001
|
+
const updated = await this.getRecord(id);
|
|
1002
|
+
if (!updated)
|
|
1003
|
+
throw new Error(`Record not found after setParent: "${id}"`);
|
|
1004
|
+
return updated;
|
|
1005
|
+
}
|
|
968
1006
|
async restoreVersion(id, version, opts = {}) {
|
|
969
1007
|
const existing = await this.getRecord(id);
|
|
970
1008
|
if (!existing)
|
|
@@ -977,12 +1015,13 @@ var SharedSqlRecordLogic = class {
|
|
|
977
1015
|
if (opts.snapshot)
|
|
978
1016
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
979
1017
|
fts5Strategy.remove(this.exec, id);
|
|
980
|
-
this.exec.run(`UPDATE records SET type_id = ?, content = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?`, [
|
|
1018
|
+
this.exec.run(`UPDATE records SET type_id = ?, content = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ?, parent_id = ? WHERE id = ?`, [
|
|
981
1019
|
target.typeId,
|
|
982
1020
|
JSON.stringify(target.content),
|
|
983
1021
|
toMs(/* @__PURE__ */ new Date()),
|
|
984
1022
|
opts.updatedBy ?? null,
|
|
985
1023
|
opts.updatedVia ?? null,
|
|
1024
|
+
target.parentId ?? null,
|
|
986
1025
|
id
|
|
987
1026
|
]);
|
|
988
1027
|
if (target.associations !== void 0) {
|
|
@@ -991,8 +1030,7 @@ var SharedSqlRecordLogic = class {
|
|
|
991
1030
|
this.insertAssociations(id, target.associations);
|
|
992
1031
|
}
|
|
993
1032
|
fts5Strategy.insert(this.exec, id, JSON.stringify(target.content));
|
|
994
|
-
this.
|
|
995
|
-
this.syncContentSort(id, target.typeId, target.content);
|
|
1033
|
+
this.syncContentIndex(id, target.typeId, target.content);
|
|
996
1034
|
});
|
|
997
1035
|
const updated = await this.getRecord(id);
|
|
998
1036
|
if (!updated)
|
|
@@ -1017,8 +1055,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1017
1055
|
id
|
|
1018
1056
|
]);
|
|
1019
1057
|
fts5Strategy.insert(this.exec, id, JSON.stringify(content));
|
|
1020
|
-
this.
|
|
1021
|
-
this.syncContentSort(id, toTypeId, content);
|
|
1058
|
+
this.syncContentIndex(id, toTypeId, content);
|
|
1022
1059
|
});
|
|
1023
1060
|
const updated = await this.getRecord(id);
|
|
1024
1061
|
if (!updated)
|
|
@@ -1026,26 +1063,25 @@ var SharedSqlRecordLogic = class {
|
|
|
1026
1063
|
return updated;
|
|
1027
1064
|
}
|
|
1028
1065
|
async queryRecords(query) {
|
|
1029
|
-
const { sql: from, params: fromParams } = buildFromClause(query);
|
|
1030
|
-
const { sql: where, params: whereParams } = buildWhereClause(query);
|
|
1031
|
-
const params = [...fromParams, ...whereParams];
|
|
1032
|
-
const order = buildOrderClause(query);
|
|
1033
1066
|
const limit = query.limit ?? 50;
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1067
|
+
const fetch = limit + 1;
|
|
1068
|
+
const rows = [];
|
|
1069
|
+
for (const page2 of buildQueryPlan(query)) {
|
|
1070
|
+
const budget = fetch - rows.length;
|
|
1071
|
+
if (budget <= 0)
|
|
1072
|
+
break;
|
|
1073
|
+
const statement2 = atBudget(page2, budget);
|
|
1074
|
+
rows.push(...asStackQueryError(query, () => this.exec.all(statement2.sql, statement2.params)));
|
|
1075
|
+
}
|
|
1038
1076
|
const hasMore = rows.length > limit;
|
|
1039
1077
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
1040
1078
|
const records = page.map((row) => {
|
|
1041
1079
|
const associations = this.getAssociationsForRecord(row.id);
|
|
1042
1080
|
return rowToRecord(row, associations);
|
|
1043
1081
|
});
|
|
1044
|
-
const countRows = asStackQueryError(query, () => this.exec.all(`SELECT COUNT(*) as total FROM ${from} ${where}`, params));
|
|
1045
|
-
const total = countRows[0]?.total ?? 0;
|
|
1046
1082
|
const lastRecord = records[records.length - 1];
|
|
1047
1083
|
const cursor = hasMore && lastRecord ? makeCursor(lastRecord, query.sort, (record, field) => this.sortFieldKind(record, field)) : null;
|
|
1048
|
-
return { records, cursor
|
|
1084
|
+
return { records, cursor };
|
|
1049
1085
|
}
|
|
1050
1086
|
/**
|
|
1051
1087
|
* Atomically verify fileId is unreferenced, then hard-delete its
|
|
@@ -1058,7 +1094,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1058
1094
|
return this.exec.transaction(() => {
|
|
1059
1095
|
const referenced = this.exec.all(`SELECT 1 as found FROM associations WHERE kind = 'attachment' AND file_id = ?
|
|
1060
1096
|
UNION ALL
|
|
1061
|
-
SELECT 1 FROM
|
|
1097
|
+
SELECT 1 FROM content_index WHERE file_id = ?
|
|
1062
1098
|
LIMIT 1`, [fileId, fileId]);
|
|
1063
1099
|
if (referenced.length) {
|
|
1064
1100
|
throw new StackConflictError("Attachment is still referenced by one or more records");
|
|
@@ -1098,8 +1134,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1098
1134
|
try {
|
|
1099
1135
|
this.exec.run(`INSERT INTO versions
|
|
1100
1136
|
(record_id, version, type_id, content, updated_at, entity_id,
|
|
1101
|
-
updated_by, updated_via, associations, permissions)
|
|
1102
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1137
|
+
updated_by, updated_via, parent_id, associations, permissions)
|
|
1138
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1103
1139
|
id,
|
|
1104
1140
|
version.version,
|
|
1105
1141
|
version.typeId,
|
|
@@ -1108,6 +1144,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1108
1144
|
version.entityId ?? null,
|
|
1109
1145
|
version.updatedBy ?? null,
|
|
1110
1146
|
version.updatedVia ?? null,
|
|
1147
|
+
version.parentId ?? null,
|
|
1111
1148
|
version.associations ? JSON.stringify(version.associations) : null,
|
|
1112
1149
|
version.permissions ? JSON.stringify(version.permissions) : null
|
|
1113
1150
|
]);
|
|
@@ -1118,14 +1155,24 @@ var SharedSqlRecordLogic = class {
|
|
|
1118
1155
|
throw err;
|
|
1119
1156
|
}
|
|
1120
1157
|
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Replaces every column insertVersionRow writes, so an overwritten row is
|
|
1160
|
+
* indistinguishable from a freshly inserted one — a column left out here
|
|
1161
|
+
* would keep the replaced row's value and surface later as a restore
|
|
1162
|
+
* putting back something the snapshot never said.
|
|
1163
|
+
*/
|
|
1121
1164
|
overwriteVersionRow(id, version) {
|
|
1122
1165
|
this.exec.run(`UPDATE versions
|
|
1123
|
-
SET type_id = ?, content = ?, updated_at = ?, entity_id = ?,
|
|
1166
|
+
SET type_id = ?, content = ?, updated_at = ?, entity_id = ?,
|
|
1167
|
+
updated_by = ?, updated_via = ?, parent_id = ?, associations = ?, permissions = ?
|
|
1124
1168
|
WHERE record_id = ? AND version = ?`, [
|
|
1125
1169
|
version.typeId,
|
|
1126
1170
|
JSON.stringify(version.content),
|
|
1127
1171
|
toMs(version.updatedAt),
|
|
1128
1172
|
version.entityId ?? null,
|
|
1173
|
+
version.updatedBy ?? null,
|
|
1174
|
+
version.updatedVia ?? null,
|
|
1175
|
+
version.parentId ?? null,
|
|
1129
1176
|
version.associations ? JSON.stringify(version.associations) : null,
|
|
1130
1177
|
version.permissions ? JSON.stringify(version.permissions) : null,
|
|
1131
1178
|
id,
|
|
@@ -1178,8 +1225,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1178
1225
|
toMs(type.createdAt)
|
|
1179
1226
|
]);
|
|
1180
1227
|
const schema = type.schema;
|
|
1181
|
-
this.
|
|
1182
|
-
this.sortFieldsByType.set(type.id, sortableFieldKinds(schema));
|
|
1228
|
+
this.indexedFieldsByType.set(type.id, indexedFieldKinds(schema));
|
|
1183
1229
|
}
|
|
1184
1230
|
async getType(id) {
|
|
1185
1231
|
const rows = this.exec.all("SELECT * FROM types WHERE id = ?", [id]);
|
|
@@ -1255,67 +1301,49 @@ var SharedSqlRecordLogic = class {
|
|
|
1255
1301
|
return rows.map(rowToAssociation);
|
|
1256
1302
|
}
|
|
1257
1303
|
/**
|
|
1258
|
-
*
|
|
1259
|
-
* back to a `types` lookup (and populating the cache) only for
|
|
1260
|
-
* this process hasn't seen via saveType() yet, e.g. right after
|
|
1261
|
-
|
|
1262
|
-
getFileRefFields(typeId) {
|
|
1263
|
-
const cached = this.fileRefFieldsByType.get(typeId);
|
|
1264
|
-
if (cached)
|
|
1265
|
-
return cached;
|
|
1266
|
-
const typeRow = this.exec.get("SELECT schema FROM types WHERE id = ?", [
|
|
1267
|
-
typeId
|
|
1268
|
-
]);
|
|
1269
|
-
const fields = typeRow ? fileRefFieldNames(JSON.parse(typeRow.schema)) : [];
|
|
1270
|
-
this.fileRefFieldsByType.set(typeId, fields);
|
|
1271
|
-
return fields;
|
|
1272
|
-
}
|
|
1273
|
-
/**
|
|
1274
|
-
* Replace a record's file_refs rows with whatever its content currently
|
|
1275
|
-
* holds in top-level file-ref fields, on every write that can change
|
|
1276
|
-
* content or typeId, so the index never drifts. Only top-level scalars
|
|
1277
|
-
* are indexed; the schema lookup is cached (fileRefFieldsByType).
|
|
1304
|
+
* The fields content_index covers for typeId, from the in-memory cache
|
|
1305
|
+
* — falling back to a `types` lookup (and populating the cache) only for
|
|
1306
|
+
* a typeId this process hasn't seen via saveType() yet, e.g. right after
|
|
1307
|
+
* open().
|
|
1278
1308
|
*/
|
|
1279
|
-
|
|
1280
|
-
const cached = this.
|
|
1309
|
+
getIndexedFields(typeId) {
|
|
1310
|
+
const cached = this.indexedFieldsByType.get(typeId);
|
|
1281
1311
|
if (cached)
|
|
1282
1312
|
return cached;
|
|
1283
1313
|
const typeRow = this.exec.get("SELECT schema FROM types WHERE id = ?", [
|
|
1284
1314
|
typeId
|
|
1285
1315
|
]);
|
|
1286
|
-
const fields = typeRow ?
|
|
1287
|
-
this.
|
|
1316
|
+
const fields = typeRow ? indexedFieldKinds(JSON.parse(typeRow.schema)) : /* @__PURE__ */ new Map();
|
|
1317
|
+
this.indexedFieldsByType.set(typeId, fields);
|
|
1288
1318
|
return fields;
|
|
1289
1319
|
}
|
|
1290
1320
|
/**
|
|
1291
|
-
* Replace a record's
|
|
1292
|
-
* holds, on every write that can change content or typeId
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1321
|
+
* Replace a record's content_index rows with what its content currently
|
|
1322
|
+
* holds, on every write that can change content or typeId, so neither
|
|
1323
|
+
* the ordering nor the file-reference lookup can drift from the record.
|
|
1324
|
+
* A field holding no orderable value gets no row at all, which is what
|
|
1325
|
+
* puts the record at the end of a sort on that field.
|
|
1295
1326
|
*/
|
|
1296
|
-
|
|
1297
|
-
this.exec.run("DELETE FROM
|
|
1298
|
-
for (const [field, kind] of this.
|
|
1327
|
+
syncContentIndex(recordId, typeId, content) {
|
|
1328
|
+
this.exec.run("DELETE FROM content_index WHERE record_id = ?", [recordId]);
|
|
1329
|
+
for (const [field, kind] of this.getIndexedFields(typeId)) {
|
|
1299
1330
|
const entry = contentSortEntry(kind, content[field]);
|
|
1300
1331
|
if (!entry)
|
|
1301
1332
|
continue;
|
|
1302
|
-
|
|
1303
|
-
|
|
1333
|
+
const sql = `INSERT OR IGNORE INTO content_index
|
|
1334
|
+
(record_id, field, value_rank, num_value, text_key, text_value, file_id)
|
|
1335
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`;
|
|
1336
|
+
if (entry.kind === "num") {
|
|
1337
|
+
this.exec.run(sql, [recordId, field, 0, entry.num, null, null, null]);
|
|
1338
|
+
} else {
|
|
1339
|
+
const fileId = kind === "file-ref" ? entry.text : null;
|
|
1340
|
+
this.exec.run(sql, [recordId, field, 1, null, entry.key, entry.text, fileId]);
|
|
1341
|
+
}
|
|
1304
1342
|
}
|
|
1305
1343
|
}
|
|
1306
1344
|
/** The declared kind of a record's top-level `field`, if it has one. */
|
|
1307
1345
|
sortFieldKind(record, field) {
|
|
1308
|
-
return this.
|
|
1309
|
-
}
|
|
1310
|
-
syncFileRefs(recordId, typeId, content) {
|
|
1311
|
-
this.exec.run("DELETE FROM file_refs WHERE record_id = ?", [recordId]);
|
|
1312
|
-
const fields = this.getFileRefFields(typeId);
|
|
1313
|
-
for (const field of fields) {
|
|
1314
|
-
const value = content[field];
|
|
1315
|
-
if (typeof value === "string") {
|
|
1316
|
-
this.exec.run("INSERT OR IGNORE INTO file_refs (record_id, field, file_id) VALUES (?, ?, ?)", [recordId, field, value]);
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1346
|
+
return this.getIndexedFields(record.typeId).get(field);
|
|
1319
1347
|
}
|
|
1320
1348
|
};
|
|
1321
1349
|
var SharedTokenLogic = class {
|
|
@@ -1536,6 +1564,9 @@ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter {
|
|
|
1536
1564
|
setUnlisted(id, unlisted, opts) {
|
|
1537
1565
|
return this.record.setUnlisted(id, unlisted, opts);
|
|
1538
1566
|
}
|
|
1567
|
+
setParent(id, parentId, opts) {
|
|
1568
|
+
return this.record.setParent(id, parentId, opts);
|
|
1569
|
+
}
|
|
1539
1570
|
restoreVersion(id, version, opts) {
|
|
1540
1571
|
return this.record.restoreVersion(id, version, opts);
|
|
1541
1572
|
}
|