@haverstack/record-adapter-do-sqlite 0.22.0 → 0.24.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 +20 -19
- package/dist/index.js +93 -79
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { StackRecordAdapter,
|
|
1
|
+
import { StackCapabilities, StackRecord, RecordChangeSet, RecordVersion, ActorOptions, TypeId, StackQuery, QueryResult, FileId, JournalQuery, RecordJournalEntry, StackType, Association } from '@haverstack/core';
|
|
2
|
+
import { StackRecordAdapter, JournalOptions } from '@haverstack/core/adapter';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Normalizes a SQLite binding's call convention (array-bind-then-step,
|
|
@@ -59,33 +59,33 @@ type StackConfig = {
|
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
61
|
declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
|
|
62
|
-
readonly capabilities:
|
|
62
|
+
readonly capabilities: StackCapabilities;
|
|
63
63
|
readonly ownerEntityId: string;
|
|
64
64
|
readonly timezone: string | undefined;
|
|
65
65
|
private readonly record;
|
|
66
66
|
protected constructor(exec: SqlExecutor, config: StackConfig);
|
|
67
67
|
createRecord(record: StackRecord, opts?: JournalOptions): Promise<StackRecord>;
|
|
68
68
|
getRecord(id: string): Promise<StackRecord | null>;
|
|
69
|
-
mutateRecord(id: string, changes:
|
|
70
|
-
|
|
69
|
+
mutateRecord(id: string, changes: RecordChangeSet, opts?: {
|
|
70
|
+
ifVersion?: number;
|
|
71
71
|
snapshot?: RecordVersion;
|
|
72
72
|
bumpsVersion?: boolean;
|
|
73
73
|
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
74
74
|
deleteRecord(id: string, opts?: {
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
purge?: boolean;
|
|
76
|
+
ifVersion?: number;
|
|
77
77
|
snapshot?: RecordVersion;
|
|
78
78
|
} & ActorOptions & JournalOptions): Promise<StackRecord | null>;
|
|
79
79
|
undeleteRecord(id: string, opts?: {
|
|
80
|
-
|
|
80
|
+
ifVersion?: number;
|
|
81
81
|
snapshot?: RecordVersion;
|
|
82
82
|
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
83
83
|
restoreVersion(id: string, version: number, opts?: {
|
|
84
|
-
|
|
84
|
+
ifVersion?: number;
|
|
85
85
|
snapshot?: RecordVersion;
|
|
86
86
|
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
87
87
|
commitMigration(id: string, toTypeId: TypeId, content: Record<string, unknown>, opts?: {
|
|
88
|
-
|
|
88
|
+
ifVersion?: number;
|
|
89
89
|
snapshot?: RecordVersion;
|
|
90
90
|
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
91
91
|
queryRecords(query: StackQuery): Promise<QueryResult>;
|
|
@@ -156,20 +156,21 @@ declare class DurableObjectSqliteExecutor implements SqlExecutor {
|
|
|
156
156
|
* SharedSqlRecordAdapter, exactly as it does for record-adapter-sqlite.
|
|
157
157
|
*/
|
|
158
158
|
|
|
159
|
-
type
|
|
159
|
+
type DoSQLiteRecordAdapterOpenOrInitializeOptions = {
|
|
160
160
|
/** Entity ID of the stack owner. Ignored if the DO's storage already has a config record. */
|
|
161
|
-
|
|
161
|
+
ownerEntityId: string;
|
|
162
162
|
/** IANA timezone string e.g. "America/New_York". Optional passthrough app metadata — no default. */
|
|
163
163
|
timezone?: string;
|
|
164
164
|
};
|
|
165
165
|
declare class DoSQLiteRecordAdapter extends SharedSqlRecordAdapter {
|
|
166
166
|
private constructor();
|
|
167
167
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
168
|
+
* Open the adapter for a DO instance, initializing its storage on first
|
|
169
|
+
* use — LocalAdapter.openOrInitialize()'s counterpart. There is no
|
|
170
|
+
* separate initialize()/open() the way file-based adapters need one: a
|
|
171
|
+
* DO id either already has a config record (reattach,
|
|
172
|
+
* opts.ownerEntityId/timezone ignored in favor of what's stored) or it
|
|
173
|
+
* doesn't (first call — opts.ownerEntityId/timezone become the config).
|
|
173
174
|
* Schema DDL is `CREATE TABLE IF NOT EXISTS`, so running it every call
|
|
174
175
|
* is idempotent and cheap.
|
|
175
176
|
*
|
|
@@ -177,11 +178,11 @@ declare class DoSQLiteRecordAdapter extends SharedSqlRecordAdapter {
|
|
|
177
178
|
* outright ("not authorized") — verified against the real runtime, not
|
|
178
179
|
* assumed — hence `wal: false`, unlike record-adapter-sqlite.
|
|
179
180
|
*/
|
|
180
|
-
static
|
|
181
|
+
static openOrInitialize(storage: DurableObjectStorage, opts: DoSQLiteRecordAdapterOpenOrInitializeOptions): Promise<DoSQLiteRecordAdapter>;
|
|
181
182
|
/** No-op: every write through ctx.storage.sql is already durable. */
|
|
182
183
|
flush(): Promise<void>;
|
|
183
184
|
/** No-op: no lock file, no connection to release — the DO's own lifecycle governs storage. */
|
|
184
185
|
close(): Promise<void>;
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
export { type
|
|
188
|
+
export { DoSQLiteRecordAdapter, type DoSQLiteRecordAdapterOpenOrInitializeOptions, DurableObjectSqliteExecutor };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StackNotFoundError, StackConflictError, applyMergePatch, StackVersionConflictError,
|
|
1
|
+
import { StackNotFoundError, StackConflictError, applyMergePatch, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
2
2
|
import { contentSortEntry, parseContentFilterKey, contentSortKey } from '@haverstack/core/adapter';
|
|
3
3
|
|
|
4
4
|
// ../sqlite-shared/dist/schema.js
|
|
@@ -68,6 +68,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
68
68
|
content TEXT NOT NULL CHECK (json_valid(content)),
|
|
69
69
|
updated_at INTEGER NOT NULL,
|
|
70
70
|
entity_id TEXT,
|
|
71
|
+
principal_id TEXT,
|
|
71
72
|
updated_by TEXT,
|
|
72
73
|
updated_via TEXT,
|
|
73
74
|
PRIMARY KEY (record_id, version)
|
|
@@ -84,7 +85,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
84
85
|
at INTEGER NOT NULL,
|
|
85
86
|
-- No 'purged': a purge destroys the log rather than appending to it,
|
|
86
87
|
-- so the constraint names exactly the kinds an entry can carry.
|
|
87
|
-
-- See docs/spec/journal.md \xA7 A
|
|
88
|
+
-- See docs/spec/journal.md \xA7 A purge destroys the journal.
|
|
88
89
|
kind TEXT NOT NULL CHECK (kind IN ('created', 'changed', 'deleted')),
|
|
89
90
|
ops TEXT NOT NULL CHECK (json_valid(ops)),
|
|
90
91
|
version INTEGER NOT NULL,
|
|
@@ -197,7 +198,7 @@ var fromBase64 = (cursor) => {
|
|
|
197
198
|
};
|
|
198
199
|
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 }));
|
|
199
200
|
var decodeCursor = (cursor) => {
|
|
200
|
-
const malformed = () => new
|
|
201
|
+
const malformed = () => new StackBadRequestError(`Invalid cursor: malformed "${cursor}"`);
|
|
201
202
|
let parsed;
|
|
202
203
|
try {
|
|
203
204
|
parsed = JSON.parse(fromBase64(cursor));
|
|
@@ -218,11 +219,11 @@ var decodeCursor = (cursor) => {
|
|
|
218
219
|
}
|
|
219
220
|
if (k === "num" || k === "native") {
|
|
220
221
|
if (typeof v !== "number" || !isFinite(v)) {
|
|
221
|
-
throw new
|
|
222
|
+
throw new StackBadRequestError(`Invalid cursor: non-numeric sort value`);
|
|
222
223
|
}
|
|
223
224
|
if (k === "native") {
|
|
224
225
|
if (!SORT_FIELDS.includes(f)) {
|
|
225
|
-
throw new
|
|
226
|
+
throw new StackBadRequestError(`Invalid cursor: unknown sort field "${f}"`);
|
|
226
227
|
}
|
|
227
228
|
return { kind: "native", field: f, value: v, id: i };
|
|
228
229
|
}
|
|
@@ -356,7 +357,7 @@ var atBudget = (page, rows) => ({
|
|
|
356
357
|
var sqlDirection = (query) => {
|
|
357
358
|
const dir = query.sort?.direction ?? "desc";
|
|
358
359
|
if (dir !== "asc" && dir !== "desc") {
|
|
359
|
-
throw new
|
|
360
|
+
throw new StackBadRequestError(`Invalid sort direction "${dir}": expected "asc" or "desc".`);
|
|
360
361
|
}
|
|
361
362
|
return dir === "asc" ? "ASC" : "DESC";
|
|
362
363
|
};
|
|
@@ -381,7 +382,7 @@ var contentPathExists = (segments, leaf, params, nextAlias) => {
|
|
|
381
382
|
};
|
|
382
383
|
var equalsLeaf = (alias) => `${alias}.type NOT IN ('object', 'array') AND ${alias}.value = ?`;
|
|
383
384
|
var SORT_ALIAS = "cs";
|
|
384
|
-
var cursorMismatch = (cursor, field) => new
|
|
385
|
+
var cursorMismatch = (cursor, field) => new StackBadRequestError(`Cursor sort field "${cursor.field}" does not match query sort field "${field}"`);
|
|
385
386
|
var nativeCursor = (cursor, field) => {
|
|
386
387
|
if (cursor.kind !== "native" || cursor.field !== field)
|
|
387
388
|
throw cursorMismatch(cursor, field);
|
|
@@ -429,8 +430,8 @@ var recordConditions = (query) => {
|
|
|
429
430
|
}
|
|
430
431
|
}
|
|
431
432
|
inFilter("app_id", f.appId);
|
|
432
|
-
inFilter("entity_id", f.
|
|
433
|
-
inFilter("principal_id", f.principalId);
|
|
433
|
+
inFilter("entity_id", f.createdBy?.subjectId);
|
|
434
|
+
inFilter("principal_id", f.createdBy?.principalId);
|
|
434
435
|
rangeFilter("created_at", f.createdAt);
|
|
435
436
|
rangeFilter("updated_at", f.updatedAt);
|
|
436
437
|
const hasAssociation = (kind, clauses) => `r.id IN (SELECT a.record_id FROM associations a WHERE a.kind = '${kind}'` + clauses.map((c) => ` AND ${c}`).join("") + `)`;
|
|
@@ -440,14 +441,22 @@ var recordConditions = (query) => {
|
|
|
440
441
|
params.push(tag);
|
|
441
442
|
}
|
|
442
443
|
}
|
|
443
|
-
if (f.
|
|
444
|
-
|
|
445
|
-
|
|
444
|
+
if (f.attachment) {
|
|
445
|
+
const clauses = [];
|
|
446
|
+
if (f.attachment.label !== void 0) {
|
|
447
|
+
clauses.push("a.label = ?");
|
|
448
|
+
params.push(f.attachment.label);
|
|
449
|
+
}
|
|
450
|
+
if (f.attachment.fileId !== void 0) {
|
|
451
|
+
clauses.push("a.file_id = ?");
|
|
452
|
+
params.push(f.attachment.fileId);
|
|
453
|
+
}
|
|
454
|
+
conditions.push(hasAssociation("attachment", clauses));
|
|
446
455
|
}
|
|
447
|
-
if (f.
|
|
456
|
+
if (f.referencesFileId) {
|
|
448
457
|
conditions.push(`(${hasAssociation("attachment", ["a.file_id = ?"])}
|
|
449
458
|
OR r.id IN (SELECT ci.record_id FROM content_index ci WHERE ci.file_id = ?))`);
|
|
450
|
-
params.push(f.
|
|
459
|
+
params.push(f.referencesFileId, f.referencesFileId);
|
|
451
460
|
}
|
|
452
461
|
if (f.relatedTo) {
|
|
453
462
|
const clauses = [];
|
|
@@ -458,11 +467,11 @@ var recordConditions = (query) => {
|
|
|
458
467
|
}
|
|
459
468
|
if (target) {
|
|
460
469
|
clauses.push("a.related_scope = ?");
|
|
461
|
-
params.push(target.
|
|
462
|
-
if (target.
|
|
470
|
+
params.push(target.kind);
|
|
471
|
+
if (target.kind === "record") {
|
|
463
472
|
clauses.push("a.related_ns = ?", "a.related_id = ?", "a.related_stack = ?");
|
|
464
473
|
params.push("", target.recordId, target.stackUrl ?? "");
|
|
465
|
-
} else if (target.
|
|
474
|
+
} else if (target.kind === "entity") {
|
|
466
475
|
clauses.push("a.related_ns = ?", "a.related_id = ?");
|
|
467
476
|
params.push("", target.entityId);
|
|
468
477
|
} else {
|
|
@@ -567,6 +576,10 @@ var buildQueryPlan = (query) => {
|
|
|
567
576
|
|
|
568
577
|
// ../sqlite-shared/dist/mappers.js
|
|
569
578
|
var toMs = (d) => d.getTime();
|
|
579
|
+
var actorFromColumns = (subject, principal) => subject == null ? void 0 : {
|
|
580
|
+
subjectId: subject,
|
|
581
|
+
...principal != null && { principalId: principal }
|
|
582
|
+
};
|
|
570
583
|
var fromMs = (ms) => new Date(ms);
|
|
571
584
|
var partitionAssociations = (associations) => ({
|
|
572
585
|
associations: associations.filter((a) => a.kind !== "permission" && a.kind !== "anyone"),
|
|
@@ -583,16 +596,14 @@ var rowToRecord = (row, associations) => {
|
|
|
583
596
|
};
|
|
584
597
|
if (row.parent_id != null)
|
|
585
598
|
record.parentId = row.parent_id;
|
|
586
|
-
if (row.entity_id != null)
|
|
587
|
-
record.entityId = row.entity_id;
|
|
588
599
|
if (row.app_id != null)
|
|
589
600
|
record.appId = row.app_id;
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (
|
|
595
|
-
record.
|
|
601
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
602
|
+
if (createdBy)
|
|
603
|
+
record.createdBy = createdBy;
|
|
604
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
605
|
+
if (updatedBy)
|
|
606
|
+
record.updatedBy = updatedBy;
|
|
596
607
|
if (row.deleted_at != null)
|
|
597
608
|
record.deletedAt = fromMs(row.deleted_at);
|
|
598
609
|
if (row.unlisted_at != null)
|
|
@@ -641,31 +652,31 @@ var associationKeyColumns = (a) => {
|
|
|
641
652
|
return [a.fileId, "", "", "", "", ""];
|
|
642
653
|
if (a.kind === "permission") {
|
|
643
654
|
const g = a.grantee;
|
|
644
|
-
return g.
|
|
655
|
+
return g.kind === "entity" ? ["", "entity", g.entityId, "", "", ""] : ["", "group", g.groupId, "", "", g.role];
|
|
645
656
|
}
|
|
646
657
|
if (a.kind !== "relationship")
|
|
647
658
|
return ["", "", "", "", "", ""];
|
|
648
659
|
const t = a.target;
|
|
649
|
-
if (t.
|
|
660
|
+
if (t.kind === "entity")
|
|
650
661
|
return ["", "entity", t.entityId, "", "", ""];
|
|
651
|
-
if (t.
|
|
662
|
+
if (t.kind === "external")
|
|
652
663
|
return ["", "external", t.id, t.ns, "", ""];
|
|
653
664
|
return ["", "record", t.recordId, "", t.stackUrl ?? "", ""];
|
|
654
665
|
};
|
|
655
666
|
var rowToGrantee = (row) => row.related_scope === "group" ? {
|
|
656
|
-
|
|
667
|
+
kind: "group",
|
|
657
668
|
groupId: row.related_id,
|
|
658
669
|
role: row.related_role
|
|
659
|
-
} : {
|
|
670
|
+
} : { kind: "entity", entityId: row.related_id };
|
|
660
671
|
var rowToTarget = (row) => {
|
|
661
672
|
const id = row.related_id;
|
|
662
673
|
if (row.related_scope === "entity")
|
|
663
|
-
return {
|
|
674
|
+
return { kind: "entity", entityId: id };
|
|
664
675
|
if (row.related_scope === "external") {
|
|
665
|
-
return {
|
|
676
|
+
return { kind: "external", ns: row.related_ns, id };
|
|
666
677
|
}
|
|
667
678
|
const stackUrl = row.related_stack;
|
|
668
|
-
return {
|
|
679
|
+
return { kind: "record", recordId: id, ...stackUrl && { stackUrl } };
|
|
669
680
|
};
|
|
670
681
|
var rowToType = (row) => {
|
|
671
682
|
const t = {
|
|
@@ -688,12 +699,12 @@ var rowToVersion = (row) => {
|
|
|
688
699
|
content: JSON.parse(row.content),
|
|
689
700
|
updatedAt: fromMs(row.updated_at)
|
|
690
701
|
};
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (
|
|
696
|
-
v.
|
|
702
|
+
const createdBy = actorFromColumns(row.entity_id, row.principal_id);
|
|
703
|
+
if (createdBy)
|
|
704
|
+
v.createdBy = createdBy;
|
|
705
|
+
const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
|
|
706
|
+
if (updatedBy)
|
|
707
|
+
v.updatedBy = updatedBy;
|
|
697
708
|
return v;
|
|
698
709
|
};
|
|
699
710
|
var rowToJournalEntry = (row) => {
|
|
@@ -736,13 +747,13 @@ var tryReadStackConfig = (exec) => {
|
|
|
736
747
|
const content = JSON.parse(row.content);
|
|
737
748
|
return { entityId: content.entityId, timezone: content.timezone };
|
|
738
749
|
};
|
|
739
|
-
var
|
|
750
|
+
var asStackBadRequestError = (query, run) => {
|
|
740
751
|
try {
|
|
741
752
|
return run();
|
|
742
753
|
} catch (err) {
|
|
743
754
|
if (!query.filter?.search)
|
|
744
755
|
throw err;
|
|
745
|
-
throw new
|
|
756
|
+
throw new StackBadRequestError(`Search text could not be parsed: ${err instanceof Error ? err.message : String(err)}`);
|
|
746
757
|
}
|
|
747
758
|
};
|
|
748
759
|
var versionConflict = (id, expected, actual) => new StackVersionConflictError(`Record "${id}" is at version ${actual}, expected ${expected}`, id, expected, actual);
|
|
@@ -751,6 +762,7 @@ var VERSION_COLUMNS = [
|
|
|
751
762
|
"content",
|
|
752
763
|
"updated_at",
|
|
753
764
|
"entity_id",
|
|
765
|
+
"principal_id",
|
|
754
766
|
"updated_by",
|
|
755
767
|
"updated_via"
|
|
756
768
|
];
|
|
@@ -758,9 +770,10 @@ var versionRowValues = (version) => [
|
|
|
758
770
|
version.typeId,
|
|
759
771
|
JSON.stringify(version.content),
|
|
760
772
|
toMs(version.updatedAt),
|
|
761
|
-
version.
|
|
762
|
-
version.
|
|
763
|
-
version.
|
|
773
|
+
version.createdBy?.subjectId ?? null,
|
|
774
|
+
version.createdBy?.principalId ?? null,
|
|
775
|
+
version.updatedBy?.subjectId ?? null,
|
|
776
|
+
version.updatedBy?.principalId ?? null
|
|
764
777
|
];
|
|
765
778
|
var indexedFieldKinds = (schema) => {
|
|
766
779
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -789,7 +802,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
789
802
|
/**
|
|
790
803
|
* Precondition check for a mutation that has already read the record —
|
|
791
804
|
* either because it can't fold the check into its own statement
|
|
792
|
-
* (
|
|
805
|
+
* (purgeRecord: children reference the row, so the DELETE can't
|
|
793
806
|
* carry it), or because it has to settle the precondition *before*
|
|
794
807
|
* fts5Strategy.remove() runs, which needs the records row still holding
|
|
795
808
|
* the old content. In the latter case versionedUpdate() re-checks it
|
|
@@ -818,7 +831,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
818
831
|
/**
|
|
819
832
|
* The one shape every mutating UPDATE in this class has: whatever columns
|
|
820
833
|
* the caller is changing, then the version bump and the actor/timestamp
|
|
821
|
-
* stamp that every mutation owes, gated on the opt-in `
|
|
834
|
+
* stamp that every mutation owes, gated on the opt-in `ifVersion`
|
|
822
835
|
* precondition and failing loudly when nothing matched.
|
|
823
836
|
*
|
|
824
837
|
* The guard rides in the WHERE clause even where the caller has already
|
|
@@ -827,17 +840,17 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
827
840
|
* rather than silently overwritten.
|
|
828
841
|
*/
|
|
829
842
|
versionedUpdate(id, sets, values, opts, now = toMs(/* @__PURE__ */ new Date())) {
|
|
830
|
-
const guarded = opts.
|
|
843
|
+
const guarded = opts.ifVersion !== void 0;
|
|
831
844
|
const changed = this.exec.run(`UPDATE records SET ${[...sets, "version = version + 1", "updated_at = ?", "updated_by = ?", "updated_via = ?"].join(", ")} WHERE id = ?${guarded ? " AND version = ?" : ""}`, [
|
|
832
845
|
...values,
|
|
833
846
|
now,
|
|
834
|
-
opts.
|
|
835
|
-
opts.
|
|
847
|
+
opts.actor?.subjectId ?? null,
|
|
848
|
+
opts.actor?.principalId ?? null,
|
|
836
849
|
id,
|
|
837
|
-
...guarded ? [opts.
|
|
850
|
+
...guarded ? [opts.ifVersion] : []
|
|
838
851
|
]);
|
|
839
852
|
if (changed === 0)
|
|
840
|
-
this.throwVersionConflict(id, opts.
|
|
853
|
+
this.throwVersionConflict(id, opts.ifVersion);
|
|
841
854
|
}
|
|
842
855
|
/**
|
|
843
856
|
* Read a record back after its own mutation committed, for the caller to
|
|
@@ -886,11 +899,11 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
886
899
|
JSON.stringify(record.content),
|
|
887
900
|
record.version,
|
|
888
901
|
record.parentId ?? null,
|
|
889
|
-
record.
|
|
902
|
+
record.createdBy?.subjectId ?? null,
|
|
890
903
|
record.appId ?? null,
|
|
891
|
-
record.principalId ?? null,
|
|
892
|
-
record.updatedBy ?? null,
|
|
893
|
-
record.
|
|
904
|
+
record.createdBy?.principalId ?? null,
|
|
905
|
+
record.updatedBy?.subjectId ?? null,
|
|
906
|
+
record.updatedBy?.principalId ?? null,
|
|
894
907
|
record.deletedAt ? toMs(record.deletedAt) : null,
|
|
895
908
|
record.unlistedAt ? toMs(record.unlistedAt) : null
|
|
896
909
|
]);
|
|
@@ -943,13 +956,13 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
943
956
|
* `associations`, `parentId` and/or `unlisted` — Stack computes this from
|
|
944
957
|
* which ops the set actually moves. Such a write still lands its columns,
|
|
945
958
|
* but leaves `version`, `updatedAt` and the actor stamps exactly as they
|
|
946
|
-
* stood and takes no snapshot. `
|
|
959
|
+
* stood and takes no snapshot. `ifVersion`, if given, is re-checked
|
|
947
960
|
* synchronously inside the transaction — the CAS guard the bumping path
|
|
948
961
|
* gets from its UPDATE's WHERE clause, restated as a read because the
|
|
949
962
|
* UPDATE here carries no version predicate. See
|
|
950
963
|
* docs/spec/versioning.md § Version history.
|
|
951
964
|
*
|
|
952
|
-
* The
|
|
965
|
+
* The ifVersion check for a bumping write is standalone rather than
|
|
953
966
|
* folded into the UPDATE alone, because a content change needs
|
|
954
967
|
* fts.remove() to run *before* the records-table content changes. The
|
|
955
968
|
* guard is still in the UPDATE's WHERE, so a writer that slipped in
|
|
@@ -959,7 +972,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
959
972
|
const existing = await this.getRecord(id);
|
|
960
973
|
if (!existing)
|
|
961
974
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
962
|
-
this.checkExpectedVersion(existing, opts.
|
|
975
|
+
this.checkExpectedVersion(existing, opts.ifVersion);
|
|
963
976
|
const now = toMs(/* @__PURE__ */ new Date());
|
|
964
977
|
const sets = [];
|
|
965
978
|
const values = [];
|
|
@@ -976,7 +989,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
976
989
|
const current = this.readRecord(id);
|
|
977
990
|
if (!current)
|
|
978
991
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
979
|
-
this.checkExpectedVersion(current, opts.
|
|
992
|
+
this.checkExpectedVersion(current, opts.ifVersion);
|
|
980
993
|
if (sets.length > 0) {
|
|
981
994
|
this.exec.run(`UPDATE records SET ${sets.join(", ")} WHERE id = ?`, [...values, id]);
|
|
982
995
|
}
|
|
@@ -1006,8 +1019,8 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1006
1019
|
return this.reread(id, "mutateRecord");
|
|
1007
1020
|
}
|
|
1008
1021
|
async deleteRecord(id, opts = {}) {
|
|
1009
|
-
if (opts.
|
|
1010
|
-
return this.exec.transaction(() => this.
|
|
1022
|
+
if (opts.purge) {
|
|
1023
|
+
return this.exec.transaction(() => this.purgeRecord(id, opts.ifVersion));
|
|
1011
1024
|
}
|
|
1012
1025
|
const now = toMs(/* @__PURE__ */ new Date());
|
|
1013
1026
|
this.exec.transaction(() => {
|
|
@@ -1023,18 +1036,18 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1023
1036
|
* index rows, and row, returning the record as it stood — the last copy that will ever
|
|
1024
1037
|
* exist, read here so that nothing can overtake it between the read and
|
|
1025
1038
|
* the deletes. Null when there was no such record. No write() call —
|
|
1026
|
-
* callers batch it.
|
|
1039
|
+
* callers batch it. ifVersion is checked first so a lost CAS race
|
|
1027
1040
|
* leaves nothing touched (children reference the row, so the check can't
|
|
1028
1041
|
* fold into the final DELETE).
|
|
1029
1042
|
*/
|
|
1030
|
-
|
|
1043
|
+
purgeRecord(id, ifVersion) {
|
|
1031
1044
|
const purged = this.readRecord(id);
|
|
1032
1045
|
if (!purged) {
|
|
1033
|
-
if (
|
|
1046
|
+
if (ifVersion !== void 0)
|
|
1034
1047
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1035
1048
|
return null;
|
|
1036
1049
|
}
|
|
1037
|
-
this.checkExpectedVersion(purged,
|
|
1050
|
+
this.checkExpectedVersion(purged, ifVersion);
|
|
1038
1051
|
fts5Strategy.remove(this.exec, id);
|
|
1039
1052
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
1040
1053
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
@@ -1056,7 +1069,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1056
1069
|
const existing = await this.getRecord(id);
|
|
1057
1070
|
if (!existing)
|
|
1058
1071
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1059
|
-
this.checkExpectedVersion(existing, opts.
|
|
1072
|
+
this.checkExpectedVersion(existing, opts.ifVersion);
|
|
1060
1073
|
const target = await this.getVersion(id, version);
|
|
1061
1074
|
if (!target)
|
|
1062
1075
|
throw new StackNotFoundError(`Version not found: "${id}"@${version}`);
|
|
@@ -1072,7 +1085,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1072
1085
|
const existing = await this.getRecord(id);
|
|
1073
1086
|
if (!existing)
|
|
1074
1087
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
1075
|
-
this.checkExpectedVersion(existing, opts.
|
|
1088
|
+
this.checkExpectedVersion(existing, opts.ifVersion);
|
|
1076
1089
|
this.exec.transaction(() => {
|
|
1077
1090
|
if (opts.snapshot)
|
|
1078
1091
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
@@ -1107,7 +1120,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1107
1120
|
if (budget <= 0)
|
|
1108
1121
|
break;
|
|
1109
1122
|
const statement2 = atBudget(page2, budget);
|
|
1110
|
-
rows.push(...
|
|
1123
|
+
rows.push(...asStackBadRequestError(query, () => this.exec.all(statement2.sql, statement2.params)));
|
|
1111
1124
|
}
|
|
1112
1125
|
const hasMore = rows.length > limit;
|
|
1113
1126
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
@@ -1120,7 +1133,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1120
1133
|
return { records, cursor };
|
|
1121
1134
|
}
|
|
1122
1135
|
/**
|
|
1123
|
-
* Atomically verify fileId is unreferenced, then
|
|
1136
|
+
* Atomically verify fileId is unreferenced, then purge its
|
|
1124
1137
|
* metadata records across every typeId in `metadataTypeIds` (see
|
|
1125
1138
|
* deleteUnreferencedAttachmentRecords on the adapter contract). A real
|
|
1126
1139
|
* SQL transaction of synchronous calls — no `await` between the check
|
|
@@ -1142,7 +1155,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1142
1155
|
WHERE type_id IN (${placeholders}) AND json_extract(content, '$.fileId') = ?`, [...metadataTypeIds, fileId]);
|
|
1143
1156
|
const deleted = [];
|
|
1144
1157
|
for (const row of metaRows) {
|
|
1145
|
-
const purged = this.
|
|
1158
|
+
const purged = this.purgeRecord(row.id);
|
|
1146
1159
|
if (purged)
|
|
1147
1160
|
deleted.push(purged);
|
|
1148
1161
|
}
|
|
@@ -1264,9 +1277,9 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1264
1277
|
}
|
|
1265
1278
|
const conditions = ["record_id = ?"];
|
|
1266
1279
|
const values = [id];
|
|
1267
|
-
if (query.
|
|
1280
|
+
if (query.afterSeq !== void 0) {
|
|
1268
1281
|
conditions.push("seq > ?");
|
|
1269
|
-
values.push(query.
|
|
1282
|
+
values.push(query.afterSeq);
|
|
1270
1283
|
}
|
|
1271
1284
|
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);
|
|
1272
1285
|
return rows.map(rowToJournalEntry);
|
|
@@ -1576,11 +1589,12 @@ var DoSQLiteRecordAdapter = class _DoSQLiteRecordAdapter extends SharedSqlRecord
|
|
|
1576
1589
|
super(exec, config);
|
|
1577
1590
|
}
|
|
1578
1591
|
/**
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1583
|
-
*
|
|
1592
|
+
* Open the adapter for a DO instance, initializing its storage on first
|
|
1593
|
+
* use — LocalAdapter.openOrInitialize()'s counterpart. There is no
|
|
1594
|
+
* separate initialize()/open() the way file-based adapters need one: a
|
|
1595
|
+
* DO id either already has a config record (reattach,
|
|
1596
|
+
* opts.ownerEntityId/timezone ignored in favor of what's stored) or it
|
|
1597
|
+
* doesn't (first call — opts.ownerEntityId/timezone become the config).
|
|
1584
1598
|
* Schema DDL is `CREATE TABLE IF NOT EXISTS`, so running it every call
|
|
1585
1599
|
* is idempotent and cheap.
|
|
1586
1600
|
*
|
|
@@ -1588,10 +1602,10 @@ var DoSQLiteRecordAdapter = class _DoSQLiteRecordAdapter extends SharedSqlRecord
|
|
|
1588
1602
|
* outright ("not authorized") — verified against the real runtime, not
|
|
1589
1603
|
* assumed — hence `wal: false`, unlike record-adapter-sqlite.
|
|
1590
1604
|
*/
|
|
1591
|
-
static async
|
|
1605
|
+
static async openOrInitialize(storage, opts) {
|
|
1592
1606
|
const exec = new DurableObjectSqliteExecutor(storage);
|
|
1593
1607
|
applyRecordSchema(exec);
|
|
1594
|
-
const config = tryReadStackConfig(exec) ?? insertConfigRecord(exec, opts.
|
|
1608
|
+
const config = tryReadStackConfig(exec) ?? insertConfigRecord(exec, opts.ownerEntityId, opts.timezone);
|
|
1595
1609
|
return new _DoSQLiteRecordAdapter(exec, config);
|
|
1596
1610
|
}
|
|
1597
1611
|
// -------------------------------------------------------
|