@haverstack/record-adapter-sqlite 0.21.0 → 0.23.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 +124 -55
- package/dist/index.js +345 -353
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, writeFileSync,
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
2
|
+
import { StackNotFoundError, StackConflictError, applyMergePatch, StackVersionConflictError, StackQueryError, 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
|
|
|
@@ -151,7 +151,13 @@ var FTS5_SCHEMA_SQL = `
|
|
|
151
151
|
`;
|
|
152
152
|
var PRAGMA_FOREIGN_KEYS_ON = `PRAGMA foreign_keys = ON;`;
|
|
153
153
|
var PRAGMA_JOURNAL_MODE_WAL = `PRAGMA journal_mode = WAL;`;
|
|
154
|
-
var
|
|
154
|
+
var applyRecordSchema = (exec, opts) => {
|
|
155
|
+
exec.exec(PRAGMA_FOREIGN_KEYS_ON);
|
|
156
|
+
exec.exec(PRAGMA_JOURNAL_MODE_WAL);
|
|
157
|
+
exec.exec(RECORD_SCHEMA_SQL);
|
|
158
|
+
exec.exec(FTS5_SCHEMA_SQL);
|
|
159
|
+
};
|
|
160
|
+
var SORT_FIELDS = NATIVE_SORT_FIELDS;
|
|
155
161
|
var toBase64 = (text) => {
|
|
156
162
|
const bytes = new TextEncoder().encode(text);
|
|
157
163
|
let binary = "";
|
|
@@ -367,17 +373,30 @@ var recordConditions = (query) => {
|
|
|
367
373
|
const conditions = ["r.id != '_config'"];
|
|
368
374
|
const params = [];
|
|
369
375
|
const f = query.filter ?? {};
|
|
376
|
+
const inFilter = (column, value) => {
|
|
377
|
+
if (value === void 0)
|
|
378
|
+
return;
|
|
379
|
+
const values = Array.isArray(value) ? value : [value];
|
|
380
|
+
conditions.push(`r.${column} IN (${values.map(() => "?").join(",")})`);
|
|
381
|
+
params.push(...values);
|
|
382
|
+
};
|
|
383
|
+
const rangeFilter = (column, range) => {
|
|
384
|
+
if (range?.after) {
|
|
385
|
+
conditions.push(`r.${column} > ?`);
|
|
386
|
+
params.push(range.after.getTime());
|
|
387
|
+
}
|
|
388
|
+
if (range?.before) {
|
|
389
|
+
conditions.push(`r.${column} < ?`);
|
|
390
|
+
params.push(range.before.getTime());
|
|
391
|
+
}
|
|
392
|
+
};
|
|
370
393
|
if (!f.includeDeleted) {
|
|
371
394
|
conditions.push("r.deleted_at IS NULL");
|
|
372
395
|
}
|
|
373
396
|
if (!f.includeUnlisted) {
|
|
374
397
|
conditions.push("r.unlisted_at IS NULL");
|
|
375
398
|
}
|
|
376
|
-
|
|
377
|
-
const ids = Array.isArray(f.typeId) ? f.typeId : [f.typeId];
|
|
378
|
-
conditions.push(`r.type_id IN (${ids.map(() => "?").join(",")})`);
|
|
379
|
-
params.push(...ids);
|
|
380
|
-
}
|
|
399
|
+
inFilter("type_id", f.typeId);
|
|
381
400
|
if (f.parentId !== void 0) {
|
|
382
401
|
if (f.parentId === null) {
|
|
383
402
|
conditions.push("r.parent_id IS NULL");
|
|
@@ -386,49 +405,24 @@ var recordConditions = (query) => {
|
|
|
386
405
|
params.push(f.parentId);
|
|
387
406
|
}
|
|
388
407
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const ids = Array.isArray(f.entityId) ? f.entityId : [f.entityId];
|
|
396
|
-
conditions.push(`r.entity_id IN (${ids.map(() => "?").join(",")})`);
|
|
397
|
-
params.push(...ids);
|
|
398
|
-
}
|
|
399
|
-
if (f.principalId !== void 0) {
|
|
400
|
-
const ids = Array.isArray(f.principalId) ? f.principalId : [f.principalId];
|
|
401
|
-
conditions.push(`r.principal_id IN (${ids.map(() => "?").join(",")})`);
|
|
402
|
-
params.push(...ids);
|
|
403
|
-
}
|
|
404
|
-
if (f.createdAt?.after) {
|
|
405
|
-
conditions.push("r.created_at > ?");
|
|
406
|
-
params.push(f.createdAt.after.getTime());
|
|
407
|
-
}
|
|
408
|
-
if (f.createdAt?.before) {
|
|
409
|
-
conditions.push("r.created_at < ?");
|
|
410
|
-
params.push(f.createdAt.before.getTime());
|
|
411
|
-
}
|
|
412
|
-
if (f.updatedAt?.after) {
|
|
413
|
-
conditions.push("r.updated_at > ?");
|
|
414
|
-
params.push(f.updatedAt.after.getTime());
|
|
415
|
-
}
|
|
416
|
-
if (f.updatedAt?.before) {
|
|
417
|
-
conditions.push("r.updated_at < ?");
|
|
418
|
-
params.push(f.updatedAt.before.getTime());
|
|
419
|
-
}
|
|
408
|
+
inFilter("app_id", f.appId);
|
|
409
|
+
inFilter("entity_id", f.entityId);
|
|
410
|
+
inFilter("principal_id", f.principalId);
|
|
411
|
+
rangeFilter("created_at", f.createdAt);
|
|
412
|
+
rangeFilter("updated_at", f.updatedAt);
|
|
413
|
+
const hasAssociation = (kind, clauses) => `r.id IN (SELECT a.record_id FROM associations a WHERE a.kind = '${kind}'` + clauses.map((c) => ` AND ${c}`).join("") + `)`;
|
|
420
414
|
if (f.tags?.length) {
|
|
421
415
|
for (const tag of f.tags) {
|
|
422
|
-
conditions.push(
|
|
416
|
+
conditions.push(hasAssociation("tag", ["a.label = ?"]));
|
|
423
417
|
params.push(tag);
|
|
424
418
|
}
|
|
425
419
|
}
|
|
426
420
|
if (f.hasAttachment) {
|
|
427
|
-
conditions.push(
|
|
421
|
+
conditions.push(hasAssociation("attachment", ["a.label = ?"]));
|
|
428
422
|
params.push(f.hasAttachment);
|
|
429
423
|
}
|
|
430
424
|
if (f.attachmentFileId) {
|
|
431
|
-
conditions.push(`(
|
|
425
|
+
conditions.push(`(${hasAssociation("attachment", ["a.file_id = ?"])}
|
|
432
426
|
OR r.id IN (SELECT ci.record_id FROM content_index ci WHERE ci.file_id = ?))`);
|
|
433
427
|
params.push(f.attachmentFileId, f.attachmentFileId);
|
|
434
428
|
}
|
|
@@ -457,7 +451,7 @@ var recordConditions = (query) => {
|
|
|
457
451
|
}
|
|
458
452
|
}
|
|
459
453
|
}
|
|
460
|
-
conditions.push(
|
|
454
|
+
conditions.push(hasAssociation("relationship", clauses));
|
|
461
455
|
}
|
|
462
456
|
if (f.content) {
|
|
463
457
|
let aliasSeq = 0;
|
|
@@ -656,64 +650,32 @@ var rowToVersion = (row) => {
|
|
|
656
650
|
v.permissions = JSON.parse(row.permissions);
|
|
657
651
|
return v;
|
|
658
652
|
};
|
|
659
|
-
var lockPathFor = (dbPath) => `${dbPath}.lock`;
|
|
660
|
-
var isProcessAlive = (pid) => {
|
|
661
|
-
try {
|
|
662
|
-
process.kill(pid, 0);
|
|
663
|
-
return true;
|
|
664
|
-
} catch (err) {
|
|
665
|
-
return err.code !== "ESRCH";
|
|
666
|
-
}
|
|
667
|
-
};
|
|
668
|
-
var acquireLock = (dbPath, force) => {
|
|
669
|
-
const lockPath = lockPathFor(dbPath);
|
|
670
|
-
const info = existsSync(lockPath) ? readLock(lockPath) : null;
|
|
671
|
-
if (info) {
|
|
672
|
-
const ownedBySelf = info.pid === process.pid;
|
|
673
|
-
if (!ownedBySelf && !force && isProcessAlive(info.pid)) {
|
|
674
|
-
throw new Error(`Stack database at "${dbPath}" is in use by another process (pid ${info.pid}). Connect via its server instead, or pass { force: true } to override. If no such process is running, remove "${lockPath}".`);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
writeFileSync(lockPath, JSON.stringify({ pid: process.pid }));
|
|
678
|
-
};
|
|
679
|
-
var readLock = (lockPath) => {
|
|
680
|
-
try {
|
|
681
|
-
const info = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
682
|
-
return typeof info?.pid === "number" ? info : null;
|
|
683
|
-
} catch {
|
|
684
|
-
return null;
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
var releaseLock = (dbPath) => {
|
|
688
|
-
const lockPath = lockPathFor(dbPath);
|
|
689
|
-
if (!existsSync(lockPath))
|
|
690
|
-
return;
|
|
691
|
-
try {
|
|
692
|
-
const info = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
693
|
-
if (info.pid === process.pid)
|
|
694
|
-
unlinkSync(lockPath);
|
|
695
|
-
} catch {
|
|
696
|
-
unlinkSync(lockPath);
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
653
|
|
|
700
654
|
// ../sqlite-shared/dist/executor.js
|
|
701
655
|
var isForeignKeyViolation = (err) => err instanceof Error && err.message.includes("FOREIGN KEY constraint failed");
|
|
702
656
|
var isUniqueConstraintViolation = (err) => err instanceof Error && err.message.includes("UNIQUE constraint failed");
|
|
703
657
|
|
|
704
658
|
// ../sqlite-shared/dist/config.js
|
|
659
|
+
var CONFIG_ROW_SQL = `SELECT content FROM records WHERE id = '_config'`;
|
|
705
660
|
var insertConfigRecord = (exec, entityId, timezone) => {
|
|
706
661
|
const now = Date.now();
|
|
707
662
|
exec.run(`INSERT INTO records (id, type_id, created_at, updated_at, content, version)
|
|
708
663
|
VALUES ('_config', '_config@1', ?, ?, ?, 1)`, [now, now, JSON.stringify({ entityId, timezone })]);
|
|
664
|
+
return { entityId, timezone };
|
|
709
665
|
};
|
|
710
|
-
var
|
|
711
|
-
const row = exec.get(
|
|
666
|
+
var tryReadStackConfig = (exec) => {
|
|
667
|
+
const row = exec.get(CONFIG_ROW_SQL);
|
|
712
668
|
if (!row)
|
|
713
|
-
|
|
669
|
+
return null;
|
|
714
670
|
const content = JSON.parse(row.content);
|
|
715
671
|
return { entityId: content.entityId, timezone: content.timezone };
|
|
716
672
|
};
|
|
673
|
+
var readStackConfig = (exec) => {
|
|
674
|
+
const config = tryReadStackConfig(exec);
|
|
675
|
+
if (!config)
|
|
676
|
+
throw new Error("Stack database is missing its config record.");
|
|
677
|
+
return config;
|
|
678
|
+
};
|
|
717
679
|
var asStackQueryError = (query, run) => {
|
|
718
680
|
try {
|
|
719
681
|
return run();
|
|
@@ -723,6 +685,29 @@ var asStackQueryError = (query, run) => {
|
|
|
723
685
|
throw new StackQueryError(`Search text could not be parsed: ${err instanceof Error ? err.message : String(err)}`);
|
|
724
686
|
}
|
|
725
687
|
};
|
|
688
|
+
var versionConflict = (id, expected, actual) => new StackVersionConflictError(`Record "${id}" is at version ${actual}, expected ${expected}`, id, expected, actual);
|
|
689
|
+
var VERSION_COLUMNS = [
|
|
690
|
+
"type_id",
|
|
691
|
+
"content",
|
|
692
|
+
"updated_at",
|
|
693
|
+
"entity_id",
|
|
694
|
+
"updated_by",
|
|
695
|
+
"updated_via",
|
|
696
|
+
"parent_id",
|
|
697
|
+
"associations",
|
|
698
|
+
"permissions"
|
|
699
|
+
];
|
|
700
|
+
var versionRowValues = (version) => [
|
|
701
|
+
version.typeId,
|
|
702
|
+
JSON.stringify(version.content),
|
|
703
|
+
toMs(version.updatedAt),
|
|
704
|
+
version.entityId ?? null,
|
|
705
|
+
version.updatedBy ?? null,
|
|
706
|
+
version.updatedVia ?? null,
|
|
707
|
+
version.parentId ?? null,
|
|
708
|
+
version.associations ? JSON.stringify(version.associations) : null,
|
|
709
|
+
version.permissions ? JSON.stringify(version.permissions) : null
|
|
710
|
+
];
|
|
726
711
|
var indexedFieldKinds = (schema) => {
|
|
727
712
|
const kinds = /* @__PURE__ */ new Map();
|
|
728
713
|
for (const [field, def] of Object.entries(schema)) {
|
|
@@ -748,26 +733,22 @@ var SharedSqlRecordLogic = class {
|
|
|
748
733
|
return this.deps.exec;
|
|
749
734
|
}
|
|
750
735
|
/**
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
* Precondition check for mutations that can't fold expectedVersion into
|
|
760
|
-
* their primary UPDATE's WHERE clause: mutateRecord/restoreVersion need
|
|
761
|
-
* fts.remove() to run *before* the records-table content changes, so
|
|
762
|
-
* the check happens first, standalone.
|
|
736
|
+
* Precondition check for a mutation that has already read the record —
|
|
737
|
+
* either because it can't fold the check into its own statement
|
|
738
|
+
* (hardDeleteRecord: children reference the row, so the DELETE can't
|
|
739
|
+
* carry it), or because it has to settle the precondition *before*
|
|
740
|
+
* fts5Strategy.remove() runs, which needs the records row still holding
|
|
741
|
+
* the old content. In the latter case versionedUpdate() re-checks it
|
|
742
|
+
* inside the statement, and that is the enforcement; this is what lets
|
|
743
|
+
* the work before it be skipped.
|
|
763
744
|
*/
|
|
764
745
|
checkExpectedVersion(record, expectedVersion) {
|
|
765
746
|
if (expectedVersion === void 0 || record.version === expectedVersion)
|
|
766
747
|
return;
|
|
767
|
-
throw
|
|
748
|
+
throw versionConflict(record.id, expectedVersion, record.version);
|
|
768
749
|
}
|
|
769
750
|
/**
|
|
770
|
-
* Called after a
|
|
751
|
+
* Called after a version-gated statement affected zero rows.
|
|
771
752
|
* Distinguishes "record doesn't exist" (StackNotFoundError) from "it
|
|
772
753
|
* exists but isn't at expectedVersion" (StackVersionConflictError, with
|
|
773
754
|
* the actual current version for the caller to act on).
|
|
@@ -778,7 +759,48 @@ var SharedSqlRecordLogic = class {
|
|
|
778
759
|
]);
|
|
779
760
|
if (!row)
|
|
780
761
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
781
|
-
throw
|
|
762
|
+
throw versionConflict(id, expectedVersion, row.version);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* The one shape every mutating UPDATE in this class has: whatever columns
|
|
766
|
+
* the caller is changing, then the version bump and the actor/timestamp
|
|
767
|
+
* stamp that every mutation owes, gated on the opt-in `expectedVersion`
|
|
768
|
+
* precondition and failing loudly when nothing matched.
|
|
769
|
+
*
|
|
770
|
+
* The guard rides in the WHERE clause even where the caller has already
|
|
771
|
+
* checked the version against a record it read: that read is outside this
|
|
772
|
+
* statement, so a writer slipping in between the two is caught here
|
|
773
|
+
* rather than silently overwritten.
|
|
774
|
+
*/
|
|
775
|
+
versionedUpdate(id, sets, values, opts, now = toMs(/* @__PURE__ */ new Date())) {
|
|
776
|
+
const guarded = opts.expectedVersion !== void 0;
|
|
777
|
+
const changed = this.exec.run(`UPDATE records SET ${[...sets, "version = version + 1", "updated_at = ?", "updated_by = ?", "updated_via = ?"].join(", ")} WHERE id = ?${guarded ? " AND version = ?" : ""}`, [
|
|
778
|
+
...values,
|
|
779
|
+
now,
|
|
780
|
+
opts.updatedBy ?? null,
|
|
781
|
+
opts.updatedVia ?? null,
|
|
782
|
+
id,
|
|
783
|
+
...guarded ? [opts.expectedVersion] : []
|
|
784
|
+
]);
|
|
785
|
+
if (changed === 0)
|
|
786
|
+
this.throwVersionConflict(id, opts.expectedVersion);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Read a record back after its own mutation committed, for the caller to
|
|
790
|
+
* return. Its absence is not a "not found" for the caller to handle — the
|
|
791
|
+
* mutation just succeeded against it — so it fails as the invariant
|
|
792
|
+
* breach it would be.
|
|
793
|
+
*
|
|
794
|
+
* Synchronous, via readRecord() rather than getRecord(), so the read
|
|
795
|
+
* cannot be separated from the commit by anything: an async caller
|
|
796
|
+
* returning this still hands back a record read in the same tick its
|
|
797
|
+
* transaction committed. See readRecord() for why that matters.
|
|
798
|
+
*/
|
|
799
|
+
reread(id, verb) {
|
|
800
|
+
const updated = this.readRecord(id);
|
|
801
|
+
if (!updated)
|
|
802
|
+
throw new Error(`Record not found after ${verb}: "${id}"`);
|
|
803
|
+
return updated;
|
|
782
804
|
}
|
|
783
805
|
// -------------------------------------------------------
|
|
784
806
|
// Records
|
|
@@ -835,11 +857,11 @@ var SharedSqlRecordLogic = class {
|
|
|
835
857
|
* reason (see SqlExecutor.transaction).
|
|
836
858
|
*
|
|
837
859
|
* The same synchrony is what lets the post-commit reads below report the
|
|
838
|
-
* version their own mutation produced:
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
* that window, and a concurrent mutation could be
|
|
842
|
-
* one's verb.
|
|
860
|
+
* version their own mutation produced: reread() calls this directly, so
|
|
861
|
+
* nothing interleaves between the commit and the read even though the
|
|
862
|
+
* method returning it is async. A backend that made these reads genuinely
|
|
863
|
+
* asynchronous would open that window, and a concurrent mutation could be
|
|
864
|
+
* reported under this one's verb.
|
|
843
865
|
*/
|
|
844
866
|
readRecord(id) {
|
|
845
867
|
const row = this.exec.get("SELECT * FROM records WHERE id = ?", [id]);
|
|
@@ -890,49 +912,28 @@ var SharedSqlRecordLogic = class {
|
|
|
890
912
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
891
913
|
if (merged !== void 0)
|
|
892
914
|
fts5Strategy.remove(this.exec, id);
|
|
893
|
-
|
|
894
|
-
const changed = this.exec.run(`UPDATE records SET ${sets.concat(["version = version + 1", "updated_at = ?", "updated_by = ?", "updated_via = ?"]).join(", ")} WHERE id = ?${clause}`, [...values, now, opts.updatedBy ?? null, opts.updatedVia ?? null, id, ...verParams]);
|
|
895
|
-
if (changed === 0)
|
|
896
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
915
|
+
this.versionedUpdate(id, sets, values, opts, now);
|
|
897
916
|
if (changes.associations !== void 0) {
|
|
898
|
-
this.
|
|
899
|
-
if (changes.associations.length)
|
|
900
|
-
this.insertAssociations(id, changes.associations);
|
|
917
|
+
this.replaceAssociations(id, changes.associations);
|
|
901
918
|
}
|
|
902
919
|
if (merged !== void 0) {
|
|
903
920
|
fts5Strategy.insert(this.exec, id, JSON.stringify(merged));
|
|
904
921
|
this.syncContentIndex(id, existing.typeId, merged);
|
|
905
922
|
}
|
|
906
923
|
});
|
|
907
|
-
|
|
908
|
-
if (!updated)
|
|
909
|
-
throw new Error(`Record not found after mutateRecord: "${id}"`);
|
|
910
|
-
return updated;
|
|
924
|
+
return this.reread(id, "mutateRecord");
|
|
911
925
|
}
|
|
912
926
|
async deleteRecord(id, opts = {}) {
|
|
913
927
|
if (opts.hard) {
|
|
914
928
|
return this.exec.transaction(() => this.hardDeleteRecord(id, opts.expectedVersion));
|
|
915
|
-
} else {
|
|
916
|
-
this.exec.transaction(() => {
|
|
917
|
-
if (opts.snapshot)
|
|
918
|
-
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
919
|
-
const { clause, params: verParams } = this.versionGuard(opts.expectedVersion);
|
|
920
|
-
const changed = this.exec.run(`UPDATE records SET deleted_at = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?${clause}`, [
|
|
921
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
922
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
923
|
-
opts.updatedBy ?? null,
|
|
924
|
-
opts.updatedVia ?? null,
|
|
925
|
-
id,
|
|
926
|
-
...verParams
|
|
927
|
-
]);
|
|
928
|
-
if (changed === 0)
|
|
929
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
930
|
-
});
|
|
931
929
|
}
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
930
|
+
const now = toMs(/* @__PURE__ */ new Date());
|
|
931
|
+
this.exec.transaction(() => {
|
|
932
|
+
if (opts.snapshot)
|
|
933
|
+
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
934
|
+
this.versionedUpdate(id, ["deleted_at = ?"], [now], opts, now);
|
|
935
|
+
});
|
|
936
|
+
return this.reread(id, "deleteRecord");
|
|
936
937
|
}
|
|
937
938
|
/**
|
|
938
939
|
* Deletes a record's FTS entry, associations, versions, file-refs, sort
|
|
@@ -945,15 +946,12 @@ var SharedSqlRecordLogic = class {
|
|
|
945
946
|
*/
|
|
946
947
|
hardDeleteRecord(id, expectedVersion) {
|
|
947
948
|
const purged = this.readRecord(id);
|
|
948
|
-
if (
|
|
949
|
-
if (
|
|
949
|
+
if (!purged) {
|
|
950
|
+
if (expectedVersion !== void 0)
|
|
950
951
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
951
|
-
if (purged.version !== expectedVersion) {
|
|
952
|
-
throw new StackVersionConflictError(`Record "${id}" is at version ${purged.version}, expected ${expectedVersion}`, id, expectedVersion, purged.version);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
if (!purged)
|
|
956
952
|
return null;
|
|
953
|
+
}
|
|
954
|
+
this.checkExpectedVersion(purged, expectedVersion);
|
|
957
955
|
fts5Strategy.remove(this.exec, id);
|
|
958
956
|
this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
|
|
959
957
|
this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
|
|
@@ -965,15 +963,9 @@ var SharedSqlRecordLogic = class {
|
|
|
965
963
|
this.exec.transaction(() => {
|
|
966
964
|
if (opts.snapshot)
|
|
967
965
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
968
|
-
|
|
969
|
-
const changed = this.exec.run(`UPDATE records SET deleted_at = NULL, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?${clause}`, [toMs(/* @__PURE__ */ new Date()), opts.updatedBy ?? null, opts.updatedVia ?? null, id, ...verParams]);
|
|
970
|
-
if (changed === 0)
|
|
971
|
-
this.throwVersionConflict(id, opts.expectedVersion);
|
|
966
|
+
this.versionedUpdate(id, ["deleted_at = NULL"], [], opts);
|
|
972
967
|
});
|
|
973
|
-
|
|
974
|
-
if (!updated)
|
|
975
|
-
throw new Error(`Record not found after undelete: "${id}"`);
|
|
976
|
-
return updated;
|
|
968
|
+
return this.reread(id, "undelete");
|
|
977
969
|
}
|
|
978
970
|
async restoreVersion(id, version, opts = {}) {
|
|
979
971
|
const existing = await this.getRecord(id);
|
|
@@ -986,29 +978,12 @@ var SharedSqlRecordLogic = class {
|
|
|
986
978
|
this.exec.transaction(() => {
|
|
987
979
|
if (opts.snapshot)
|
|
988
980
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
989
|
-
|
|
990
|
-
this.exec.run(`UPDATE records SET type_id = ?, content = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ?, parent_id = ? WHERE id = ?`, [
|
|
991
|
-
target.typeId,
|
|
992
|
-
JSON.stringify(target.content),
|
|
993
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
994
|
-
opts.updatedBy ?? null,
|
|
995
|
-
opts.updatedVia ?? null,
|
|
996
|
-
target.parentId ?? null,
|
|
997
|
-
id
|
|
998
|
-
]);
|
|
981
|
+
this.rewriteContent(id, target.typeId, target.content, opts, ["parent_id = ?"], [target.parentId ?? null]);
|
|
999
982
|
const applied = opts.restoreAssociations === false ? void 0 : target.associations;
|
|
1000
|
-
if (applied !== void 0)
|
|
1001
|
-
this.
|
|
1002
|
-
if (applied.length)
|
|
1003
|
-
this.insertAssociations(id, applied);
|
|
1004
|
-
}
|
|
1005
|
-
fts5Strategy.insert(this.exec, id, JSON.stringify(target.content));
|
|
1006
|
-
this.syncContentIndex(id, target.typeId, target.content);
|
|
983
|
+
if (applied !== void 0)
|
|
984
|
+
this.replaceAssociations(id, applied);
|
|
1007
985
|
});
|
|
1008
|
-
|
|
1009
|
-
if (!updated)
|
|
1010
|
-
throw new Error(`Record not found after restoreVersion: "${id}"`);
|
|
1011
|
-
return updated;
|
|
986
|
+
return this.reread(id, "restoreVersion");
|
|
1012
987
|
}
|
|
1013
988
|
async commitMigration(id, toTypeId, content, opts = {}) {
|
|
1014
989
|
const existing = await this.getRecord(id);
|
|
@@ -1018,22 +993,27 @@ var SharedSqlRecordLogic = class {
|
|
|
1018
993
|
this.exec.transaction(() => {
|
|
1019
994
|
if (opts.snapshot)
|
|
1020
995
|
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
1021
|
-
|
|
1022
|
-
this.exec.run(`UPDATE records SET type_id = ?, content = ?, version = version + 1, updated_at = ?, updated_by = ?, updated_via = ? WHERE id = ?`, [
|
|
1023
|
-
toTypeId,
|
|
1024
|
-
JSON.stringify(content),
|
|
1025
|
-
toMs(/* @__PURE__ */ new Date()),
|
|
1026
|
-
opts.updatedBy ?? null,
|
|
1027
|
-
opts.updatedVia ?? null,
|
|
1028
|
-
id
|
|
1029
|
-
]);
|
|
1030
|
-
fts5Strategy.insert(this.exec, id, JSON.stringify(content));
|
|
1031
|
-
this.syncContentIndex(id, toTypeId, content);
|
|
996
|
+
this.rewriteContent(id, toTypeId, content, opts);
|
|
1032
997
|
});
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
998
|
+
return this.reread(id, "commitMigration");
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Replace a record's type and content wholesale — what restoreVersion and
|
|
1002
|
+
* commitMigration each do, differing only in where the content came from
|
|
1003
|
+
* and whether the container moves with it.
|
|
1004
|
+
*
|
|
1005
|
+
* The order is the constraint: fts5Strategy.remove() has to read the old
|
|
1006
|
+
* content off the records row, so it runs before the UPDATE, and the
|
|
1007
|
+
* re-index runs after. Every caller must already sit inside
|
|
1008
|
+
* exec.transaction() — a failure between the two halves would otherwise
|
|
1009
|
+
* leave the record unindexed.
|
|
1010
|
+
*/
|
|
1011
|
+
rewriteContent(id, typeId, content, opts, extraSets = [], extraValues = []) {
|
|
1012
|
+
const json = JSON.stringify(content);
|
|
1013
|
+
fts5Strategy.remove(this.exec, id);
|
|
1014
|
+
this.versionedUpdate(id, ["type_id = ?", "content = ?", ...extraSets], [typeId, json, ...extraValues], opts);
|
|
1015
|
+
fts5Strategy.insert(this.exec, id, json);
|
|
1016
|
+
this.syncContentIndex(id, typeId, content);
|
|
1037
1017
|
}
|
|
1038
1018
|
async queryRecords(query) {
|
|
1039
1019
|
const limit = query.limit ?? 50;
|
|
@@ -1094,8 +1074,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1094
1074
|
return rows.map(rowToVersion);
|
|
1095
1075
|
}
|
|
1096
1076
|
async getVersion(id, version) {
|
|
1097
|
-
const
|
|
1098
|
-
return
|
|
1077
|
+
const row = this.exec.get("SELECT * FROM versions WHERE record_id = ? AND version = ?", [id, version]);
|
|
1078
|
+
return row ? rowToVersion(row) : null;
|
|
1099
1079
|
}
|
|
1100
1080
|
/**
|
|
1101
1081
|
* A (record_id, version) collision is rejected loudly via the UNIQUE
|
|
@@ -1106,21 +1086,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1106
1086
|
insertVersionRow(id, version) {
|
|
1107
1087
|
try {
|
|
1108
1088
|
this.exec.run(`INSERT INTO versions
|
|
1109
|
-
(record_id, version,
|
|
1110
|
-
|
|
1111
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1112
|
-
id,
|
|
1113
|
-
version.version,
|
|
1114
|
-
version.typeId,
|
|
1115
|
-
JSON.stringify(version.content),
|
|
1116
|
-
toMs(version.updatedAt),
|
|
1117
|
-
version.entityId ?? null,
|
|
1118
|
-
version.updatedBy ?? null,
|
|
1119
|
-
version.updatedVia ?? null,
|
|
1120
|
-
version.parentId ?? null,
|
|
1121
|
-
version.associations ? JSON.stringify(version.associations) : null,
|
|
1122
|
-
version.permissions ? JSON.stringify(version.permissions) : null
|
|
1123
|
-
]);
|
|
1089
|
+
(record_id, version, ${VERSION_COLUMNS.join(", ")})
|
|
1090
|
+
VALUES (?, ?, ${VERSION_COLUMNS.map(() => "?").join(", ")})`, [id, version.version, ...versionRowValues(version)]);
|
|
1124
1091
|
} catch (err) {
|
|
1125
1092
|
if (isUniqueConstraintViolation(err)) {
|
|
1126
1093
|
throw new StackConflictError(`Version ${version.version} already exists for record "${id}" \u2014 a concurrent writer raced past this version. Use ifVersion to detect this before it happens.`);
|
|
@@ -1129,28 +1096,15 @@ var SharedSqlRecordLogic = class {
|
|
|
1129
1096
|
}
|
|
1130
1097
|
}
|
|
1131
1098
|
/**
|
|
1132
|
-
* Replaces every column insertVersionRow writes
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1099
|
+
* Replaces every column insertVersionRow writes — structurally, off the
|
|
1100
|
+
* same VERSION_COLUMNS list, because a column left out here would keep
|
|
1101
|
+
* the replaced row's value and surface later as a restore putting back
|
|
1102
|
+
* something the snapshot never said.
|
|
1136
1103
|
*/
|
|
1137
1104
|
overwriteVersionRow(id, version) {
|
|
1138
1105
|
this.exec.run(`UPDATE versions
|
|
1139
|
-
SET
|
|
1140
|
-
|
|
1141
|
-
WHERE record_id = ? AND version = ?`, [
|
|
1142
|
-
version.typeId,
|
|
1143
|
-
JSON.stringify(version.content),
|
|
1144
|
-
toMs(version.updatedAt),
|
|
1145
|
-
version.entityId ?? null,
|
|
1146
|
-
version.updatedBy ?? null,
|
|
1147
|
-
version.updatedVia ?? null,
|
|
1148
|
-
version.parentId ?? null,
|
|
1149
|
-
version.associations ? JSON.stringify(version.associations) : null,
|
|
1150
|
-
version.permissions ? JSON.stringify(version.permissions) : null,
|
|
1151
|
-
id,
|
|
1152
|
-
version.version
|
|
1153
|
-
]);
|
|
1106
|
+
SET ${VERSION_COLUMNS.map((c) => `${c} = ?`).join(", ")}
|
|
1107
|
+
WHERE record_id = ? AND version = ?`, [...versionRowValues(version), id, version.version]);
|
|
1154
1108
|
}
|
|
1155
1109
|
/**
|
|
1156
1110
|
* Standalone snapshot write for tooling and tests — loud on any
|
|
@@ -1201,8 +1155,8 @@ var SharedSqlRecordLogic = class {
|
|
|
1201
1155
|
this.indexedFieldsByType.set(type.id, indexedFieldKinds(schema));
|
|
1202
1156
|
}
|
|
1203
1157
|
async getType(id) {
|
|
1204
|
-
const
|
|
1205
|
-
return
|
|
1158
|
+
const row = this.exec.get("SELECT * FROM types WHERE id = ?", [id]);
|
|
1159
|
+
return row ? rowToType(row) : null;
|
|
1206
1160
|
}
|
|
1207
1161
|
async listTypes() {
|
|
1208
1162
|
const rows = this.exec.all("SELECT * FROM types ORDER BY base_id, version");
|
|
@@ -1215,19 +1169,16 @@ var SharedSqlRecordLogic = class {
|
|
|
1215
1169
|
this.exec.transaction(() => {
|
|
1216
1170
|
if (opts.snapshot)
|
|
1217
1171
|
this.snapshotBeforeMutation(recordId, opts.snapshot);
|
|
1218
|
-
this.
|
|
1172
|
+
this.versionedUpdate(recordId, [], [], opts);
|
|
1219
1173
|
this.insertAssociations(recordId, [association]);
|
|
1220
1174
|
});
|
|
1221
|
-
|
|
1222
|
-
if (!updated)
|
|
1223
|
-
throw new Error(`Record not found after associate: "${recordId}"`);
|
|
1224
|
-
return updated;
|
|
1175
|
+
return this.reread(recordId, "associate");
|
|
1225
1176
|
}
|
|
1226
1177
|
async dissociate(recordId, association, opts = {}) {
|
|
1227
1178
|
this.exec.transaction(() => {
|
|
1228
1179
|
if (opts.snapshot)
|
|
1229
1180
|
this.snapshotBeforeMutation(recordId, opts.snapshot);
|
|
1230
|
-
this.
|
|
1181
|
+
this.versionedUpdate(recordId, [], [], opts);
|
|
1231
1182
|
this.exec.run(`DELETE FROM associations
|
|
1232
1183
|
WHERE record_id = ?
|
|
1233
1184
|
AND kind = ?
|
|
@@ -1238,16 +1189,18 @@ var SharedSqlRecordLogic = class {
|
|
|
1238
1189
|
AND related_ns = ?
|
|
1239
1190
|
AND related_stack = ?`, [recordId, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1240
1191
|
});
|
|
1241
|
-
|
|
1242
|
-
if (!updated)
|
|
1243
|
-
throw new Error(`Record not found after dissociate: "${recordId}"`);
|
|
1244
|
-
return updated;
|
|
1192
|
+
return this.reread(recordId, "dissociate");
|
|
1245
1193
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1194
|
+
/**
|
|
1195
|
+
* Swap a record's association set for `associations` wholesale — the
|
|
1196
|
+
* "replace, don't merge" semantics a change set's association list and a
|
|
1197
|
+
* version snapshot's both carry. Caller-transactional, like every write
|
|
1198
|
+
* below.
|
|
1199
|
+
*/
|
|
1200
|
+
replaceAssociations(recordId, associations) {
|
|
1201
|
+
this.exec.run("DELETE FROM associations WHERE record_id = ?", [recordId]);
|
|
1202
|
+
if (associations.length)
|
|
1203
|
+
this.insertAssociations(recordId, associations);
|
|
1251
1204
|
}
|
|
1252
1205
|
/**
|
|
1253
1206
|
* FK enforcement (PRAGMA_FOREIGN_KEYS_ON) means inserting an
|
|
@@ -1319,6 +1272,134 @@ var SharedSqlRecordLogic = class {
|
|
|
1319
1272
|
return this.getIndexedFields(record.typeId).get(field);
|
|
1320
1273
|
}
|
|
1321
1274
|
};
|
|
1275
|
+
|
|
1276
|
+
// ../sqlite-shared/dist/record-adapter.js
|
|
1277
|
+
var SQLITE_RECORD_CAPABILITIES = {
|
|
1278
|
+
filter: {
|
|
1279
|
+
content: "path",
|
|
1280
|
+
contentPresent: true,
|
|
1281
|
+
search: true
|
|
1282
|
+
},
|
|
1283
|
+
sort: {
|
|
1284
|
+
fields: ["createdAt", "updatedAt", "version"],
|
|
1285
|
+
contentField: true
|
|
1286
|
+
},
|
|
1287
|
+
limits: {
|
|
1288
|
+
attachmentBytes: null,
|
|
1289
|
+
contentBytes: null
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
var SharedSqlRecordAdapter = class {
|
|
1293
|
+
capabilities = SQLITE_RECORD_CAPABILITIES;
|
|
1294
|
+
ownerEntityId;
|
|
1295
|
+
timezone;
|
|
1296
|
+
record;
|
|
1297
|
+
constructor(exec, config) {
|
|
1298
|
+
this.record = new SharedSqlRecordLogic({ exec });
|
|
1299
|
+
this.ownerEntityId = config.entityId;
|
|
1300
|
+
this.timezone = config.timezone;
|
|
1301
|
+
}
|
|
1302
|
+
// -------------------------------------------------------
|
|
1303
|
+
// Records
|
|
1304
|
+
// -------------------------------------------------------
|
|
1305
|
+
createRecord(record) {
|
|
1306
|
+
return this.record.createRecord(record);
|
|
1307
|
+
}
|
|
1308
|
+
getRecord(id) {
|
|
1309
|
+
return this.record.getRecord(id);
|
|
1310
|
+
}
|
|
1311
|
+
mutateRecord(id, changes, opts) {
|
|
1312
|
+
return this.record.mutateRecord(id, changes, opts);
|
|
1313
|
+
}
|
|
1314
|
+
deleteRecord(id, opts) {
|
|
1315
|
+
return this.record.deleteRecord(id, opts);
|
|
1316
|
+
}
|
|
1317
|
+
undeleteRecord(id, opts) {
|
|
1318
|
+
return this.record.undeleteRecord(id, opts);
|
|
1319
|
+
}
|
|
1320
|
+
restoreVersion(id, version, opts) {
|
|
1321
|
+
return this.record.restoreVersion(id, version, opts);
|
|
1322
|
+
}
|
|
1323
|
+
commitMigration(id, toTypeId, content, opts) {
|
|
1324
|
+
return this.record.commitMigration(id, toTypeId, content, opts);
|
|
1325
|
+
}
|
|
1326
|
+
queryRecords(query) {
|
|
1327
|
+
return this.record.queryRecords(query);
|
|
1328
|
+
}
|
|
1329
|
+
deleteUnreferencedAttachmentRecords(fileId, metadataTypeIds) {
|
|
1330
|
+
return this.record.deleteUnreferencedAttachmentRecords(fileId, metadataTypeIds);
|
|
1331
|
+
}
|
|
1332
|
+
// -------------------------------------------------------
|
|
1333
|
+
// Versions
|
|
1334
|
+
// -------------------------------------------------------
|
|
1335
|
+
getVersions(id) {
|
|
1336
|
+
return this.record.getVersions(id);
|
|
1337
|
+
}
|
|
1338
|
+
getVersion(id, version) {
|
|
1339
|
+
return this.record.getVersion(id, version);
|
|
1340
|
+
}
|
|
1341
|
+
saveVersion(id, version) {
|
|
1342
|
+
return this.record.saveVersion(id, version);
|
|
1343
|
+
}
|
|
1344
|
+
// -------------------------------------------------------
|
|
1345
|
+
// Types
|
|
1346
|
+
// -------------------------------------------------------
|
|
1347
|
+
saveType(type) {
|
|
1348
|
+
return this.record.saveType(type);
|
|
1349
|
+
}
|
|
1350
|
+
getType(id) {
|
|
1351
|
+
return this.record.getType(id);
|
|
1352
|
+
}
|
|
1353
|
+
listTypes() {
|
|
1354
|
+
return this.record.listTypes();
|
|
1355
|
+
}
|
|
1356
|
+
// -------------------------------------------------------
|
|
1357
|
+
// Associations
|
|
1358
|
+
// -------------------------------------------------------
|
|
1359
|
+
associate(recordId, association, opts) {
|
|
1360
|
+
return this.record.associate(recordId, association, opts);
|
|
1361
|
+
}
|
|
1362
|
+
dissociate(recordId, association, opts) {
|
|
1363
|
+
return this.record.dissociate(recordId, association, opts);
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
var lockPathFor = (dbPath) => `${dbPath}.lock`;
|
|
1367
|
+
var isProcessAlive = (pid) => {
|
|
1368
|
+
try {
|
|
1369
|
+
process.kill(pid, 0);
|
|
1370
|
+
return true;
|
|
1371
|
+
} catch (err) {
|
|
1372
|
+
return err.code !== "ESRCH";
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
var acquireLock = (dbPath, force) => {
|
|
1376
|
+
const lockPath = lockPathFor(dbPath);
|
|
1377
|
+
const info = existsSync(lockPath) ? readLock(lockPath) : null;
|
|
1378
|
+
if (info) {
|
|
1379
|
+
const ownedBySelf = info.pid === process.pid;
|
|
1380
|
+
if (!ownedBySelf && !force && isProcessAlive(info.pid)) {
|
|
1381
|
+
throw new Error(`Stack database at "${dbPath}" is in use by another process (pid ${info.pid}). Connect via its server instead, or pass { force: true } to override. If no such process is running, remove "${lockPath}".`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
writeFileSync(lockPath, JSON.stringify({ pid: process.pid }));
|
|
1385
|
+
};
|
|
1386
|
+
var readLock = (lockPath) => {
|
|
1387
|
+
try {
|
|
1388
|
+
const info = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
1389
|
+
return typeof info?.pid === "number" ? info : null;
|
|
1390
|
+
} catch {
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
var releaseLock = (dbPath) => {
|
|
1395
|
+
const lockPath = lockPathFor(dbPath);
|
|
1396
|
+
if (!existsSync(lockPath))
|
|
1397
|
+
return;
|
|
1398
|
+
const info = readLock(lockPath);
|
|
1399
|
+
if (!info || info.pid === process.pid)
|
|
1400
|
+
unlinkSync(lockPath);
|
|
1401
|
+
};
|
|
1402
|
+
var hashToken = (token) => createHash("sha256").update(token).digest("hex");
|
|
1322
1403
|
var SharedTokenLogic = class {
|
|
1323
1404
|
deps;
|
|
1324
1405
|
constructor(deps) {
|
|
@@ -1330,7 +1411,7 @@ var SharedTokenLogic = class {
|
|
|
1330
1411
|
async createToken(principalId, opts = {}) {
|
|
1331
1412
|
const id = randomBytes(8).toString("hex");
|
|
1332
1413
|
const token = randomBytes(32).toString("hex");
|
|
1333
|
-
const tokenHash =
|
|
1414
|
+
const tokenHash = hashToken(token);
|
|
1334
1415
|
this.exec.run("INSERT INTO tokens (id, token_hash, principal_id, subject_id, label, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [
|
|
1335
1416
|
id,
|
|
1336
1417
|
tokenHash,
|
|
@@ -1345,7 +1426,7 @@ var SharedTokenLogic = class {
|
|
|
1345
1426
|
return { id, token };
|
|
1346
1427
|
}
|
|
1347
1428
|
async lookupToken(token) {
|
|
1348
|
-
const hash =
|
|
1429
|
+
const hash = hashToken(token);
|
|
1349
1430
|
const row = this.exec.get("SELECT principal_id, subject_id, expires_at FROM tokens WHERE token_hash = ?", [hash]);
|
|
1350
1431
|
if (!row)
|
|
1351
1432
|
return null;
|
|
@@ -1439,33 +1520,26 @@ var NativeTokenStore = class _NativeTokenStore {
|
|
|
1439
1520
|
};
|
|
1440
1521
|
|
|
1441
1522
|
// src/index.ts
|
|
1442
|
-
var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter {
|
|
1443
|
-
constructor(path) {
|
|
1523
|
+
var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter extends SharedSqlRecordAdapter {
|
|
1524
|
+
constructor(path, db, exec, config) {
|
|
1525
|
+
super(exec, config);
|
|
1444
1526
|
this.path = path;
|
|
1527
|
+
this.db = db;
|
|
1445
1528
|
}
|
|
1446
1529
|
path;
|
|
1447
|
-
capabilities = {
|
|
1448
|
-
filter: {
|
|
1449
|
-
content: "path",
|
|
1450
|
-
contentPresent: true,
|
|
1451
|
-
search: true
|
|
1452
|
-
},
|
|
1453
|
-
sort: {
|
|
1454
|
-
fields: ["createdAt", "updatedAt", "version"],
|
|
1455
|
-
contentField: true
|
|
1456
|
-
},
|
|
1457
|
-
limits: {
|
|
1458
|
-
attachmentBytes: null,
|
|
1459
|
-
contentBytes: null
|
|
1460
|
-
}
|
|
1461
|
-
};
|
|
1462
|
-
ownerEntityId;
|
|
1463
|
-
timezone;
|
|
1464
1530
|
db;
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1531
|
+
/**
|
|
1532
|
+
* Takes the storage-ownership lock, opens the file, and brings the
|
|
1533
|
+
* schema up. Shared by initialize() and open(), which differ only in
|
|
1534
|
+
* whether the file may already exist and in where the config record
|
|
1535
|
+
* comes from.
|
|
1536
|
+
*/
|
|
1537
|
+
static attach(path, force) {
|
|
1538
|
+
acquireLock(path, force);
|
|
1539
|
+
const db = new DatabaseSync(path);
|
|
1540
|
+
const exec = new NativeSqliteExecutor(db);
|
|
1541
|
+
applyRecordSchema(exec);
|
|
1542
|
+
return [db, exec];
|
|
1469
1543
|
}
|
|
1470
1544
|
/**
|
|
1471
1545
|
* Initialize a new stack database. Fails if the file already exists —
|
|
@@ -1477,18 +1551,9 @@ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter {
|
|
|
1477
1551
|
`Cannot initialize: database already exists at "${opts.path}". Use NativeSQLiteRecordAdapter.open() instead.`
|
|
1478
1552
|
);
|
|
1479
1553
|
}
|
|
1480
|
-
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
adapter.db.exec(PRAGMA_FOREIGN_KEYS_ON);
|
|
1484
|
-
adapter.db.exec(PRAGMA_JOURNAL_MODE_WAL);
|
|
1485
|
-
adapter.db.exec(RECORD_SCHEMA_SQL);
|
|
1486
|
-
adapter.db.exec(FTS5_SCHEMA_SQL);
|
|
1487
|
-
adapter.wire();
|
|
1488
|
-
insertConfigRecord(new NativeSqliteExecutor(adapter.db), opts.entityId, opts.timezone);
|
|
1489
|
-
adapter.ownerEntityId = opts.entityId;
|
|
1490
|
-
adapter.timezone = opts.timezone;
|
|
1491
|
-
return adapter;
|
|
1554
|
+
const [db, exec] = _NativeSQLiteRecordAdapter.attach(opts.path, opts.force);
|
|
1555
|
+
const config = insertConfigRecord(exec, opts.entityId, opts.timezone);
|
|
1556
|
+
return new _NativeSQLiteRecordAdapter(opts.path, db, exec, config);
|
|
1492
1557
|
}
|
|
1493
1558
|
/**
|
|
1494
1559
|
* Open an existing stack database. Fails if the file does not exist —
|
|
@@ -1500,81 +1565,8 @@ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter {
|
|
|
1500
1565
|
`Cannot open: no database found at "${opts.path}". Use NativeSQLiteRecordAdapter.initialize() to create one.`
|
|
1501
1566
|
);
|
|
1502
1567
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
adapter.db = new DatabaseSync(opts.path);
|
|
1506
|
-
adapter.db.exec(PRAGMA_FOREIGN_KEYS_ON);
|
|
1507
|
-
adapter.db.exec(PRAGMA_JOURNAL_MODE_WAL);
|
|
1508
|
-
adapter.db.exec(RECORD_SCHEMA_SQL);
|
|
1509
|
-
adapter.db.exec(FTS5_SCHEMA_SQL);
|
|
1510
|
-
adapter.wire();
|
|
1511
|
-
const config = readStackConfig(new NativeSqliteExecutor(adapter.db));
|
|
1512
|
-
adapter.ownerEntityId = config.entityId;
|
|
1513
|
-
adapter.timezone = config.timezone;
|
|
1514
|
-
return adapter;
|
|
1515
|
-
}
|
|
1516
|
-
// -------------------------------------------------------
|
|
1517
|
-
// Records
|
|
1518
|
-
// -------------------------------------------------------
|
|
1519
|
-
createRecord(record) {
|
|
1520
|
-
return this.record.createRecord(record);
|
|
1521
|
-
}
|
|
1522
|
-
getRecord(id) {
|
|
1523
|
-
return this.record.getRecord(id);
|
|
1524
|
-
}
|
|
1525
|
-
mutateRecord(id, changes, opts) {
|
|
1526
|
-
return this.record.mutateRecord(id, changes, opts);
|
|
1527
|
-
}
|
|
1528
|
-
deleteRecord(id, opts) {
|
|
1529
|
-
return this.record.deleteRecord(id, opts);
|
|
1530
|
-
}
|
|
1531
|
-
undeleteRecord(id, opts) {
|
|
1532
|
-
return this.record.undeleteRecord(id, opts);
|
|
1533
|
-
}
|
|
1534
|
-
restoreVersion(id, version, opts) {
|
|
1535
|
-
return this.record.restoreVersion(id, version, opts);
|
|
1536
|
-
}
|
|
1537
|
-
commitMigration(id, toTypeId, content, opts) {
|
|
1538
|
-
return this.record.commitMigration(id, toTypeId, content, opts);
|
|
1539
|
-
}
|
|
1540
|
-
queryRecords(query) {
|
|
1541
|
-
return this.record.queryRecords(query);
|
|
1542
|
-
}
|
|
1543
|
-
deleteUnreferencedAttachmentRecords(fileId, metadataTypeIds) {
|
|
1544
|
-
return this.record.deleteUnreferencedAttachmentRecords(fileId, metadataTypeIds);
|
|
1545
|
-
}
|
|
1546
|
-
// -------------------------------------------------------
|
|
1547
|
-
// Versions
|
|
1548
|
-
// -------------------------------------------------------
|
|
1549
|
-
getVersions(id) {
|
|
1550
|
-
return this.record.getVersions(id);
|
|
1551
|
-
}
|
|
1552
|
-
getVersion(id, version) {
|
|
1553
|
-
return this.record.getVersion(id, version);
|
|
1554
|
-
}
|
|
1555
|
-
saveVersion(id, version) {
|
|
1556
|
-
return this.record.saveVersion(id, version);
|
|
1557
|
-
}
|
|
1558
|
-
// -------------------------------------------------------
|
|
1559
|
-
// Types
|
|
1560
|
-
// -------------------------------------------------------
|
|
1561
|
-
saveType(type) {
|
|
1562
|
-
return this.record.saveType(type);
|
|
1563
|
-
}
|
|
1564
|
-
getType(id) {
|
|
1565
|
-
return this.record.getType(id);
|
|
1566
|
-
}
|
|
1567
|
-
listTypes() {
|
|
1568
|
-
return this.record.listTypes();
|
|
1569
|
-
}
|
|
1570
|
-
// -------------------------------------------------------
|
|
1571
|
-
// Associations
|
|
1572
|
-
// -------------------------------------------------------
|
|
1573
|
-
associate(recordId, association, opts) {
|
|
1574
|
-
return this.record.associate(recordId, association, opts);
|
|
1575
|
-
}
|
|
1576
|
-
dissociate(recordId, association, opts) {
|
|
1577
|
-
return this.record.dissociate(recordId, association, opts);
|
|
1568
|
+
const [db, exec] = _NativeSQLiteRecordAdapter.attach(opts.path, opts.force);
|
|
1569
|
+
return new _NativeSQLiteRecordAdapter(opts.path, db, exec, readStackConfig(exec));
|
|
1578
1570
|
}
|
|
1579
1571
|
// -------------------------------------------------------
|
|
1580
1572
|
// Lifecycle
|