@haverstack/record-adapter-do-sqlite 0.11.0 → 0.13.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 -203
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -25,6 +25,10 @@ var RECORD_SCHEMA_SQL = `
|
|
|
25
25
|
-- qualifier: related_stack for a record in another stack, related_ns for
|
|
26
26
|
-- a foreign namespace. All four are in the primary key, so two targets
|
|
27
27
|
-- differing only by namespace are two associations.
|
|
28
|
+
--
|
|
29
|
+
-- The primary key leads with record_id, so its implicit index already
|
|
30
|
+
-- serves every by-record read and delete; the indexes below exist only
|
|
31
|
+
-- for the filter directions it cannot answer.
|
|
28
32
|
CREATE TABLE IF NOT EXISTS associations (
|
|
29
33
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
30
34
|
kind TEXT NOT NULL CHECK (kind IN ('tag', 'attachment', 'relationship')),
|
|
@@ -37,6 +41,9 @@ var RECORD_SCHEMA_SQL = `
|
|
|
37
41
|
PRIMARY KEY (record_id, kind, label, file_id, related_scope, related_id, related_ns, related_stack)
|
|
38
42
|
) STRICT;
|
|
39
43
|
|
|
44
|
+
-- parent_id holds the container the record sat in, NULL for the root --
|
|
45
|
+
-- the same encoding records.parent_id uses, since a snapshot states
|
|
46
|
+
-- where the record was rather than an instruction to apply.
|
|
40
47
|
CREATE TABLE IF NOT EXISTS versions (
|
|
41
48
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
42
49
|
version INTEGER NOT NULL,
|
|
@@ -46,6 +53,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
46
53
|
entity_id TEXT,
|
|
47
54
|
updated_by TEXT,
|
|
48
55
|
updated_via TEXT,
|
|
56
|
+
parent_id TEXT,
|
|
49
57
|
associations TEXT CHECK (associations IS NULL OR json_valid(associations)),
|
|
50
58
|
permissions TEXT CHECK (permissions IS NULL OR json_valid(permissions)),
|
|
51
59
|
PRIMARY KEY (record_id, version)
|
|
@@ -62,57 +70,65 @@ var RECORD_SCHEMA_SQL = `
|
|
|
62
70
|
created_at INTEGER NOT NULL
|
|
63
71
|
) STRICT;
|
|
64
72
|
|
|
65
|
-
-- One row per top-level
|
|
66
|
-
--
|
|
67
|
-
--
|
|
68
|
-
--
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
) STRICT;
|
|
75
|
-
|
|
76
|
-
-- One row per top-level scalar content field on a record, rewritten
|
|
77
|
-
-- beside file_refs on every content/typeId write. Exactly one of
|
|
78
|
-
-- num_value / text_key+text_value is set, by the field's declared kind:
|
|
79
|
-
-- what a field orders as must not vary with the value a given record
|
|
80
|
-
-- happens to hold. text_key is the folded form ordering compares
|
|
81
|
-
-- (@haverstack/core's contentSortKey), text_value the stored one, which
|
|
82
|
-
-- breaks a tie between two values that fold together.
|
|
83
|
-
CREATE TABLE IF NOT EXISTS content_sort (
|
|
73
|
+
-- One row per top-level scalar content field a record holds a value at:
|
|
74
|
+
-- what it orders as, and the file it references. See
|
|
75
|
+
-- docs/spec/adapters.md \xA7 Adapter backends and
|
|
76
|
+
-- docs/spec/data-model.md \xA7 Sorting by a content field.
|
|
77
|
+
--
|
|
78
|
+
-- value_rank is 0 for a numeric value and 1 for a text one \u2014 the same
|
|
79
|
+
-- split as "num_value IS NULL", but non-null, so one index can carry
|
|
80
|
+
-- the whole ordering. file_id is set only for a 'file-ref' field.
|
|
81
|
+
CREATE TABLE IF NOT EXISTS content_index (
|
|
84
82
|
record_id TEXT NOT NULL REFERENCES records(id),
|
|
85
83
|
field TEXT NOT NULL,
|
|
84
|
+
value_rank INTEGER NOT NULL CHECK (value_rank IN (0, 1)),
|
|
86
85
|
num_value REAL,
|
|
87
86
|
text_key TEXT,
|
|
88
87
|
text_value TEXT,
|
|
88
|
+
file_id TEXT,
|
|
89
89
|
PRIMARY KEY (record_id, field)
|
|
90
90
|
) STRICT;
|
|
91
91
|
|
|
92
|
-
-- Indexes
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
CREATE INDEX IF NOT EXISTS
|
|
101
|
-
CREATE INDEX IF NOT EXISTS
|
|
102
|
-
CREATE INDEX IF NOT EXISTS
|
|
103
|
-
CREATE INDEX IF NOT EXISTS
|
|
104
|
-
CREATE INDEX IF NOT EXISTS
|
|
105
|
-
CREATE INDEX IF NOT EXISTS
|
|
106
|
-
CREATE INDEX IF NOT EXISTS
|
|
107
|
-
CREATE INDEX IF NOT EXISTS
|
|
108
|
-
|
|
109
|
-
|
|
92
|
+
-- Indexes. Each filter column continues into (created_at, id) \u2014 the
|
|
93
|
+
-- default ordering and its tiebreak \u2014 because these columns are
|
|
94
|
+
-- low-cardinality by design, and a narrow index on one leaves the
|
|
95
|
+
-- planner sorting the whole matching set before it can honor LIMIT.
|
|
96
|
+
--
|
|
97
|
+
-- deleted_at and unlisted_at deliberately have none: nothing searches
|
|
98
|
+
-- for the rows they mark, and an index on a column every query tests
|
|
99
|
+
-- for IS NULL only lures the planner off the one answering ORDER BY.
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_records_type_id ON records(type_id, created_at, id);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_records_parent_id ON records(parent_id, created_at, id);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_records_entity_id ON records(entity_id, created_at, id);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_records_app_id ON records(app_id, created_at, id);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_records_principal_id ON records(principal_id, created_at, id);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_records_created_at ON records(created_at, id);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_records_updated_at ON records(updated_at, id);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_records_version ON records(version, id);
|
|
108
|
+
|
|
109
|
+
-- Each association index ends with record_id so the semi-joins in
|
|
110
|
+
-- query.ts read the matching record ids out of the index alone.
|
|
111
|
+
-- idx_assoc_related carries related_ns ahead of related_id because a
|
|
112
|
+
-- record- or entity-scoped target always stores '' there, which
|
|
113
|
+
-- buildQueryPlan states explicitly to keep the prefix contiguous.
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_assoc_kind_label ON associations(kind, label, record_id);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_assoc_kind_file_id ON associations(kind, file_id, record_id);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_assoc_related ON associations(kind, related_scope, related_ns, related_id, record_id);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_types_base_id ON types(base_id, version);
|
|
118
|
+
|
|
119
|
+
-- The whole content ordering, in index order, for one field at a time:
|
|
120
|
+
-- a content-sorted page is an ordered walk of a slice of this index.
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_content_index_sort
|
|
122
|
+
ON content_index(field, value_rank, num_value, text_key, text_value, record_id);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_content_index_file
|
|
124
|
+
ON content_index(file_id, record_id) WHERE file_id IS NOT NULL;
|
|
110
125
|
`;
|
|
111
126
|
var FTS5_SCHEMA_SQL = `
|
|
112
127
|
CREATE VIRTUAL TABLE IF NOT EXISTS records_fts USING fts5(
|
|
113
128
|
content,
|
|
114
129
|
content='records',
|
|
115
|
-
content_rowid='rowid'
|
|
130
|
+
content_rowid='rowid',
|
|
131
|
+
columnsize=0
|
|
116
132
|
);
|
|
117
133
|
`;
|
|
118
134
|
var PRAGMA_FOREIGN_KEYS_ON = `PRAGMA foreign_keys = ON;`;
|
|
@@ -184,14 +200,16 @@ var makeCursor = (record, sort, kindOf) => {
|
|
|
184
200
|
|
|
185
201
|
// ../sqlite-shared/dist/fts5.js
|
|
186
202
|
var OUTSIDE_PHRASE_DISALLOWED = /[^\p{L}\p{N}\p{M}_\s()"]/gu;
|
|
203
|
+
var collapseWhitespace = (query) => query.replace(/\s+/g, " ");
|
|
187
204
|
var sanitizeFts5Query = (query, maxDepth = 2) => {
|
|
188
205
|
if (!query)
|
|
189
206
|
return "";
|
|
190
207
|
let clean = query.replace(/[\p{Cc}\p{Cf}]/gu, " ");
|
|
191
208
|
clean = clean.replace(/\*/g, "");
|
|
192
|
-
clean = clean.replace(/\bNEAR\s*\(
|
|
209
|
+
clean = clean.replace(/\bNEAR\s*\(([^,()]*)(?:,[^()]*)?\)/gi, "$1");
|
|
193
210
|
clean = clean.replace(/\bNEAR\s*\(/gi, "(");
|
|
194
211
|
clean = clean.replace(/"[^"]*"|[^"]+/g, (span) => span.startsWith('"') ? span : span.replace(OUTSIDE_PHRASE_DISALLOWED, " "));
|
|
212
|
+
clean = collapseWhitespace(clean);
|
|
195
213
|
clean = clean.replace(/(?:^|\(\s*)NOT\s+/gi, (m) => m.replace(/NOT\s+/i, ""));
|
|
196
214
|
let currentDepth = 0;
|
|
197
215
|
let inPhrase = false;
|
|
@@ -225,12 +243,11 @@ var sanitizeFts5Query = (query, maxDepth = 2) => {
|
|
|
225
243
|
let prev;
|
|
226
244
|
do {
|
|
227
245
|
prev = result;
|
|
228
|
-
result = result.replace(/""
|
|
229
|
-
result = result.replace(/\(\s*\)/g, " ");
|
|
246
|
+
result = result.replace(/(?:""|\(\s*\))+/g, " ");
|
|
230
247
|
result = dropDanglingOperators(result);
|
|
231
248
|
result = restoreImplicitAnd(result);
|
|
232
249
|
} while (result !== prev);
|
|
233
|
-
return result
|
|
250
|
+
return collapseWhitespace(result).replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").trim();
|
|
234
251
|
};
|
|
235
252
|
var OPERATOR = String.raw`(?:AND|OR|NOT)`;
|
|
236
253
|
var PHRASE_HOLE = "\uE000";
|
|
@@ -241,7 +258,7 @@ var dropDanglingOperators = (query) => withPhrasesMasked(query, (masked) => {
|
|
|
241
258
|
prev = out;
|
|
242
259
|
out = out.replace(new RegExp(String.raw`\b(${OPERATOR})\b(?:\s+\b${OPERATOR}\b)+`, "gi"), "$1");
|
|
243
260
|
out = out.replace(new RegExp(String.raw`(^|\()\s*\b${OPERATOR}\b\s*`, "gi"), "$1");
|
|
244
|
-
out = out.replace(new RegExp(String.raw`\
|
|
261
|
+
out = out.replace(new RegExp(String.raw`\b${OPERATOR}\b\s*($|\))`, "gi"), "$1");
|
|
245
262
|
} while (out !== prev);
|
|
246
263
|
return out;
|
|
247
264
|
});
|
|
@@ -259,7 +276,7 @@ var isOperator = (word) => /^(?:AND|OR|NOT)$/i.test(word);
|
|
|
259
276
|
var restoreImplicitAnd = (query) => withPhrasesMasked(query, (masked) => masked.replace(new RegExp(String.raw`\)\s*(?=${OPERAND_CHAR}|\()`, "gu"), (match, offset, whole) => {
|
|
260
277
|
const next = /^\s*([\p{L}\p{N}_]+)/u.exec(whole.slice(offset + match.length));
|
|
261
278
|
return next && isOperator(next[1]) ? match : ") AND ";
|
|
262
|
-
}).replace(new RegExp(String.raw`(${OPERAND_CHAR}+)\s*(?=\()`, "gu"), (match, word) => isOperator(word) ? match : `${word} AND `));
|
|
279
|
+
}).replace(new RegExp(String.raw`(?<!${OPERAND_CHAR})(${OPERAND_CHAR}+)\s*(?=\()`, "gu"), (match, word) => isOperator(word) ? match : `${word} AND `));
|
|
263
280
|
var fts5Strategy = {
|
|
264
281
|
/** Index a record that has no FTS entry yet (used right after an INSERT). */
|
|
265
282
|
insert(exec, recordId, content) {
|
|
@@ -284,6 +301,10 @@ var fts5Strategy = {
|
|
|
284
301
|
};
|
|
285
302
|
|
|
286
303
|
// ../sqlite-shared/dist/query.js
|
|
304
|
+
var atBudget = (page, rows) => ({
|
|
305
|
+
sql: page.sql,
|
|
306
|
+
params: [...page.params, rows]
|
|
307
|
+
});
|
|
287
308
|
var sqlDirection = (query) => {
|
|
288
309
|
const dir = query.sort?.direction ?? "desc";
|
|
289
310
|
if (dir !== "asc" && dir !== "desc") {
|
|
@@ -312,49 +333,18 @@ var contentPathExists = (segments, leaf, params, nextAlias) => {
|
|
|
312
333
|
};
|
|
313
334
|
var equalsLeaf = (alias) => `${alias}.type NOT IN ('object', 'array') AND ${alias}.value = ?`;
|
|
314
335
|
var SORT_ALIAS = "cs";
|
|
315
|
-
var
|
|
316
|
-
|
|
317
|
-
if (field
|
|
318
|
-
|
|
319
|
-
return
|
|
320
|
-
sql: `records r LEFT JOIN content_sort ${SORT_ALIAS} ON ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = ?`,
|
|
321
|
-
params: [field]
|
|
322
|
-
};
|
|
336
|
+
var cursorMismatch = (cursor, field) => new StackQueryError(`Cursor sort field "${cursor.field}" does not match query sort field "${field}"`);
|
|
337
|
+
var nativeCursor = (cursor, field) => {
|
|
338
|
+
if (cursor.kind !== "native" || cursor.field !== field)
|
|
339
|
+
throw cursorMismatch(cursor, field);
|
|
340
|
+
return cursor;
|
|
323
341
|
};
|
|
324
|
-
var
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (contentField !== void 0 || cursor.field !== sortField) {
|
|
329
|
-
throw new StackQueryError(`Cursor sort field "${cursor.field}" does not match query sort field "${contentField ?? sortField}"`);
|
|
330
|
-
}
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (contentField === void 0 || cursor.field !== contentField) {
|
|
334
|
-
throw new StackQueryError(`Cursor sort field "${cursor.field}" does not match query sort field "${contentField ?? getSortField(query)}"`);
|
|
335
|
-
}
|
|
342
|
+
var contentCursor = (cursor, field) => {
|
|
343
|
+
if (cursor.kind === "native" || cursor.field !== field)
|
|
344
|
+
throw cursorMismatch(cursor, field);
|
|
345
|
+
return cursor;
|
|
336
346
|
};
|
|
337
|
-
var
|
|
338
|
-
const ascending = op === ">";
|
|
339
|
-
const numRank = ascending ? 0 : 1;
|
|
340
|
-
const textRank = ascending ? 1 : 0;
|
|
341
|
-
const rank = `CASE WHEN ${SORT_ALIAS}.record_id IS NULL THEN 2 WHEN ${SORT_ALIAS}.num_value IS NULL THEN ${textRank} ELSE ${numRank} END`;
|
|
342
|
-
const cursorRank = cursor.kind === "absent" ? 2 : cursor.kind === "num" ? numRank : textRank;
|
|
343
|
-
let tail;
|
|
344
|
-
if (cursor.kind === "num") {
|
|
345
|
-
tail = `(${SORT_ALIAS}.num_value ${op} ? OR (${SORT_ALIAS}.num_value = ? AND r.id ${op} ?))`;
|
|
346
|
-
params.push(cursor.value, cursor.value, cursor.id);
|
|
347
|
-
} else if (cursor.kind === "text") {
|
|
348
|
-
const key = contentSortKey(cursor.value);
|
|
349
|
-
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} ?))))`;
|
|
350
|
-
params.push(key, key, cursor.value, cursor.value, cursor.id);
|
|
351
|
-
} else {
|
|
352
|
-
tail = `r.id ${op} ?`;
|
|
353
|
-
params.push(cursor.id);
|
|
354
|
-
}
|
|
355
|
-
return `((${rank}) > ${cursorRank} OR ((${rank}) = ${cursorRank} AND ${tail}))`;
|
|
356
|
-
};
|
|
357
|
-
var buildWhereClause = (query) => {
|
|
347
|
+
var recordConditions = (query) => {
|
|
358
348
|
const conditions = ["r.id != '_config'"];
|
|
359
349
|
const params = [];
|
|
360
350
|
const f = query.filter ?? {};
|
|
@@ -420,7 +410,7 @@ var buildWhereClause = (query) => {
|
|
|
420
410
|
}
|
|
421
411
|
if (f.attachmentFileId) {
|
|
422
412
|
conditions.push(`(r.id IN (SELECT a.record_id FROM associations a WHERE a.kind = 'attachment' AND a.file_id = ?)
|
|
423
|
-
OR r.id IN (SELECT
|
|
413
|
+
OR r.id IN (SELECT ci.record_id FROM content_index ci WHERE ci.file_id = ?))`);
|
|
424
414
|
params.push(f.attachmentFileId, f.attachmentFileId);
|
|
425
415
|
}
|
|
426
416
|
if (f.relatedTo) {
|
|
@@ -434,11 +424,11 @@ var buildWhereClause = (query) => {
|
|
|
434
424
|
clauses.push("a.related_scope = ?");
|
|
435
425
|
params.push(target.scope);
|
|
436
426
|
if (target.scope === "record") {
|
|
437
|
-
clauses.push("a.related_id = ?", "a.related_stack = ?");
|
|
438
|
-
params.push(target.recordId, target.stackUrl ?? "");
|
|
427
|
+
clauses.push("a.related_ns = ?", "a.related_id = ?", "a.related_stack = ?");
|
|
428
|
+
params.push("", target.recordId, target.stackUrl ?? "");
|
|
439
429
|
} else if (target.scope === "entity") {
|
|
440
|
-
clauses.push("a.related_id = ?");
|
|
441
|
-
params.push(target.entityId);
|
|
430
|
+
clauses.push("a.related_ns = ?", "a.related_id = ?");
|
|
431
|
+
params.push("", target.entityId);
|
|
442
432
|
} else {
|
|
443
433
|
clauses.push("a.related_ns = ?");
|
|
444
434
|
params.push(target.ns);
|
|
@@ -482,29 +472,61 @@ var buildWhereClause = (query) => {
|
|
|
482
472
|
conditions.push("0");
|
|
483
473
|
}
|
|
484
474
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
conditions.push(`(r.${col} ${op} ? OR (r.${col} = ? AND r.id ${op} ?))`);
|
|
492
|
-
params.push(cursor.value, cursor.value, cursor.id);
|
|
493
|
-
} else {
|
|
494
|
-
conditions.push(contentCursorCondition(cursor, op, params));
|
|
495
|
-
}
|
|
475
|
+
return { conditions, params };
|
|
476
|
+
};
|
|
477
|
+
var presentCursorCondition = (cursor, op, params) => {
|
|
478
|
+
if (cursor.kind === "num") {
|
|
479
|
+
params.push(cursor.value, cursor.value, cursor.id);
|
|
480
|
+
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} ?))))`;
|
|
496
481
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
};
|
|
482
|
+
const key = contentSortKey(cursor.value);
|
|
483
|
+
params.push(key, key, cursor.value, cursor.value, cursor.id);
|
|
484
|
+
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} ?))))))`;
|
|
501
485
|
};
|
|
502
|
-
var
|
|
486
|
+
var statement = (select, from, conditions, params, tail = "") => ({
|
|
487
|
+
sql: `SELECT ${select} FROM ${from} WHERE ${conditions.join(" AND ")}${tail}`,
|
|
488
|
+
params
|
|
489
|
+
});
|
|
490
|
+
var buildQueryPlan = (query) => {
|
|
503
491
|
const dir = sqlDirection(query);
|
|
504
|
-
|
|
505
|
-
|
|
492
|
+
const op = dir === "ASC" ? ">" : "<";
|
|
493
|
+
const decoded = query.cursor ? decodeCursor(query.cursor) : null;
|
|
494
|
+
const contentField = query.sort?.contentField;
|
|
495
|
+
if (contentField === void 0) {
|
|
496
|
+
const { conditions, params } = recordConditions(query);
|
|
497
|
+
const col = getSortColumn(getSortField(query));
|
|
498
|
+
if (decoded) {
|
|
499
|
+
const cursor2 = nativeCursor(decoded, getSortField(query));
|
|
500
|
+
conditions.push(`(r.${col} ${op} ? OR (r.${col} = ? AND r.id ${op} ?))`);
|
|
501
|
+
params.push(cursor2.value, cursor2.value, cursor2.id);
|
|
502
|
+
}
|
|
503
|
+
return [
|
|
504
|
+
statement("r.*", "records r", conditions, params, ` ORDER BY r.${col} ${dir}, r.id ${dir} LIMIT ?`)
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
const cursor = decoded ? contentCursor(decoded, contentField) : null;
|
|
508
|
+
const pages = [];
|
|
509
|
+
if (cursor?.kind !== "absent") {
|
|
510
|
+
const { conditions, params } = recordConditions(query);
|
|
511
|
+
const from = `content_index ${SORT_ALIAS} JOIN records r ON r.id = ${SORT_ALIAS}.record_id`;
|
|
512
|
+
conditions.push(`${SORT_ALIAS}.field = ?`);
|
|
513
|
+
params.push(contentField);
|
|
514
|
+
if (cursor)
|
|
515
|
+
conditions.push(presentCursorCondition(cursor, op, params));
|
|
516
|
+
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}`;
|
|
517
|
+
pages.push(statement("r.*", from, conditions, params, ` ORDER BY ${order} LIMIT ?`));
|
|
518
|
+
}
|
|
519
|
+
{
|
|
520
|
+
const { conditions, params } = recordConditions(query);
|
|
521
|
+
conditions.push(`NOT EXISTS (SELECT 1 FROM content_index ${SORT_ALIAS} WHERE ${SORT_ALIAS}.record_id = r.id AND ${SORT_ALIAS}.field = ?)`);
|
|
522
|
+
params.push(contentField);
|
|
523
|
+
if (cursor?.kind === "absent") {
|
|
524
|
+
conditions.push(`r.id ${op} ?`);
|
|
525
|
+
params.push(cursor.id);
|
|
526
|
+
}
|
|
527
|
+
pages.push(statement("r.*", "records r", conditions, params, ` ORDER BY r.id ${dir} LIMIT ?`));
|
|
506
528
|
}
|
|
507
|
-
return
|
|
529
|
+
return pages;
|
|
508
530
|
};
|
|
509
531
|
|
|
510
532
|
// ../sqlite-shared/dist/mappers.js
|
|
@@ -519,23 +541,23 @@ var rowToRecord = (row, associations) => {
|
|
|
519
541
|
content: JSON.parse(row.content),
|
|
520
542
|
version: row.version
|
|
521
543
|
};
|
|
522
|
-
if (row.parent_id)
|
|
544
|
+
if (row.parent_id != null)
|
|
523
545
|
record.parentId = row.parent_id;
|
|
524
|
-
if (row.entity_id)
|
|
546
|
+
if (row.entity_id != null)
|
|
525
547
|
record.entityId = row.entity_id;
|
|
526
|
-
if (row.app_id)
|
|
548
|
+
if (row.app_id != null)
|
|
527
549
|
record.appId = row.app_id;
|
|
528
|
-
if (row.principal_id)
|
|
550
|
+
if (row.principal_id != null)
|
|
529
551
|
record.principalId = row.principal_id;
|
|
530
|
-
if (row.updated_by)
|
|
552
|
+
if (row.updated_by != null)
|
|
531
553
|
record.updatedBy = row.updated_by;
|
|
532
|
-
if (row.updated_via)
|
|
554
|
+
if (row.updated_via != null)
|
|
533
555
|
record.updatedVia = row.updated_via;
|
|
534
|
-
if (row.deleted_at)
|
|
556
|
+
if (row.deleted_at != null)
|
|
535
557
|
record.deletedAt = fromMs(row.deleted_at);
|
|
536
|
-
if (row.unlisted_at)
|
|
558
|
+
if (row.unlisted_at != null)
|
|
537
559
|
record.unlistedAt = fromMs(row.unlisted_at);
|
|
538
|
-
if (row.permissions)
|
|
560
|
+
if (row.permissions != null)
|
|
539
561
|
record.permissions = JSON.parse(row.permissions);
|
|
540
562
|
if (associations.length)
|
|
541
563
|
record.associations = associations;
|
|
@@ -601,15 +623,17 @@ var rowToVersion = (row) => {
|
|
|
601
623
|
content: JSON.parse(row.content),
|
|
602
624
|
updatedAt: fromMs(row.updated_at)
|
|
603
625
|
};
|
|
604
|
-
if (row.entity_id)
|
|
626
|
+
if (row.entity_id != null)
|
|
605
627
|
v.entityId = row.entity_id;
|
|
606
|
-
if (row.updated_by)
|
|
628
|
+
if (row.updated_by != null)
|
|
607
629
|
v.updatedBy = row.updated_by;
|
|
608
|
-
if (row.updated_via)
|
|
630
|
+
if (row.updated_via != null)
|
|
609
631
|
v.updatedVia = row.updated_via;
|
|
610
|
-
if (row.
|
|
632
|
+
if (row.parent_id != null)
|
|
633
|
+
v.parentId = row.parent_id;
|
|
634
|
+
if (row.associations != null)
|
|
611
635
|
v.associations = JSON.parse(row.associations);
|
|
612
|
-
if (row.permissions)
|
|
636
|
+
if (row.permissions != null)
|
|
613
637
|
v.permissions = JSON.parse(row.permissions);
|
|
614
638
|
return v;
|
|
615
639
|
};
|
|
@@ -640,8 +664,7 @@ var asStackQueryError = (query, run) => {
|
|
|
640
664
|
throw new StackQueryError(`Search text could not be parsed: ${err instanceof Error ? err.message : String(err)}`);
|
|
641
665
|
}
|
|
642
666
|
};
|
|
643
|
-
var
|
|
644
|
-
var sortableFieldKinds = (schema) => {
|
|
667
|
+
var indexedFieldKinds = (schema) => {
|
|
645
668
|
const kinds = /* @__PURE__ */ new Map();
|
|
646
669
|
for (const [field, def] of Object.entries(schema)) {
|
|
647
670
|
if (!def.kind || def.kind === "array" || def.kind === "object")
|
|
@@ -653,13 +676,12 @@ var sortableFieldKinds = (schema) => {
|
|
|
653
676
|
var SharedSqlRecordLogic = class {
|
|
654
677
|
deps;
|
|
655
678
|
/**
|
|
656
|
-
* Per-typeId cache of
|
|
657
|
-
* hit the `types` table and
|
|
658
|
-
* Populated eagerly in saveType()
|
|
679
|
+
* Per-typeId cache of the fields content_index covers and their declared
|
|
680
|
+
* kinds, so syncContentIndex() doesn't hit the `types` table and
|
|
681
|
+
* re-parse schema JSON on every write. Populated eagerly in saveType()
|
|
682
|
+
* and lazily via getIndexedFields().
|
|
659
683
|
*/
|
|
660
|
-
|
|
661
|
-
/** Per-typeId cache of sortable field kinds, as fileRefFieldsByType is. */
|
|
662
|
-
sortFieldsByType = /* @__PURE__ */ new Map();
|
|
684
|
+
indexedFieldsByType = /* @__PURE__ */ new Map();
|
|
663
685
|
constructor(deps) {
|
|
664
686
|
this.deps = deps;
|
|
665
687
|
}
|
|
@@ -740,8 +762,7 @@ var SharedSqlRecordLogic = class {
|
|
|
740
762
|
this.insertAssociations(record.id, record.associations);
|
|
741
763
|
}
|
|
742
764
|
fts5Strategy.insert(this.exec, record.id, JSON.stringify(record.content));
|
|
743
|
-
this.
|
|
744
|
-
this.syncContentSort(record.id, record.typeId, record.content);
|
|
765
|
+
this.syncContentIndex(record.id, record.typeId, record.content);
|
|
745
766
|
return record;
|
|
746
767
|
}
|
|
747
768
|
async getRecord(id) {
|
|
@@ -786,8 +807,7 @@ var SharedSqlRecordLogic = class {
|
|
|
786
807
|
id
|
|
787
808
|
]);
|
|
788
809
|
fts5Strategy.insert(this.exec, id, JSON.stringify(merged));
|
|
789
|
-
this.
|
|
790
|
-
this.syncContentSort(id, existing.typeId, merged);
|
|
810
|
+
this.syncContentIndex(id, existing.typeId, merged);
|
|
791
811
|
});
|
|
792
812
|
const updated = await this.getRecord(id);
|
|
793
813
|
if (!updated)
|
|
@@ -842,8 +862,7 @@ var SharedSqlRecordLogic = class {
|
|
|
842
862
|
fts5Strategy.remove(this.exec, id);
|
|
843
863
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
844
864
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
845
|
-
this.exec.run("DELETE FROM
|
|
846
|
-
this.exec.run("DELETE FROM content_sort WHERE record_id = ?", [id]);
|
|
865
|
+
this.exec.run("DELETE FROM content_index WHERE record_id = ?", [id]);
|
|
847
866
|
this.exec.run("DELETE FROM records WHERE id = ?", [id]);
|
|
848
867
|
return purged;
|
|
849
868
|
}
|
|
@@ -904,6 +923,27 @@ var SharedSqlRecordLogic = class {
|
|
|
904
923
|
throw new Error(`Record not found after setUnlisted: "${id}"`);
|
|
905
924
|
return updated;
|
|
906
925
|
}
|
|
926
|
+
async setParent(id, parentId, opts = {}) {
|
|
927
|
+
this.exec.transaction(() => {
|
|
928
|
+
if (opts.snapshot)
|
|
929
|
+
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
930
|
+
const { clause, params: verParams } = this.versionGuard(opts.expectedVersion);
|
|
931
|
+
const changed = this.exec.run(`UPDATE records SET parent_id = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?${clause}`, [
|
|
932
|
+
parentId,
|
|
933
|
+
toMs(/* @__PURE__ */ new Date()),
|
|
934
|
+
opts.updatedBy ?? null,
|
|
935
|
+
opts.updatedVia ?? null,
|
|
936
|
+
id,
|
|
937
|
+
...verParams
|
|
938
|
+
]);
|
|
939
|
+
if (changed === 0)
|
|
940
|
+
this.throwVersionConflict(id, opts.expectedVersion);
|
|
941
|
+
});
|
|
942
|
+
const updated = await this.getRecord(id);
|
|
943
|
+
if (!updated)
|
|
944
|
+
throw new Error(`Record not found after setParent: "${id}"`);
|
|
945
|
+
return updated;
|
|
946
|
+
}
|
|
907
947
|
async restoreVersion(id, version, opts = {}) {
|
|
908
948
|
const existing = await this.getRecord(id);
|
|
909
949
|
if (!existing)
|
|
@@ -916,12 +956,13 @@ var SharedSqlRecordLogic = class {
|
|
|
916
956
|
if (opts.snapshot)
|
|
917
957
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
918
958
|
fts5Strategy.remove(this.exec, id);
|
|
919
|
-
this.exec.run(`UPDATE records SET type_id = ?, content = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?`, [
|
|
959
|
+
this.exec.run(`UPDATE records SET type_id = ?, content = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ?, parent_id = ? WHERE id = ?`, [
|
|
920
960
|
target.typeId,
|
|
921
961
|
JSON.stringify(target.content),
|
|
922
962
|
toMs(/* @__PURE__ */ new Date()),
|
|
923
963
|
opts.updatedBy ?? null,
|
|
924
964
|
opts.updatedVia ?? null,
|
|
965
|
+
target.parentId ?? null,
|
|
925
966
|
id
|
|
926
967
|
]);
|
|
927
968
|
if (target.associations !== void 0) {
|
|
@@ -930,8 +971,7 @@ var SharedSqlRecordLogic = class {
|
|
|
930
971
|
this.insertAssociations(id, target.associations);
|
|
931
972
|
}
|
|
932
973
|
fts5Strategy.insert(this.exec, id, JSON.stringify(target.content));
|
|
933
|
-
this.
|
|
934
|
-
this.syncContentSort(id, target.typeId, target.content);
|
|
974
|
+
this.syncContentIndex(id, target.typeId, target.content);
|
|
935
975
|
});
|
|
936
976
|
const updated = await this.getRecord(id);
|
|
937
977
|
if (!updated)
|
|
@@ -956,8 +996,7 @@ var SharedSqlRecordLogic = class {
|
|
|
956
996
|
id
|
|
957
997
|
]);
|
|
958
998
|
fts5Strategy.insert(this.exec, id, JSON.stringify(content));
|
|
959
|
-
this.
|
|
960
|
-
this.syncContentSort(id, toTypeId, content);
|
|
999
|
+
this.syncContentIndex(id, toTypeId, content);
|
|
961
1000
|
});
|
|
962
1001
|
const updated = await this.getRecord(id);
|
|
963
1002
|
if (!updated)
|
|
@@ -965,26 +1004,25 @@ var SharedSqlRecordLogic = class {
|
|
|
965
1004
|
return updated;
|
|
966
1005
|
}
|
|
967
1006
|
async queryRecords(query) {
|
|
968
|
-
const { sql: from, params: fromParams } = buildFromClause(query);
|
|
969
|
-
const { sql: where, params: whereParams } = buildWhereClause(query);
|
|
970
|
-
const params = [...fromParams, ...whereParams];
|
|
971
|
-
const order = buildOrderClause(query);
|
|
972
1007
|
const limit = query.limit ?? 50;
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1008
|
+
const fetch = limit + 1;
|
|
1009
|
+
const rows = [];
|
|
1010
|
+
for (const page2 of buildQueryPlan(query)) {
|
|
1011
|
+
const budget = fetch - rows.length;
|
|
1012
|
+
if (budget <= 0)
|
|
1013
|
+
break;
|
|
1014
|
+
const statement2 = atBudget(page2, budget);
|
|
1015
|
+
rows.push(...asStackQueryError(query, () => this.exec.all(statement2.sql, statement2.params)));
|
|
1016
|
+
}
|
|
977
1017
|
const hasMore = rows.length > limit;
|
|
978
1018
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
979
1019
|
const records = page.map((row) => {
|
|
980
1020
|
const associations = this.getAssociationsForRecord(row.id);
|
|
981
1021
|
return rowToRecord(row, associations);
|
|
982
1022
|
});
|
|
983
|
-
const countRows = asStackQueryError(query, () => this.exec.all(`SELECT COUNT(*) as total FROM ${from} ${where}`, params));
|
|
984
|
-
const total = countRows[0]?.total ?? 0;
|
|
985
1023
|
const lastRecord = records[records.length - 1];
|
|
986
1024
|
const cursor = hasMore && lastRecord ? makeCursor(lastRecord, query.sort, (record, field) => this.sortFieldKind(record, field)) : null;
|
|
987
|
-
return { records, cursor
|
|
1025
|
+
return { records, cursor };
|
|
988
1026
|
}
|
|
989
1027
|
/**
|
|
990
1028
|
* Atomically verify fileId is unreferenced, then hard-delete its
|
|
@@ -997,7 +1035,7 @@ var SharedSqlRecordLogic = class {
|
|
|
997
1035
|
return this.exec.transaction(() => {
|
|
998
1036
|
const referenced = this.exec.all(`SELECT 1 as found FROM associations WHERE kind = 'attachment' AND file_id = ?
|
|
999
1037
|
UNION ALL
|
|
1000
|
-
SELECT 1 FROM
|
|
1038
|
+
SELECT 1 FROM content_index WHERE file_id = ?
|
|
1001
1039
|
LIMIT 1`, [fileId, fileId]);
|
|
1002
1040
|
if (referenced.length) {
|
|
1003
1041
|
throw new StackConflictError("Attachment is still referenced by one or more records");
|
|
@@ -1037,8 +1075,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1037
1075
|
try {
|
|
1038
1076
|
this.exec.run(`INSERT INTO versions
|
|
1039
1077
|
(record_id, version, type_id, content, updated_at, entity_id,
|
|
1040
|
-
updated_by, updated_via, associations, permissions)
|
|
1041
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1078
|
+
updated_by, updated_via, parent_id, associations, permissions)
|
|
1079
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1042
1080
|
id,
|
|
1043
1081
|
version.version,
|
|
1044
1082
|
version.typeId,
|
|
@@ -1047,6 +1085,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1047
1085
|
version.entityId ?? null,
|
|
1048
1086
|
version.updatedBy ?? null,
|
|
1049
1087
|
version.updatedVia ?? null,
|
|
1088
|
+
version.parentId ?? null,
|
|
1050
1089
|
version.associations ? JSON.stringify(version.associations) : null,
|
|
1051
1090
|
version.permissions ? JSON.stringify(version.permissions) : null
|
|
1052
1091
|
]);
|
|
@@ -1057,14 +1096,24 @@ var SharedSqlRecordLogic = class {
|
|
|
1057
1096
|
throw err;
|
|
1058
1097
|
}
|
|
1059
1098
|
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Replaces every column insertVersionRow writes, so an overwritten row is
|
|
1101
|
+
* indistinguishable from a freshly inserted one — a column left out here
|
|
1102
|
+
* would keep the replaced row's value and surface later as a restore
|
|
1103
|
+
* putting back something the snapshot never said.
|
|
1104
|
+
*/
|
|
1060
1105
|
overwriteVersionRow(id, version) {
|
|
1061
1106
|
this.exec.run(`UPDATE versions
|
|
1062
|
-
SET type_id = ?, content = ?, updated_at = ?, entity_id = ?,
|
|
1107
|
+
SET type_id = ?, content = ?, updated_at = ?, entity_id = ?,
|
|
1108
|
+
updated_by = ?, updated_via = ?, parent_id = ?, associations = ?, permissions = ?
|
|
1063
1109
|
WHERE record_id = ? AND version = ?`, [
|
|
1064
1110
|
version.typeId,
|
|
1065
1111
|
JSON.stringify(version.content),
|
|
1066
1112
|
toMs(version.updatedAt),
|
|
1067
1113
|
version.entityId ?? null,
|
|
1114
|
+
version.updatedBy ?? null,
|
|
1115
|
+
version.updatedVia ?? null,
|
|
1116
|
+
version.parentId ?? null,
|
|
1068
1117
|
version.associations ? JSON.stringify(version.associations) : null,
|
|
1069
1118
|
version.permissions ? JSON.stringify(version.permissions) : null,
|
|
1070
1119
|
id,
|
|
@@ -1117,8 +1166,7 @@ var SharedSqlRecordLogic = class {
|
|
|
1117
1166
|
toMs(type.createdAt)
|
|
1118
1167
|
]);
|
|
1119
1168
|
const schema = type.schema;
|
|
1120
|
-
this.
|
|
1121
|
-
this.sortFieldsByType.set(type.id, sortableFieldKinds(schema));
|
|
1169
|
+
this.indexedFieldsByType.set(type.id, indexedFieldKinds(schema));
|
|
1122
1170
|
}
|
|
1123
1171
|
async getType(id) {
|
|
1124
1172
|
const rows = this.exec.all("SELECT * FROM types WHERE id = ?", [id]);
|
|
@@ -1194,67 +1242,49 @@ var SharedSqlRecordLogic = class {
|
|
|
1194
1242
|
return rows.map(rowToAssociation);
|
|
1195
1243
|
}
|
|
1196
1244
|
/**
|
|
1197
|
-
*
|
|
1198
|
-
* back to a `types` lookup (and populating the cache) only for
|
|
1199
|
-
* this process hasn't seen via saveType() yet, e.g. right after
|
|
1200
|
-
|
|
1201
|
-
getFileRefFields(typeId) {
|
|
1202
|
-
const cached = this.fileRefFieldsByType.get(typeId);
|
|
1203
|
-
if (cached)
|
|
1204
|
-
return cached;
|
|
1205
|
-
const typeRow = this.exec.get("SELECT schema FROM types WHERE id = ?", [
|
|
1206
|
-
typeId
|
|
1207
|
-
]);
|
|
1208
|
-
const fields = typeRow ? fileRefFieldNames(JSON.parse(typeRow.schema)) : [];
|
|
1209
|
-
this.fileRefFieldsByType.set(typeId, fields);
|
|
1210
|
-
return fields;
|
|
1211
|
-
}
|
|
1212
|
-
/**
|
|
1213
|
-
* Replace a record's file_refs rows with whatever its content currently
|
|
1214
|
-
* holds in top-level file-ref fields, on every write that can change
|
|
1215
|
-
* content or typeId, so the index never drifts. Only top-level scalars
|
|
1216
|
-
* are indexed; the schema lookup is cached (fileRefFieldsByType).
|
|
1245
|
+
* The fields content_index covers for typeId, from the in-memory cache
|
|
1246
|
+
* — falling back to a `types` lookup (and populating the cache) only for
|
|
1247
|
+
* a typeId this process hasn't seen via saveType() yet, e.g. right after
|
|
1248
|
+
* open().
|
|
1217
1249
|
*/
|
|
1218
|
-
|
|
1219
|
-
const cached = this.
|
|
1250
|
+
getIndexedFields(typeId) {
|
|
1251
|
+
const cached = this.indexedFieldsByType.get(typeId);
|
|
1220
1252
|
if (cached)
|
|
1221
1253
|
return cached;
|
|
1222
1254
|
const typeRow = this.exec.get("SELECT schema FROM types WHERE id = ?", [
|
|
1223
1255
|
typeId
|
|
1224
1256
|
]);
|
|
1225
|
-
const fields = typeRow ?
|
|
1226
|
-
this.
|
|
1257
|
+
const fields = typeRow ? indexedFieldKinds(JSON.parse(typeRow.schema)) : /* @__PURE__ */ new Map();
|
|
1258
|
+
this.indexedFieldsByType.set(typeId, fields);
|
|
1227
1259
|
return fields;
|
|
1228
1260
|
}
|
|
1229
1261
|
/**
|
|
1230
|
-
* Replace a record's
|
|
1231
|
-
* holds, on every write that can change content or typeId
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1262
|
+
* Replace a record's content_index rows with what its content currently
|
|
1263
|
+
* holds, on every write that can change content or typeId, so neither
|
|
1264
|
+
* the ordering nor the file-reference lookup can drift from the record.
|
|
1265
|
+
* A field holding no orderable value gets no row at all, which is what
|
|
1266
|
+
* puts the record at the end of a sort on that field.
|
|
1234
1267
|
*/
|
|
1235
|
-
|
|
1236
|
-
this.exec.run("DELETE FROM
|
|
1237
|
-
for (const [field, kind] of this.
|
|
1268
|
+
syncContentIndex(recordId, typeId, content) {
|
|
1269
|
+
this.exec.run("DELETE FROM content_index WHERE record_id = ?", [recordId]);
|
|
1270
|
+
for (const [field, kind] of this.getIndexedFields(typeId)) {
|
|
1238
1271
|
const entry = contentSortEntry(kind, content[field]);
|
|
1239
1272
|
if (!entry)
|
|
1240
1273
|
continue;
|
|
1241
|
-
|
|
1242
|
-
|
|
1274
|
+
const sql = `INSERT OR IGNORE INTO content_index
|
|
1275
|
+
(record_id, field, value_rank, num_value, text_key, text_value, file_id)
|
|
1276
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`;
|
|
1277
|
+
if (entry.kind === "num") {
|
|
1278
|
+
this.exec.run(sql, [recordId, field, 0, entry.num, null, null, null]);
|
|
1279
|
+
} else {
|
|
1280
|
+
const fileId = kind === "file-ref" ? entry.text : null;
|
|
1281
|
+
this.exec.run(sql, [recordId, field, 1, null, entry.key, entry.text, fileId]);
|
|
1282
|
+
}
|
|
1243
1283
|
}
|
|
1244
1284
|
}
|
|
1245
1285
|
/** The declared kind of a record's top-level `field`, if it has one. */
|
|
1246
1286
|
sortFieldKind(record, field) {
|
|
1247
|
-
return this.
|
|
1248
|
-
}
|
|
1249
|
-
syncFileRefs(recordId, typeId, content) {
|
|
1250
|
-
this.exec.run("DELETE FROM file_refs WHERE record_id = ?", [recordId]);
|
|
1251
|
-
const fields = this.getFileRefFields(typeId);
|
|
1252
|
-
for (const field of fields) {
|
|
1253
|
-
const value = content[field];
|
|
1254
|
-
if (typeof value === "string") {
|
|
1255
|
-
this.exec.run("INSERT OR IGNORE INTO file_refs (record_id, field, file_id) VALUES (?, ?, ?)", [recordId, field, value]);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1287
|
+
return this.getIndexedFields(record.typeId).get(field);
|
|
1258
1288
|
}
|
|
1259
1289
|
};
|
|
1260
1290
|
|
|
@@ -1364,6 +1394,9 @@ var DoSQLiteRecordAdapter = class _DoSQLiteRecordAdapter {
|
|
|
1364
1394
|
setUnlisted(id, unlisted, opts) {
|
|
1365
1395
|
return this.record.setUnlisted(id, unlisted, opts);
|
|
1366
1396
|
}
|
|
1397
|
+
setParent(id, parentId, opts) {
|
|
1398
|
+
return this.record.setParent(id, parentId, opts);
|
|
1399
|
+
}
|
|
1367
1400
|
restoreVersion(id, version, opts) {
|
|
1368
1401
|
return this.record.restoreVersion(id, version, opts);
|
|
1369
1402
|
}
|