@haverstack/record-adapter-sqlite 0.29.0 → 0.31.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 +21 -22
- package/dist/index.js +91 -78
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { StackRecordAdapter,
|
|
3
|
-
import { StackTokenStore, TokenInfo } from '@haverstack/core/wire';
|
|
1
|
+
import { StackCapabilities, StackRecord, RecordChangeSet, RecordVersion, ActorOptions, TypeId, StackQuery, QueryResult, FileId, JournalQuery, RecordJournalEntry, StackType, Association, Actor } from '@haverstack/core';
|
|
2
|
+
import { StackRecordAdapter, JournalOptions } from '@haverstack/core/adapter';
|
|
3
|
+
import { StackTokenStore, TokenSession, TokenInfo } from '@haverstack/core/wire';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Normalizes a SQLite binding's call convention (array-bind-then-step,
|
|
@@ -60,33 +60,33 @@ type StackConfig = {
|
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
62
|
declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
|
|
63
|
-
readonly capabilities:
|
|
63
|
+
readonly capabilities: StackCapabilities;
|
|
64
64
|
readonly ownerEntityId: string;
|
|
65
65
|
readonly timezone: string | undefined;
|
|
66
66
|
private readonly record;
|
|
67
67
|
protected constructor(exec: SqlExecutor, config: StackConfig);
|
|
68
68
|
createRecord(record: StackRecord, opts?: JournalOptions): Promise<StackRecord>;
|
|
69
69
|
getRecord(id: string): Promise<StackRecord | null>;
|
|
70
|
-
mutateRecord(id: string, changes:
|
|
71
|
-
|
|
70
|
+
mutateRecord(id: string, changes: RecordChangeSet, opts?: {
|
|
71
|
+
ifVersion?: number;
|
|
72
72
|
snapshot?: RecordVersion;
|
|
73
73
|
bumpsVersion?: boolean;
|
|
74
74
|
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
75
75
|
deleteRecord(id: string, opts?: {
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
purge?: boolean;
|
|
77
|
+
ifVersion?: number;
|
|
78
78
|
snapshot?: RecordVersion;
|
|
79
79
|
} & ActorOptions & JournalOptions): Promise<StackRecord | null>;
|
|
80
80
|
undeleteRecord(id: string, opts?: {
|
|
81
|
-
|
|
81
|
+
ifVersion?: number;
|
|
82
82
|
snapshot?: RecordVersion;
|
|
83
83
|
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
84
84
|
restoreVersion(id: string, version: number, opts?: {
|
|
85
|
-
|
|
85
|
+
ifVersion?: number;
|
|
86
86
|
snapshot?: RecordVersion;
|
|
87
87
|
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
88
88
|
commitMigration(id: string, toTypeId: TypeId, content: Record<string, unknown>, opts?: {
|
|
89
|
-
|
|
89
|
+
ifVersion?: number;
|
|
90
90
|
snapshot?: RecordVersion;
|
|
91
91
|
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
92
92
|
queryRecords(query: StackQuery): Promise<QueryResult>;
|
|
@@ -120,7 +120,7 @@ declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
|
|
|
120
120
|
|
|
121
121
|
/** Conventional sibling path for a token store beside a stack's main .db file. */
|
|
122
122
|
declare const defaultTokenStorePath: (dbPath: string) => string;
|
|
123
|
-
type
|
|
123
|
+
type NativeTokenStoreOpenOptions = {
|
|
124
124
|
/** Absolute path to the token store file. Created if it doesn't exist. */
|
|
125
125
|
path: string;
|
|
126
126
|
/** Bypass the storage-ownership lock check. See docs/spec/adapters.md § Concurrency & storage ownership. */
|
|
@@ -132,9 +132,8 @@ declare class NativeTokenStore implements StackTokenStore {
|
|
|
132
132
|
private tokens;
|
|
133
133
|
private constructor();
|
|
134
134
|
/** Opens the token store, creating the file and schema if needed. */
|
|
135
|
-
static open(opts:
|
|
136
|
-
createToken(
|
|
137
|
-
onBehalfOf?: string;
|
|
135
|
+
static open(opts: NativeTokenStoreOpenOptions): Promise<NativeTokenStore>;
|
|
136
|
+
createToken(actor: Actor, opts?: {
|
|
138
137
|
label?: string;
|
|
139
138
|
expiresAt?: Date;
|
|
140
139
|
}): Promise<{
|
|
@@ -174,17 +173,17 @@ declare class NativeTokenStore implements StackTokenStore {
|
|
|
174
173
|
* spread-args run/get/all calls to it).
|
|
175
174
|
*/
|
|
176
175
|
|
|
177
|
-
type
|
|
176
|
+
type NativeSQLiteRecordAdapterInitializeOptions = {
|
|
178
177
|
/** Absolute path to the .db file. Must not already exist. */
|
|
179
178
|
path: string;
|
|
180
179
|
/** IANA timezone string e.g. "America/New_York". Optional passthrough app metadata — no default. */
|
|
181
180
|
timezone?: string;
|
|
182
181
|
/** Entity ID of the stack owner. */
|
|
183
|
-
|
|
184
|
-
/** Bypass the storage-ownership lock check. See
|
|
182
|
+
ownerEntityId: string;
|
|
183
|
+
/** Bypass the storage-ownership lock check. See NativeSQLiteRecordAdapterOpenOptions.force. */
|
|
185
184
|
force?: boolean;
|
|
186
185
|
};
|
|
187
|
-
type
|
|
186
|
+
type NativeSQLiteRecordAdapterOpenOptions = {
|
|
188
187
|
/** Absolute path to an existing .db file. */
|
|
189
188
|
path: string;
|
|
190
189
|
/**
|
|
@@ -210,15 +209,15 @@ declare class NativeSQLiteRecordAdapter extends SharedSqlRecordAdapter {
|
|
|
210
209
|
* Initialize a new stack database. Fails if the file already exists —
|
|
211
210
|
* use open() for existing databases.
|
|
212
211
|
*/
|
|
213
|
-
static initialize(opts:
|
|
212
|
+
static initialize(opts: NativeSQLiteRecordAdapterInitializeOptions): Promise<NativeSQLiteRecordAdapter>;
|
|
214
213
|
/**
|
|
215
214
|
* Open an existing stack database. Fails if the file does not exist —
|
|
216
215
|
* use initialize() for new databases.
|
|
217
216
|
*/
|
|
218
|
-
static open(opts:
|
|
217
|
+
static open(opts: NativeSQLiteRecordAdapterOpenOptions): Promise<NativeSQLiteRecordAdapter>;
|
|
219
218
|
/** Folds the WAL back into the main file — useful before copying/backing up the database. */
|
|
220
219
|
flush(): Promise<void>;
|
|
221
220
|
close(): Promise<void>;
|
|
222
221
|
}
|
|
223
222
|
|
|
224
|
-
export { type
|
|
223
|
+
export { NativeSQLiteRecordAdapter, type NativeSQLiteRecordAdapterInitializeOptions, type NativeSQLiteRecordAdapterOpenOptions, NativeTokenStore, type NativeTokenStoreOpenOptions, defaultTokenStorePath };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
2
|
-
import { StackNotFoundError, StackConflictError, applyMergePatch, StackVersionConflictError,
|
|
2
|
+
import { StackNotFoundError, StackConflictError, applyMergePatch, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
3
3
|
import { contentSortEntry, parseContentFilterKey, contentSortKey } from '@haverstack/core/adapter';
|
|
4
4
|
import { randomBytes, createHash } from 'crypto';
|
|
5
5
|
|
|
@@ -75,6 +75,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
75
75
|
content TEXT NOT NULL CHECK (json_valid(content)),
|
|
76
76
|
updated_at INTEGER NOT NULL,
|
|
77
77
|
entity_id TEXT,
|
|
78
|
+
principal_id TEXT,
|
|
78
79
|
updated_by TEXT,
|
|
79
80
|
updated_via TEXT,
|
|
80
81
|
PRIMARY KEY (record_id, version)
|
|
@@ -91,7 +92,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
91
92
|
at INTEGER NOT NULL,
|
|
92
93
|
-- No 'purged': a purge destroys the log rather than appending to it,
|
|
93
94
|
-- so the constraint names exactly the kinds an entry can carry.
|
|
94
|
-
-- See docs/spec/journal.md \xA7 A
|
|
95
|
+
-- See docs/spec/journal.md \xA7 A purge destroys the journal.
|
|
95
96
|
kind TEXT NOT NULL CHECK (kind IN ('created', 'changed', 'deleted')),
|
|
96
97
|
ops TEXT NOT NULL CHECK (json_valid(ops)),
|
|
97
98
|
version INTEGER NOT NULL,
|
|
@@ -217,7 +218,7 @@ var fromBase64 = (cursor) => {
|
|
|
217
218
|
};
|
|
218
219
|
var encodeCursor = (decoded) => toBase64(JSON.stringify(decoded.kind === "absent" ? { k: decoded.kind, f: decoded.field, i: decoded.id } : { k: decoded.kind, f: decoded.field, i: decoded.id, v: decoded.value }));
|
|
219
220
|
var decodeCursor = (cursor) => {
|
|
220
|
-
const malformed = () => new
|
|
221
|
+
const malformed = () => new StackBadRequestError(`Invalid cursor: malformed "${cursor}"`);
|
|
221
222
|
let parsed;
|
|
222
223
|
try {
|
|
223
224
|
parsed = JSON.parse(fromBase64(cursor));
|
|
@@ -238,11 +239,11 @@ var decodeCursor = (cursor) => {
|
|
|
238
239
|
}
|
|
239
240
|
if (k === "num" || k === "native") {
|
|
240
241
|
if (typeof v !== "number" || !isFinite(v)) {
|
|
241
|
-
throw new
|
|
242
|
+
throw new StackBadRequestError(`Invalid cursor: non-numeric sort value`);
|
|
242
243
|
}
|
|
243
244
|
if (k === "native") {
|
|
244
245
|
if (!SORT_FIELDS.includes(f)) {
|
|
245
|
-
throw new
|
|
246
|
+
throw new StackBadRequestError(`Invalid cursor: unknown sort field "${f}"`);
|
|
246
247
|
}
|
|
247
248
|
return { kind: "native", field: f, value: v, id: i };
|
|
248
249
|
}
|
|
@@ -376,7 +377,7 @@ var atBudget = (page, rows) => ({
|
|
|
376
377
|
var sqlDirection = (query) => {
|
|
377
378
|
const dir = query.sort?.direction ?? "desc";
|
|
378
379
|
if (dir !== "asc" && dir !== "desc") {
|
|
379
|
-
throw new
|
|
380
|
+
throw new StackBadRequestError(`Invalid sort direction "${dir}": expected "asc" or "desc".`);
|
|
380
381
|
}
|
|
381
382
|
return dir === "asc" ? "ASC" : "DESC";
|
|
382
383
|
};
|
|
@@ -401,7 +402,7 @@ var contentPathExists = (segments, leaf, params, nextAlias) => {
|
|
|
401
402
|
};
|
|
402
403
|
var equalsLeaf = (alias) => `${alias}.type NOT IN ('object', 'array') AND ${alias}.value = ?`;
|
|
403
404
|
var SORT_ALIAS = "cs";
|
|
404
|
-
var cursorMismatch = (cursor, field) => new
|
|
405
|
+
var cursorMismatch = (cursor, field) => new StackBadRequestError(`Cursor sort field "${cursor.field}" does not match query sort field "${field}"`);
|
|
405
406
|
var nativeCursor = (cursor, field) => {
|
|
406
407
|
if (cursor.kind !== "native" || cursor.field !== field)
|
|
407
408
|
throw cursorMismatch(cursor, field);
|
|
@@ -449,8 +450,8 @@ var recordConditions = (query) => {
|
|
|
449
450
|
}
|
|
450
451
|
}
|
|
451
452
|
inFilter("app_id", f.appId);
|
|
452
|
-
inFilter("entity_id", f.
|
|
453
|
-
inFilter("principal_id", f.principalId);
|
|
453
|
+
inFilter("entity_id", f.createdBy?.subjectId);
|
|
454
|
+
inFilter("principal_id", f.createdBy?.principalId);
|
|
454
455
|
rangeFilter("created_at", f.createdAt);
|
|
455
456
|
rangeFilter("updated_at", f.updatedAt);
|
|
456
457
|
const hasAssociation = (kind, clauses) => `r.id IN (SELECT a.record_id FROM associations a WHERE a.kind = '${kind}'` + clauses.map((c) => ` AND ${c}`).join("") + `)`;
|
|
@@ -460,14 +461,22 @@ var recordConditions = (query) => {
|
|
|
460
461
|
params.push(tag);
|
|
461
462
|
}
|
|
462
463
|
}
|
|
463
|
-
if (f.
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
if (f.attachment) {
|
|
465
|
+
const clauses = [];
|
|
466
|
+
if (f.attachment.label !== void 0) {
|
|
467
|
+
clauses.push("a.label = ?");
|
|
468
|
+
params.push(f.attachment.label);
|
|
469
|
+
}
|
|
470
|
+
if (f.attachment.fileId !== void 0) {
|
|
471
|
+
clauses.push("a.file_id = ?");
|
|
472
|
+
params.push(f.attachment.fileId);
|
|
473
|
+
}
|
|
474
|
+
conditions.push(hasAssociation("attachment", clauses));
|
|
466
475
|
}
|
|
467
|
-
if (f.
|
|
476
|
+
if (f.referencesFileId) {
|
|
468
477
|
conditions.push(`(${hasAssociation("attachment", ["a.file_id = ?"])}
|
|
469
478
|
OR r.id IN (SELECT ci.record_id FROM content_index ci WHERE ci.file_id = ?))`);
|
|
470
|
-
params.push(f.
|
|
479
|
+
params.push(f.referencesFileId, f.referencesFileId);
|
|
471
480
|
}
|
|
472
481
|
if (f.relatedTo) {
|
|
473
482
|
const clauses = [];
|
|
@@ -478,11 +487,11 @@ var recordConditions = (query) => {
|
|
|
478
487
|
}
|
|
479
488
|
if (target) {
|
|
480
489
|
clauses.push("a.related_scope = ?");
|
|
481
|
-
params.push(target.
|
|
482
|
-
if (target.
|
|
490
|
+
params.push(target.kind);
|
|
491
|
+
if (target.kind === "record") {
|
|
483
492
|
clauses.push("a.related_ns = ?", "a.related_id = ?", "a.related_stack = ?");
|
|
484
493
|
params.push("", target.recordId, target.stackUrl ?? "");
|
|
485
|
-
} else if (target.
|
|
494
|
+
} else if (target.kind === "entity") {
|
|
486
495
|
clauses.push("a.related_ns = ?", "a.related_id = ?");
|
|
487
496
|
params.push("", target.entityId);
|
|
488
497
|
} else {
|
|
@@ -587,6 +596,10 @@ var buildQueryPlan = (query) => {
|
|
|
587
596
|
|
|
588
597
|
// ../sqlite-shared/dist/mappers.js
|
|
589
598
|
var toMs = (d) => d.getTime();
|
|
599
|
+
var actorFromColumns = (subject, principal) => subject == null ? void 0 : {
|
|
600
|
+
subjectId: subject,
|
|
601
|
+
...principal != null && { principalId: principal }
|
|
602
|
+
};
|
|
590
603
|
var fromMs = (ms) => new Date(ms);
|
|
591
604
|
var partitionAssociations = (associations) => ({
|
|
592
605
|
associations: associations.filter((a) => a.kind !== "permission" && a.kind !== "anyone"),
|
|
@@ -603,16 +616,14 @@ var rowToRecord = (row, associations) => {
|
|
|
603
616
|
};
|
|
604
617
|
if (row.parent_id != null)
|
|
605
618
|
record.parentId = row.parent_id;
|
|
606
|
-
if (row.entity_id != null)
|
|
607
|
-
record.entityId = row.entity_id;
|
|
608
619
|
if (row.app_id != null)
|
|
609
620
|
record.appId = row.app_id;
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
record.
|
|
621
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
622
|
+
if (createdBy)
|
|
623
|
+
record.createdBy = createdBy;
|
|
624
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
625
|
+
if (updatedBy)
|
|
626
|
+
record.updatedBy = updatedBy;
|
|
616
627
|
if (row.deleted_at != null)
|
|
617
628
|
record.deletedAt = fromMs(row.deleted_at);
|
|
618
629
|
if (row.unlisted_at != null)
|
|
@@ -661,31 +672,31 @@ var associationKeyColumns = (a) => {
|
|
|
661
672
|
return [a.fileId, "", "", "", "", ""];
|
|
662
673
|
if (a.kind === "permission") {
|
|
663
674
|
const g = a.grantee;
|
|
664
|
-
return g.
|
|
675
|
+
return g.kind === "entity" ? ["", "entity", g.entityId, "", "", ""] : ["", "group", g.groupId, "", "", g.role];
|
|
665
676
|
}
|
|
666
677
|
if (a.kind !== "relationship")
|
|
667
678
|
return ["", "", "", "", "", ""];
|
|
668
679
|
const t = a.target;
|
|
669
|
-
if (t.
|
|
680
|
+
if (t.kind === "entity")
|
|
670
681
|
return ["", "entity", t.entityId, "", "", ""];
|
|
671
|
-
if (t.
|
|
682
|
+
if (t.kind === "external")
|
|
672
683
|
return ["", "external", t.id, t.ns, "", ""];
|
|
673
684
|
return ["", "record", t.recordId, "", t.stackUrl ?? "", ""];
|
|
674
685
|
};
|
|
675
686
|
var rowToGrantee = (row) => row.related_scope === "group" ? {
|
|
676
|
-
|
|
687
|
+
kind: "group",
|
|
677
688
|
groupId: row.related_id,
|
|
678
689
|
role: row.related_role
|
|
679
|
-
} : {
|
|
690
|
+
} : { kind: "entity", entityId: row.related_id };
|
|
680
691
|
var rowToTarget = (row) => {
|
|
681
692
|
const id = row.related_id;
|
|
682
693
|
if (row.related_scope === "entity")
|
|
683
|
-
return {
|
|
694
|
+
return { kind: "entity", entityId: id };
|
|
684
695
|
if (row.related_scope === "external") {
|
|
685
|
-
return {
|
|
696
|
+
return { kind: "external", ns: row.related_ns, id };
|
|
686
697
|
}
|
|
687
698
|
const stackUrl = row.related_stack;
|
|
688
|
-
return {
|
|
699
|
+
return { kind: "record", recordId: id, ...stackUrl && { stackUrl } };
|
|
689
700
|
};
|
|
690
701
|
var rowToType = (row) => {
|
|
691
702
|
const t = {
|
|
@@ -708,12 +719,12 @@ var rowToVersion = (row) => {
|
|
|
708
719
|
content: JSON.parse(row.content),
|
|
709
720
|
updatedAt: fromMs(row.updated_at)
|
|
710
721
|
};
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (
|
|
716
|
-
v.
|
|
722
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
723
|
+
if (createdBy)
|
|
724
|
+
v.createdBy = createdBy;
|
|
725
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
726
|
+
if (updatedBy)
|
|
727
|
+
v.updatedBy = updatedBy;
|
|
717
728
|
return v;
|
|
718
729
|
};
|
|
719
730
|
var rowToJournalEntry = (row) => {
|
|
@@ -762,13 +773,13 @@ var readStackConfig = (exec) => {
|
|
|
762
773
|
throw new Error("Stack database is missing its config record.");
|
|
763
774
|
return config;
|
|
764
775
|
};
|
|
765
|
-
var
|
|
776
|
+
var asStackBadRequestError = (query, run) => {
|
|
766
777
|
try {
|
|
767
778
|
return run();
|
|
768
779
|
} catch (err) {
|
|
769
780
|
if (!query.filter?.search)
|
|
770
781
|
throw err;
|
|
771
|
-
throw new
|
|
782
|
+
throw new StackBadRequestError(`Search text could not be parsed: ${err instanceof Error ? err.message : String(err)}`);
|
|
772
783
|
}
|
|
773
784
|
};
|
|
774
785
|
var versionConflict = (id, expected, actual) => new StackVersionConflictError(`Record "${id}" is at version ${actual}, expected ${expected}`, id, expected, actual);
|
|
@@ -777,6 +788,7 @@ var VERSION_COLUMNS = [
|
|
|
777
788
|
"content",
|
|
778
789
|
"updated_at",
|
|
779
790
|
"entity_id",
|
|
791
|
+
"principal_id",
|
|
780
792
|
"updated_by",
|
|
781
793
|
"updated_via"
|
|
782
794
|
];
|
|
@@ -784,9 +796,10 @@ var versionRowValues = (version) => [
|
|
|
784
796
|
version.typeId,
|
|
785
797
|
JSON.stringify(version.content),
|
|
786
798
|
toMs(version.updatedAt),
|
|
787
|
-
version.
|
|
788
|
-
version.
|
|
789
|
-
version.
|
|
799
|
+
version.createdBy?.subjectId ?? null,
|
|
800
|
+
version.createdBy?.principalId ?? null,
|
|
801
|
+
version.updatedBy?.subjectId ?? null,
|
|
802
|
+
version.updatedBy?.principalId ?? null
|
|
790
803
|
];
|
|
791
804
|
var indexedFieldKinds = (schema) => {
|
|
792
805
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -815,7 +828,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
815
828
|
/**
|
|
816
829
|
* Precondition check for a mutation that has already read the record —
|
|
817
830
|
* either because it can't fold the check into its own statement
|
|
818
|
-
* (
|
|
831
|
+
* (purgeRecord: children reference the row, so the DELETE can't
|
|
819
832
|
* carry it), or because it has to settle the precondition *before*
|
|
820
833
|
* fts5Strategy.remove() runs, which needs the records row still holding
|
|
821
834
|
* the old content. In the latter case versionedUpdate() re-checks it
|
|
@@ -844,7 +857,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
844
857
|
/**
|
|
845
858
|
* The one shape every mutating UPDATE in this class has: whatever columns
|
|
846
859
|
* the caller is changing, then the version bump and the actor/timestamp
|
|
847
|
-
* stamp that every mutation owes, gated on the opt-in `
|
|
860
|
+
* stamp that every mutation owes, gated on the opt-in `ifVersion`
|
|
848
861
|
* precondition and failing loudly when nothing matched.
|
|
849
862
|
*
|
|
850
863
|
* The guard rides in the WHERE clause even where the caller has already
|
|
@@ -853,17 +866,17 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
853
866
|
* rather than silently overwritten.
|
|
854
867
|
*/
|
|
855
868
|
versionedUpdate(id, sets, values, opts, now = toMs(/* @__PURE__ */ new Date())) {
|
|
856
|
-
const guarded = opts.
|
|
869
|
+
const guarded = opts.ifVersion !== void 0;
|
|
857
870
|
const changed = this.exec.run(`UPDATE records SET ${[...sets, "version = version + 1", "updated_at = ?", "updated_by = ?", "updated_via = ?"].join(", ")} WHERE id = ?${guarded ? " AND version = ?" : ""}`, [
|
|
858
871
|
...values,
|
|
859
872
|
now,
|
|
860
|
-
opts.
|
|
861
|
-
opts.
|
|
873
|
+
opts.actor?.subjectId ?? null,
|
|
874
|
+
opts.actor?.principalId ?? null,
|
|
862
875
|
id,
|
|
863
|
-
...guarded ? [opts.
|
|
876
|
+
...guarded ? [opts.ifVersion] : []
|
|
864
877
|
]);
|
|
865
878
|
if (changed === 0)
|
|
866
|
-
this.throwVersionConflict(id, opts.
|
|
879
|
+
this.throwVersionConflict(id, opts.ifVersion);
|
|
867
880
|
}
|
|
868
881
|
/**
|
|
869
882
|
* Read a record back after its own mutation committed, for the caller to
|
|
@@ -912,11 +925,11 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
912
925
|
JSON.stringify(record.content),
|
|
913
926
|
record.version,
|
|
914
927
|
record.parentId ?? null,
|
|
915
|
-
record.
|
|
928
|
+
record.createdBy?.subjectId ?? null,
|
|
916
929
|
record.appId ?? null,
|
|
917
|
-
record.principalId ?? null,
|
|
918
|
-
record.updatedBy ?? null,
|
|
919
|
-
record.
|
|
930
|
+
record.createdBy?.principalId ?? null,
|
|
931
|
+
record.updatedBy?.subjectId ?? null,
|
|
932
|
+
record.updatedBy?.principalId ?? null,
|
|
920
933
|
record.deletedAt ? toMs(record.deletedAt) : null,
|
|
921
934
|
record.unlistedAt ? toMs(record.unlistedAt) : null
|
|
922
935
|
]);
|
|
@@ -969,13 +982,13 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
969
982
|
* `associations`, `parentId` and/or `unlisted` — Stack computes this from
|
|
970
983
|
* which ops the set actually moves. Such a write still lands its columns,
|
|
971
984
|
* but leaves `version`, `updatedAt` and the actor stamps exactly as they
|
|
972
|
-
* stood and takes no snapshot. `
|
|
985
|
+
* stood and takes no snapshot. `ifVersion`, if given, is re-checked
|
|
973
986
|
* synchronously inside the transaction — the CAS guard the bumping path
|
|
974
987
|
* gets from its UPDATE's WHERE clause, restated as a read because the
|
|
975
988
|
* UPDATE here carries no version predicate. See
|
|
976
989
|
* docs/spec/versioning.md § Version history.
|
|
977
990
|
*
|
|
978
|
-
* The
|
|
991
|
+
* The ifVersion check for a bumping write is standalone rather than
|
|
979
992
|
* folded into the UPDATE alone, because a content change needs
|
|
980
993
|
* fts.remove() to run *before* the records-table content changes. The
|
|
981
994
|
* guard is still in the UPDATE's WHERE, so a writer that slipped in
|
|
@@ -985,7 +998,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
985
998
|
const existing = await this.getRecord(id);
|
|
986
999
|
if (!existing)
|
|
987
1000
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
988
|
-
this.checkExpectedVersion(existing, opts.
|
|
1001
|
+
this.checkExpectedVersion(existing, opts.ifVersion);
|
|
989
1002
|
const now = toMs(/* @__PURE__ */ new Date());
|
|
990
1003
|
const sets = [];
|
|
991
1004
|
const values = [];
|
|
@@ -1002,7 +1015,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1002
1015
|
const current = this.readRecord(id);
|
|
1003
1016
|
if (!current)
|
|
1004
1017
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1005
|
-
this.checkExpectedVersion(current, opts.
|
|
1018
|
+
this.checkExpectedVersion(current, opts.ifVersion);
|
|
1006
1019
|
if (sets.length > 0) {
|
|
1007
1020
|
this.exec.run(`UPDATE records SET ${sets.join(", ")} WHERE id = ?`, [...values, id]);
|
|
1008
1021
|
}
|
|
@@ -1032,8 +1045,8 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1032
1045
|
return this.reread(id, "mutateRecord");
|
|
1033
1046
|
}
|
|
1034
1047
|
async deleteRecord(id, opts = {}) {
|
|
1035
|
-
if (opts.
|
|
1036
|
-
return this.exec.transaction(() => this.
|
|
1048
|
+
if (opts.purge) {
|
|
1049
|
+
return this.exec.transaction(() => this.purgeRecord(id, opts.ifVersion));
|
|
1037
1050
|
}
|
|
1038
1051
|
const now = toMs(/* @__PURE__ */ new Date());
|
|
1039
1052
|
this.exec.transaction(() => {
|
|
@@ -1049,18 +1062,18 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1049
1062
|
* index rows, and row, returning the record as it stood — the last copy that will ever
|
|
1050
1063
|
* exist, read here so that nothing can overtake it between the read and
|
|
1051
1064
|
* the deletes. Null when there was no such record. No write() call —
|
|
1052
|
-
* callers batch it.
|
|
1065
|
+
* callers batch it. ifVersion is checked first so a lost CAS race
|
|
1053
1066
|
* leaves nothing touched (children reference the row, so the check can't
|
|
1054
1067
|
* fold into the final DELETE).
|
|
1055
1068
|
*/
|
|
1056
|
-
|
|
1069
|
+
purgeRecord(id, ifVersion) {
|
|
1057
1070
|
const purged = this.readRecord(id);
|
|
1058
1071
|
if (!purged) {
|
|
1059
|
-
if (
|
|
1072
|
+
if (ifVersion !== void 0)
|
|
1060
1073
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1061
1074
|
return null;
|
|
1062
1075
|
}
|
|
1063
|
-
this.checkExpectedVersion(purged,
|
|
1076
|
+
this.checkExpectedVersion(purged, ifVersion);
|
|
1064
1077
|
fts5Strategy.remove(this.exec, id);
|
|
1065
1078
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
1066
1079
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
@@ -1082,7 +1095,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1082
1095
|
const existing = await this.getRecord(id);
|
|
1083
1096
|
if (!existing)
|
|
1084
1097
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1085
|
-
this.checkExpectedVersion(existing, opts.
|
|
1098
|
+
this.checkExpectedVersion(existing, opts.ifVersion);
|
|
1086
1099
|
const target = await this.getVersion(id, version);
|
|
1087
1100
|
if (!target)
|
|
1088
1101
|
throw new StackNotFoundError(`Version not found: "${id}"@${version}`);
|
|
@@ -1098,7 +1111,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1098
1111
|
const existing = await this.getRecord(id);
|
|
1099
1112
|
if (!existing)
|
|
1100
1113
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1101
|
-
this.checkExpectedVersion(existing, opts.
|
|
1114
|
+
this.checkExpectedVersion(existing, opts.ifVersion);
|
|
1102
1115
|
this.exec.transaction(() => {
|
|
1103
1116
|
if (opts.snapshot)
|
|
1104
1117
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
@@ -1133,7 +1146,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1133
1146
|
if (budget <= 0)
|
|
1134
1147
|
break;
|
|
1135
1148
|
const statement2 = atBudget(page2, budget);
|
|
1136
|
-
rows.push(...
|
|
1149
|
+
rows.push(...asStackBadRequestError(query, () => this.exec.all(statement2.sql, statement2.params)));
|
|
1137
1150
|
}
|
|
1138
1151
|
const hasMore = rows.length > limit;
|
|
1139
1152
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
@@ -1146,7 +1159,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1146
1159
|
return { records, cursor };
|
|
1147
1160
|
}
|
|
1148
1161
|
/**
|
|
1149
|
-
* Atomically verify fileId is unreferenced, then
|
|
1162
|
+
* Atomically verify fileId is unreferenced, then purge its
|
|
1150
1163
|
* metadata records across every typeId in `metadataTypeIds` (see
|
|
1151
1164
|
* deleteUnreferencedAttachmentRecords on the adapter contract). A real
|
|
1152
1165
|
* SQL transaction of synchronous calls — no `await` between the check
|
|
@@ -1168,7 +1181,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1168
1181
|
WHERE type_id IN (${placeholders}) AND json_extract(content, '$.fileId') = ?`, [...metadataTypeIds, fileId]);
|
|
1169
1182
|
const deleted = [];
|
|
1170
1183
|
for (const row of metaRows) {
|
|
1171
|
-
const purged = this.
|
|
1184
|
+
const purged = this.purgeRecord(row.id);
|
|
1172
1185
|
if (purged)
|
|
1173
1186
|
deleted.push(purged);
|
|
1174
1187
|
}
|
|
@@ -1290,9 +1303,9 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1290
1303
|
}
|
|
1291
1304
|
const conditions = ["record_id = ?"];
|
|
1292
1305
|
const values = [id];
|
|
1293
|
-
if (query.
|
|
1306
|
+
if (query.afterSeq !== void 0) {
|
|
1294
1307
|
conditions.push("seq > ?");
|
|
1295
|
-
values.push(query.
|
|
1308
|
+
values.push(query.afterSeq);
|
|
1296
1309
|
}
|
|
1297
1310
|
const rows = 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);
|
|
1298
1311
|
return rows.map(rowToJournalEntry);
|
|
@@ -1612,17 +1625,17 @@ var SharedTokenLogic = class {
|
|
|
1612
1625
|
get exec() {
|
|
1613
1626
|
return this.deps.exec;
|
|
1614
1627
|
}
|
|
1615
|
-
async createToken(
|
|
1628
|
+
async createToken(actor, opts = {}) {
|
|
1616
1629
|
const id = randomBytes(8).toString("hex");
|
|
1617
1630
|
const token = randomBytes(32).toString("hex");
|
|
1618
1631
|
const tokenHash = hashToken(token);
|
|
1619
1632
|
this.exec.run("INSERT INTO tokens (id, token_hash, principal_id, subject_id, label, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [
|
|
1620
1633
|
id,
|
|
1621
1634
|
tokenHash,
|
|
1622
|
-
principalId,
|
|
1623
1635
|
// Stored rather than left null for an undelegated token, so a
|
|
1624
1636
|
// reader never has to know which column stands in for the other.
|
|
1625
|
-
|
|
1637
|
+
actor.principalId ?? actor.subjectId,
|
|
1638
|
+
actor.subjectId,
|
|
1626
1639
|
opts.label ?? null,
|
|
1627
1640
|
toMs(/* @__PURE__ */ new Date()),
|
|
1628
1641
|
opts.expiresAt ? toMs(opts.expiresAt) : null
|
|
@@ -1705,8 +1718,8 @@ var NativeTokenStore = class _NativeTokenStore {
|
|
|
1705
1718
|
store.tokens = new SharedTokenLogic({ exec: new NativeSqliteExecutor(store.db) });
|
|
1706
1719
|
return store;
|
|
1707
1720
|
}
|
|
1708
|
-
createToken(
|
|
1709
|
-
return this.tokens.createToken(
|
|
1721
|
+
createToken(actor, opts) {
|
|
1722
|
+
return this.tokens.createToken(actor, opts);
|
|
1710
1723
|
}
|
|
1711
1724
|
lookupToken(token) {
|
|
1712
1725
|
return this.tokens.lookupToken(token);
|
|
@@ -1756,7 +1769,7 @@ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter extends SharedS
|
|
|
1756
1769
|
);
|
|
1757
1770
|
}
|
|
1758
1771
|
const [db, exec] = _NativeSQLiteRecordAdapter.attach(opts.path, opts.force);
|
|
1759
|
-
const config = insertConfigRecord(exec, opts.
|
|
1772
|
+
const config = insertConfigRecord(exec, opts.ownerEntityId, opts.timezone);
|
|
1760
1773
|
return new _NativeSQLiteRecordAdapter(opts.path, db, exec, config);
|
|
1761
1774
|
}
|
|
1762
1775
|
/**
|