@haverstack/record-adapter-sqlite 0.31.0 → 0.33.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 +31 -33
- package/dist/index.js +84 -65
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StackCapabilities, StackRecord, RecordChangeSet, RecordVersion, ActorOptions, TypeId, StackQuery, QueryResult, FileId, JournalQuery, RecordJournalEntry, StackType,
|
|
1
|
+
import { StackCapabilities, StackRecord, RecordChangeSet, RecordVersion, ActorOptions, TypeId, StackQuery, QueryResult, FileId, VersionsQuery, JournalQuery, RecordJournalEntry, StackType, AssociationEdit, Actor } from '@haverstack/core';
|
|
2
2
|
import { StackRecordAdapter, JournalOptions } from '@haverstack/core/adapter';
|
|
3
3
|
import { StackTokenStore, TokenSession, TokenInfo } from '@haverstack/core/wire';
|
|
4
4
|
|
|
@@ -91,15 +91,14 @@ declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
|
|
|
91
91
|
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
92
92
|
queryRecords(query: StackQuery): Promise<QueryResult>;
|
|
93
93
|
deleteUnreferencedAttachmentRecords(fileId: FileId, metadataTypeIds: TypeId[]): Promise<StackRecord[]>;
|
|
94
|
-
getVersions(id: string): Promise<RecordVersion[]>;
|
|
94
|
+
getVersions(id: string, query?: VersionsQuery): Promise<RecordVersion[]>;
|
|
95
95
|
getVersion(id: string, version: number): Promise<RecordVersion | null>;
|
|
96
96
|
saveVersion(id: string, version: RecordVersion): Promise<void>;
|
|
97
97
|
getJournal(id: string, query?: JournalQuery): Promise<RecordJournalEntry[]>;
|
|
98
98
|
saveType(type: StackType): Promise<void>;
|
|
99
99
|
getType(id: TypeId): Promise<StackType | null>;
|
|
100
100
|
listTypes(): Promise<StackType[]>;
|
|
101
|
-
|
|
102
|
-
dissociate(recordId: string, association: Association, opts?: JournalOptions): Promise<StackRecord>;
|
|
101
|
+
amendAssociations(recordId: string, changes: AssociationEdit[], opts?: JournalOptions): Promise<StackRecord>;
|
|
103
102
|
abstract flush(): Promise<void>;
|
|
104
103
|
abstract close(): Promise<void>;
|
|
105
104
|
}
|
|
@@ -157,8 +156,8 @@ declare class NativeTokenStore implements StackTokenStore {
|
|
|
157
156
|
* Full-text search uses FTS5.
|
|
158
157
|
*
|
|
159
158
|
* A stack file is owned by exactly one process at a time (see
|
|
160
|
-
* docs/spec/adapters.md § Concurrency & storage ownership). open()
|
|
161
|
-
*
|
|
159
|
+
* docs/spec/adapters.md § Concurrency & storage ownership). open()
|
|
160
|
+
* acquires a PID-stamped lock file beside the database and rejects if
|
|
162
161
|
* another live process already holds it; close() releases it.
|
|
163
162
|
*
|
|
164
163
|
* Token storage is a separate concern — see NativeTokenStore in this
|
|
@@ -173,19 +172,15 @@ declare class NativeTokenStore implements StackTokenStore {
|
|
|
173
172
|
* spread-args run/get/all calls to it).
|
|
174
173
|
*/
|
|
175
174
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
timezone?: string;
|
|
181
|
-
/** Entity ID of the stack owner. */
|
|
182
|
-
ownerEntityId: string;
|
|
183
|
-
/** Bypass the storage-ownership lock check. See NativeSQLiteRecordAdapterOpenOptions.force. */
|
|
184
|
-
force?: boolean;
|
|
185
|
-
};
|
|
175
|
+
/** What open() does when the store is missing or present, like O_CREAT / O_EXCL. */
|
|
176
|
+
type StoreCreateMode = 'never' | 'ifMissing' | 'exclusive';
|
|
177
|
+
/** A plain DID, or a lazy provider called only when a store is actually created. */
|
|
178
|
+
type OwnerEntityIdInput = string | (() => string | Promise<string>);
|
|
186
179
|
type NativeSQLiteRecordAdapterOpenOptions = {
|
|
187
|
-
/** Absolute path to
|
|
180
|
+
/** Absolute path to the .db file. */
|
|
188
181
|
path: string;
|
|
182
|
+
/** IANA timezone string e.g. "America/New_York". Consulted only when a store is created. No default. */
|
|
183
|
+
timezone?: string;
|
|
189
184
|
/**
|
|
190
185
|
* Open even if a lock file from another live process is present.
|
|
191
186
|
* Only needed if that process is gone but its PID was reused by
|
|
@@ -193,26 +188,29 @@ type NativeSQLiteRecordAdapterOpenOptions = {
|
|
|
193
188
|
* locks whose owning process is no longer running).
|
|
194
189
|
*/
|
|
195
190
|
force?: boolean;
|
|
196
|
-
}
|
|
191
|
+
} & ({
|
|
192
|
+
/** Open an existing store; fail if the file is missing. */
|
|
193
|
+
create?: 'never';
|
|
194
|
+
/** Checked against the existing store's owner; a mismatch throws OwnerMismatchError. */
|
|
195
|
+
ownerEntityId?: string;
|
|
196
|
+
} | {
|
|
197
|
+
/** `'ifMissing'` opens the store if present and creates it if not; `'exclusive'` creates it and fails if present. */
|
|
198
|
+
create: 'ifMissing' | 'exclusive';
|
|
199
|
+
/**
|
|
200
|
+
* Owner of a newly created store. A plain string is also checked against an
|
|
201
|
+
* existing store's owner; a lazy provider is never called just to compare.
|
|
202
|
+
*/
|
|
203
|
+
ownerEntityId: OwnerEntityIdInput;
|
|
204
|
+
});
|
|
197
205
|
declare class NativeSQLiteRecordAdapter extends SharedSqlRecordAdapter {
|
|
198
206
|
private readonly path;
|
|
199
207
|
private readonly db;
|
|
200
208
|
private constructor();
|
|
201
209
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*/
|
|
207
|
-
private static attach;
|
|
208
|
-
/**
|
|
209
|
-
* Initialize a new stack database. Fails if the file already exists —
|
|
210
|
-
* use open() for existing databases.
|
|
211
|
-
*/
|
|
212
|
-
static initialize(opts: NativeSQLiteRecordAdapterInitializeOptions): Promise<NativeSQLiteRecordAdapter>;
|
|
213
|
-
/**
|
|
214
|
-
* Open an existing stack database. Fails if the file does not exist —
|
|
215
|
-
* use initialize() for new databases.
|
|
210
|
+
* Opens or creates the stack database at `path` according to `create`.
|
|
211
|
+
* Takes the storage-ownership lock first and releases it again if the
|
|
212
|
+
* open fails, since the caller never receives an adapter to close.
|
|
213
|
+
* See docs/spec/adapters.md § Construction.
|
|
216
214
|
*/
|
|
217
215
|
static open(opts: NativeSQLiteRecordAdapterOpenOptions): Promise<NativeSQLiteRecordAdapter>;
|
|
218
216
|
/** Folds the WAL back into the main file — useful before copying/backing up the database. */
|
|
@@ -220,4 +218,4 @@ declare class NativeSQLiteRecordAdapter extends SharedSqlRecordAdapter {
|
|
|
220
218
|
close(): Promise<void>;
|
|
221
219
|
}
|
|
222
220
|
|
|
223
|
-
export { NativeSQLiteRecordAdapter, type
|
|
221
|
+
export { NativeSQLiteRecordAdapter, type NativeSQLiteRecordAdapterOpenOptions, NativeTokenStore, type NativeTokenStoreOpenOptions, type OwnerEntityIdInput, type StoreCreateMode, defaultTokenStorePath };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
2
|
-
import { StackNotFoundError, StackConflictError, applyMergePatch, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
3
|
-
import { contentSortEntry, parseContentFilterKey, contentSortKey } from '@haverstack/core/adapter';
|
|
2
|
+
import { StackNotFoundError, StackConflictError, applyMergePatch, assertOneSurface, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
|
|
3
|
+
import { OwnerMismatchError, contentSortEntry, parseContentFilterKey, contentSortKey } from '@haverstack/core/adapter';
|
|
4
4
|
import { randomBytes, createHash } from 'crypto';
|
|
5
5
|
|
|
6
6
|
// src/node-sqlite.ts
|
|
@@ -44,7 +44,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
44
44
|
--
|
|
45
45
|
-- attachment_record_id stays out of the primary key: it annotates a
|
|
46
46
|
-- reference rather than naming one, so re-pointing it updates the row an
|
|
47
|
-
--
|
|
47
|
+
-- an amendAssociations() add lands on instead of writing a second reference to the same
|
|
48
48
|
-- file. See docs/spec/attachments.md \xA7 Naming the upload a reference came from.
|
|
49
49
|
--
|
|
50
50
|
-- The primary key leads with record_id, so its implicit index already
|
|
@@ -93,7 +93,7 @@ var RECORD_SCHEMA_SQL = `
|
|
|
93
93
|
-- No 'purged': a purge destroys the log rather than appending to it,
|
|
94
94
|
-- so the constraint names exactly the kinds an entry can carry.
|
|
95
95
|
-- See docs/spec/journal.md \xA7 A purge destroys the journal.
|
|
96
|
-
kind TEXT NOT NULL CHECK (kind IN ('created', 'changed', '
|
|
96
|
+
kind TEXT NOT NULL CHECK (kind IN ('created', 'changed', 'removed')),
|
|
97
97
|
ops TEXT NOT NULL CHECK (json_valid(ops)),
|
|
98
98
|
version INTEGER NOT NULL,
|
|
99
99
|
type_id TEXT NOT NULL,
|
|
@@ -375,7 +375,7 @@ var atBudget = (page, rows) => ({
|
|
|
375
375
|
params: [...page.params, rows]
|
|
376
376
|
});
|
|
377
377
|
var sqlDirection = (query) => {
|
|
378
|
-
const dir = query.sort
|
|
378
|
+
const dir = query.sort ? query.sort.direction : "desc";
|
|
379
379
|
if (dir !== "asc" && dir !== "desc") {
|
|
380
380
|
throw new StackBadRequestError(`Invalid sort direction "${dir}": expected "asc" or "desc".`);
|
|
381
381
|
}
|
|
@@ -1191,8 +1191,14 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1191
1191
|
// -------------------------------------------------------
|
|
1192
1192
|
// Versions
|
|
1193
1193
|
// -------------------------------------------------------
|
|
1194
|
-
async getVersions(id) {
|
|
1195
|
-
const
|
|
1194
|
+
async getVersions(id, query = {}) {
|
|
1195
|
+
const conditions = ["record_id = ?"];
|
|
1196
|
+
const values = [id];
|
|
1197
|
+
if (query.beforeVersion !== void 0) {
|
|
1198
|
+
conditions.push("version < ?");
|
|
1199
|
+
values.push(query.beforeVersion);
|
|
1200
|
+
}
|
|
1201
|
+
const rows = this.exec.all(`SELECT * FROM versions WHERE ${conditions.join(" AND ")} ORDER BY version DESC${query.limit !== void 0 ? " LIMIT ?" : ""}`, query.limit !== void 0 ? [...values, query.limit] : values);
|
|
1196
1202
|
return rows.map(rowToVersion);
|
|
1197
1203
|
}
|
|
1198
1204
|
async getVersion(id, version) {
|
|
@@ -1342,24 +1348,20 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1342
1348
|
// -------------------------------------------------------
|
|
1343
1349
|
/**
|
|
1344
1350
|
* Never bumps `version`/`updatedAt` and never snapshots — see
|
|
1345
|
-
* StackRecordAdapter.
|
|
1346
|
-
* only the associations table changes
|
|
1351
|
+
* StackRecordAdapter.amendAssociations(). The records row is untouched
|
|
1352
|
+
* entirely; only the associations table changes, in one transaction:
|
|
1353
|
+
* removes first, then adds.
|
|
1347
1354
|
*/
|
|
1348
|
-
async
|
|
1355
|
+
async amendAssociations(recordId, changes, opts = {}) {
|
|
1356
|
+
assertOneSurface(changes);
|
|
1349
1357
|
this.exec.transaction(() => {
|
|
1350
1358
|
if (!this.readRecord(recordId))
|
|
1351
1359
|
throw new StackNotFoundError(`Record not found: "${recordId}"`);
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
/** Never bumps `version`/`updatedAt` — see associate(). */
|
|
1358
|
-
async dissociate(recordId, association, opts = {}) {
|
|
1359
|
-
this.exec.transaction(() => {
|
|
1360
|
-
if (!this.readRecord(recordId))
|
|
1361
|
-
throw new StackNotFoundError(`Record not found: "${recordId}"`);
|
|
1362
|
-
this.exec.run(`DELETE FROM associations
|
|
1360
|
+
for (const change of changes) {
|
|
1361
|
+
if (change.op !== "remove")
|
|
1362
|
+
continue;
|
|
1363
|
+
const { association } = change;
|
|
1364
|
+
this.exec.run(`DELETE FROM associations
|
|
1363
1365
|
WHERE record_id = ?
|
|
1364
1366
|
AND kind = ?
|
|
1365
1367
|
AND label = ?
|
|
@@ -1369,9 +1371,13 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1369
1371
|
AND related_ns = ?
|
|
1370
1372
|
AND related_stack = ?
|
|
1371
1373
|
AND related_role = ?`, [recordId, association.kind, association.label, ...associationKeyColumns(association)]);
|
|
1374
|
+
}
|
|
1375
|
+
const adds = changes.flatMap((c) => c.op === "add" ? [c.association] : []);
|
|
1376
|
+
if (adds.length)
|
|
1377
|
+
this.insertAssociations(recordId, adds);
|
|
1372
1378
|
this.appendJournal(recordId, opts.journal);
|
|
1373
1379
|
});
|
|
1374
|
-
return this.reread(recordId, "
|
|
1380
|
+
return this.reread(recordId, "amendAssociations");
|
|
1375
1381
|
}
|
|
1376
1382
|
/** The kinds each change-set key replaces, and nothing else. */
|
|
1377
1383
|
static DATA_KINDS = ["tag", "attachment", "relationship"];
|
|
@@ -1408,7 +1414,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
|
|
|
1408
1414
|
*
|
|
1409
1415
|
* FK enforcement (PRAGMA_FOREIGN_KEYS_ON) means inserting an
|
|
1410
1416
|
* association against a record that doesn't exist throws — mapped
|
|
1411
|
-
* here to StackNotFoundError so
|
|
1417
|
+
* here to StackNotFoundError so amendAssociations() on a nonexistent record
|
|
1412
1418
|
* fails loudly instead of silently creating an orphan row.
|
|
1413
1419
|
*/
|
|
1414
1420
|
insertAssociations(recordId, associations) {
|
|
@@ -1543,8 +1549,8 @@ var SharedSqlRecordAdapter = class {
|
|
|
1543
1549
|
// -------------------------------------------------------
|
|
1544
1550
|
// Versions
|
|
1545
1551
|
// -------------------------------------------------------
|
|
1546
|
-
getVersions(id) {
|
|
1547
|
-
return this.record.getVersions(id);
|
|
1552
|
+
getVersions(id, query) {
|
|
1553
|
+
return this.record.getVersions(id, query);
|
|
1548
1554
|
}
|
|
1549
1555
|
getVersion(id, version) {
|
|
1550
1556
|
return this.record.getVersion(id, version);
|
|
@@ -1573,11 +1579,8 @@ var SharedSqlRecordAdapter = class {
|
|
|
1573
1579
|
// -------------------------------------------------------
|
|
1574
1580
|
// Associations
|
|
1575
1581
|
// -------------------------------------------------------
|
|
1576
|
-
|
|
1577
|
-
return this.record.
|
|
1578
|
-
}
|
|
1579
|
-
dissociate(recordId, association, opts) {
|
|
1580
|
-
return this.record.dissociate(recordId, association, opts);
|
|
1582
|
+
amendAssociations(recordId, changes, opts) {
|
|
1583
|
+
return this.record.amendAssociations(recordId, changes, opts);
|
|
1581
1584
|
}
|
|
1582
1585
|
};
|
|
1583
1586
|
var lockPathFor = (dbPath) => `${dbPath}.lock`;
|
|
@@ -1746,44 +1749,60 @@ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter extends SharedS
|
|
|
1746
1749
|
path;
|
|
1747
1750
|
db;
|
|
1748
1751
|
/**
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
*/
|
|
1754
|
-
static attach(path, force) {
|
|
1755
|
-
acquireLock(path, force);
|
|
1756
|
-
const db = new DatabaseSync(path);
|
|
1757
|
-
const exec = new NativeSqliteExecutor(db);
|
|
1758
|
-
applyRecordSchema(exec);
|
|
1759
|
-
return [db, exec];
|
|
1760
|
-
}
|
|
1761
|
-
/**
|
|
1762
|
-
* Initialize a new stack database. Fails if the file already exists —
|
|
1763
|
-
* use open() for existing databases.
|
|
1764
|
-
*/
|
|
1765
|
-
static async initialize(opts) {
|
|
1766
|
-
if (existsSync(opts.path)) {
|
|
1767
|
-
throw new Error(
|
|
1768
|
-
`Cannot initialize: database already exists at "${opts.path}". Use NativeSQLiteRecordAdapter.open() instead.`
|
|
1769
|
-
);
|
|
1770
|
-
}
|
|
1771
|
-
const [db, exec] = _NativeSQLiteRecordAdapter.attach(opts.path, opts.force);
|
|
1772
|
-
const config = insertConfigRecord(exec, opts.ownerEntityId, opts.timezone);
|
|
1773
|
-
return new _NativeSQLiteRecordAdapter(opts.path, db, exec, config);
|
|
1774
|
-
}
|
|
1775
|
-
/**
|
|
1776
|
-
* Open an existing stack database. Fails if the file does not exist —
|
|
1777
|
-
* use initialize() for new databases.
|
|
1752
|
+
* Opens or creates the stack database at `path` according to `create`.
|
|
1753
|
+
* Takes the storage-ownership lock first and releases it again if the
|
|
1754
|
+
* open fails, since the caller never receives an adapter to close.
|
|
1755
|
+
* See docs/spec/adapters.md § Construction.
|
|
1778
1756
|
*/
|
|
1779
1757
|
static async open(opts) {
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1758
|
+
const mode = opts.create ?? "never";
|
|
1759
|
+
const checkMode = (exists) => {
|
|
1760
|
+
if (!exists && mode === "never") {
|
|
1761
|
+
throw new Error(
|
|
1762
|
+
`Cannot open: no database found at "${opts.path}". Pass create: 'ifMissing' or 'exclusive' to create one.`
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
if (exists && mode === "exclusive") {
|
|
1766
|
+
throw new Error(
|
|
1767
|
+
`Cannot create: database already exists at "${opts.path}". Pass create: 'never' or 'ifMissing' to open it.`
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
const existedBeforeLock = existsSync(opts.path);
|
|
1772
|
+
checkMode(existedBeforeLock);
|
|
1773
|
+
let newOwner;
|
|
1774
|
+
if (!existedBeforeLock) {
|
|
1775
|
+
newOwner = typeof opts.ownerEntityId === "function" ? await opts.ownerEntityId() : opts.ownerEntityId;
|
|
1776
|
+
if (!newOwner) {
|
|
1777
|
+
throw new Error(
|
|
1778
|
+
`Cannot create: no ownerEntityId given for the new database at "${opts.path}".`
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
acquireLock(opts.path, opts.force);
|
|
1783
|
+
let db;
|
|
1784
|
+
try {
|
|
1785
|
+
const exists = existsSync(opts.path);
|
|
1786
|
+
checkMode(exists);
|
|
1787
|
+
db = new DatabaseSync(opts.path);
|
|
1788
|
+
const exec = new NativeSqliteExecutor(db);
|
|
1789
|
+
applyRecordSchema(exec, { wal: true });
|
|
1790
|
+
let config;
|
|
1791
|
+
if (exists) {
|
|
1792
|
+
config = readStackConfig(exec);
|
|
1793
|
+
if (typeof opts.ownerEntityId === "string" && opts.ownerEntityId !== config.entityId) {
|
|
1794
|
+
throw new OwnerMismatchError(opts.ownerEntityId, config.entityId, `"${opts.path}"`);
|
|
1795
|
+
}
|
|
1796
|
+
} else {
|
|
1797
|
+
if (!newOwner) throw new Error(`Cannot create: "${opts.path}" vanished while opening.`);
|
|
1798
|
+
config = insertConfigRecord(exec, newOwner, opts.timezone);
|
|
1799
|
+
}
|
|
1800
|
+
return new _NativeSQLiteRecordAdapter(opts.path, db, exec, config);
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
db?.close();
|
|
1803
|
+
releaseLock(opts.path);
|
|
1804
|
+
throw err;
|
|
1784
1805
|
}
|
|
1785
|
-
const [db, exec] = _NativeSQLiteRecordAdapter.attach(opts.path, opts.force);
|
|
1786
|
-
return new _NativeSQLiteRecordAdapter(opts.path, db, exec, readStackConfig(exec));
|
|
1787
1806
|
}
|
|
1788
1807
|
// -------------------------------------------------------
|
|
1789
1808
|
// Lifecycle
|