@haverstack/record-adapter-sqlite 0.22.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 CHANGED
@@ -1,7 +1,115 @@
1
- import { TokenSession, StackRecord, RecordChanges, RecordVersion, ActorOptions, TypeId, StackQuery, QueryResult, FileId, StackType, Association } from '@haverstack/core';
1
+ import { StackRecord, RecordChanges, RecordVersion, ActorOptions, TypeId, StackQuery, QueryResult, FileId, StackType, Association, TokenSession } from '@haverstack/core';
2
2
  import { StackRecordAdapter, AdapterCapabilities } from '@haverstack/core/adapter';
3
3
  import { StackTokenStore, TokenInfo } from '@haverstack/core/wire';
4
4
 
5
+ /**
6
+ * Normalizes a SQLite binding's call convention (array-bind-then-step,
7
+ * spread-args run/get/all, cursor-returning exec) behind one interface,
8
+ * so the actual CRUD/query/version/type/association/token logic can live
9
+ * once in this package instead of being re-typed per engine. Each adapter
10
+ * package provides a thin SqlExecutor implementation over its own
11
+ * database handle.
12
+ *
13
+ * Deliberately synchronous: every binding this targets executes queries
14
+ * in-process without yielding, and an async interface would force the
15
+ * shared logic's transaction sequences to interleave.
16
+ */
17
+ interface SqlExecutor {
18
+ /** Run DDL, pragmas, or any statement(s) with no bound params and no results needed. */
19
+ exec(sql: string): void;
20
+ /** Run a single parameterized statement. Returns the number of rows affected (INSERT/UPDATE/DELETE). */
21
+ run(sql: string, params?: readonly unknown[]): number;
22
+ /** Run a parameterized statement and return the first result row, or undefined. */
23
+ get<T = Record<string, unknown>>(sql: string, params?: readonly unknown[]): T | undefined;
24
+ /** Run a parameterized statement and return all result rows. */
25
+ all<T = Record<string, unknown>>(sql: string, params?: readonly unknown[]): T[];
26
+ /**
27
+ * Run `fn` atomically: every statement it issues commits together, or
28
+ * none do if it throws. `fn` must be synchronous and call back into
29
+ * this executor only — not every engine's transaction primitive can
30
+ * straddle a suspended call. A binding with a real multi-statement
31
+ * transaction (BEGIN/COMMIT/ROLLBACK) implements this with one; a
32
+ * binding whose transaction primitive is itself a callback wrapper
33
+ * (e.g. a Durable Object's storage.transactionSync) can pass `fn`
34
+ * straight through to it.
35
+ */
36
+ transaction<T>(fn: () => T): T;
37
+ }
38
+
39
+ /** timezone is optional passthrough app metadata — no default. */
40
+ type StackConfig = {
41
+ entityId: string;
42
+ timezone: string | undefined;
43
+ };
44
+
45
+ /**
46
+ * The engine-independent half of a SQLite-backed StackRecordAdapter: the
47
+ * capability declaration and the delegation of all seventeen
48
+ * StackRecordAdapter methods onto SharedSqlRecordLogic. Every method here
49
+ * is a forward with no engine-specific content, so an adapter that owns a
50
+ * SqlExecutor already knows how to answer all of them.
51
+ *
52
+ * What a concrete adapter is left to supply is exactly what differs
53
+ * between bindings: constructing the database handle, applying the
54
+ * schema, and its own lifecycle (flush/close) — hence those two being the
55
+ * only abstract members.
56
+ *
57
+ * Lives in record.js's barrel rather than index.js's, so an adapter that
58
+ * imports it never pulls in token-logic.ts's `node:crypto` — see the note
59
+ * at the top of record.ts.
60
+ */
61
+
62
+ declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
63
+ readonly capabilities: AdapterCapabilities;
64
+ readonly ownerEntityId: string;
65
+ readonly timezone: string | undefined;
66
+ private readonly record;
67
+ protected constructor(exec: SqlExecutor, config: StackConfig);
68
+ createRecord(record: StackRecord): Promise<StackRecord>;
69
+ getRecord(id: string): Promise<StackRecord | null>;
70
+ mutateRecord(id: string, changes: RecordChanges, opts?: {
71
+ expectedVersion?: number;
72
+ snapshot?: RecordVersion;
73
+ } & ActorOptions): Promise<StackRecord>;
74
+ deleteRecord(id: string, opts?: {
75
+ hard?: boolean;
76
+ expectedVersion?: number;
77
+ snapshot?: RecordVersion;
78
+ } & ActorOptions): Promise<StackRecord | null>;
79
+ undeleteRecord(id: string, opts?: {
80
+ expectedVersion?: number;
81
+ snapshot?: RecordVersion;
82
+ } & ActorOptions): Promise<StackRecord>;
83
+ restoreVersion(id: string, version: number, opts?: {
84
+ expectedVersion?: number;
85
+ snapshot?: RecordVersion;
86
+ } & {
87
+ restoreAssociations?: boolean;
88
+ } & ActorOptions): Promise<StackRecord>;
89
+ commitMigration(id: string, toTypeId: TypeId, content: Record<string, unknown>, opts?: {
90
+ expectedVersion?: number;
91
+ snapshot?: RecordVersion;
92
+ } & ActorOptions): Promise<StackRecord>;
93
+ queryRecords(query: StackQuery): Promise<QueryResult>;
94
+ deleteUnreferencedAttachmentRecords(fileId: FileId, metadataTypeIds: TypeId[]): Promise<StackRecord[]>;
95
+ getVersions(id: string): Promise<RecordVersion[]>;
96
+ getVersion(id: string, version: number): Promise<RecordVersion | null>;
97
+ saveVersion(id: string, version: RecordVersion): Promise<void>;
98
+ saveType(type: StackType): Promise<void>;
99
+ getType(id: TypeId): Promise<StackType | null>;
100
+ listTypes(): Promise<StackType[]>;
101
+ associate(recordId: string, association: Association, opts?: {
102
+ expectedVersion?: number;
103
+ snapshot?: RecordVersion;
104
+ } & ActorOptions): Promise<StackRecord>;
105
+ dissociate(recordId: string, association: Association, opts?: {
106
+ expectedVersion?: number;
107
+ snapshot?: RecordVersion;
108
+ } & ActorOptions): Promise<StackRecord>;
109
+ abstract flush(): Promise<void>;
110
+ abstract close(): Promise<void>;
111
+ }
112
+
5
113
  /**
6
114
  * Bearer-token storage backing @haverstack/core's StackTokenStore,
7
115
  * implemented against its own file — deliberately separate from
@@ -64,12 +172,12 @@ declare class NativeTokenStore implements StackTokenStore {
64
172
  * package, which implements @haverstack/core's StackTokenStore against
65
173
  * its own file rather than this adapter's records database.
66
174
  *
67
- * This class itself is a thin node:sqlite binding: schema setup,
68
- * pragmas, and the storage-ownership lock are genuinely engine-specific
69
- * and live here. The actual StackRecordAdapter logic lives in
70
- * @haverstack/sqlite-shared's SharedSqlRecordLogic, reached through the
71
- * SqlExecutor interface (NativeSqliteExecutor here normalizes
72
- * node:sqlite's spread-args run/get/all calls to it).
175
+ * This class itself is a thin node:sqlite binding: opening the file, the
176
+ * storage-ownership lock, and the WAL checkpoint are what's genuinely
177
+ * engine-specific and live here. Everything above storage comes from
178
+ * @haverstack/sqlite-shared's SharedSqlRecordAdapter, reached through the
179
+ * SqlExecutor interface (NativeSqliteExecutor here normalizes node:sqlite's
180
+ * spread-args run/get/all calls to it).
73
181
  */
74
182
 
75
183
  type NativeRecordInitializeOptions = {
@@ -93,15 +201,17 @@ type NativeRecordOpenOptions = {
93
201
  */
94
202
  force?: boolean;
95
203
  };
96
- declare class NativeSQLiteRecordAdapter implements StackRecordAdapter {
204
+ declare class NativeSQLiteRecordAdapter extends SharedSqlRecordAdapter {
97
205
  private readonly path;
98
- readonly capabilities: AdapterCapabilities;
99
- ownerEntityId: string;
100
- timezone: string | undefined;
101
- private db;
102
- private record;
206
+ private readonly db;
103
207
  private constructor();
104
- private wire;
208
+ /**
209
+ * Takes the storage-ownership lock, opens the file, and brings the
210
+ * schema up. Shared by initialize() and open(), which differ only in
211
+ * whether the file may already exist and in where the config record
212
+ * comes from.
213
+ */
214
+ private static attach;
105
215
  /**
106
216
  * Initialize a new stack database. Fails if the file already exists —
107
217
  * use open() for existing databases.
@@ -112,47 +222,6 @@ declare class NativeSQLiteRecordAdapter implements StackRecordAdapter {
112
222
  * use initialize() for new databases.
113
223
  */
114
224
  static open(opts: NativeRecordOpenOptions): Promise<NativeSQLiteRecordAdapter>;
115
- createRecord(record: StackRecord): Promise<StackRecord>;
116
- getRecord(id: string): Promise<StackRecord | null>;
117
- mutateRecord(id: string, changes: RecordChanges, opts?: {
118
- expectedVersion?: number;
119
- snapshot?: RecordVersion;
120
- } & ActorOptions): Promise<StackRecord>;
121
- deleteRecord(id: string, opts?: {
122
- hard?: boolean;
123
- expectedVersion?: number;
124
- snapshot?: RecordVersion;
125
- } & ActorOptions): Promise<StackRecord | null>;
126
- undeleteRecord(id: string, opts?: {
127
- expectedVersion?: number;
128
- snapshot?: RecordVersion;
129
- } & ActorOptions): Promise<StackRecord>;
130
- restoreVersion(id: string, version: number, opts?: {
131
- expectedVersion?: number;
132
- snapshot?: RecordVersion;
133
- } & {
134
- restoreAssociations?: boolean;
135
- } & ActorOptions): Promise<StackRecord>;
136
- commitMigration(id: string, toTypeId: TypeId, content: Record<string, unknown>, opts?: {
137
- expectedVersion?: number;
138
- snapshot?: RecordVersion;
139
- } & ActorOptions): Promise<StackRecord>;
140
- queryRecords(query: StackQuery): Promise<QueryResult>;
141
- deleteUnreferencedAttachmentRecords(fileId: FileId, metadataTypeIds: TypeId[]): Promise<StackRecord[]>;
142
- getVersions(id: string): Promise<RecordVersion[]>;
143
- getVersion(id: string, version: number): Promise<RecordVersion | null>;
144
- saveVersion(id: string, version: RecordVersion): Promise<void>;
145
- saveType(type: StackType): Promise<void>;
146
- getType(id: TypeId): Promise<StackType | null>;
147
- listTypes(): Promise<StackType[]>;
148
- associate(recordId: string, association: Association, opts?: {
149
- expectedVersion?: number;
150
- snapshot?: RecordVersion;
151
- } & ActorOptions): Promise<StackRecord>;
152
- dissociate(recordId: string, association: Association, opts?: {
153
- expectedVersion?: number;
154
- snapshot?: RecordVersion;
155
- } & ActorOptions): Promise<StackRecord>;
156
225
  /** Folds the WAL back into the main file — useful before copying/backing up the database. */
157
226
  flush(): Promise<void>;
158
227
  close(): Promise<void>;
package/dist/index.js CHANGED
@@ -151,6 +151,12 @@ 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 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
+ };
154
160
  var SORT_FIELDS = NATIVE_SORT_FIELDS;
155
161
  var toBase64 = (text) => {
156
162
  const bytes = new TextEncoder().encode(text);
@@ -650,18 +656,26 @@ var isForeignKeyViolation = (err) => err instanceof Error && err.message.include
650
656
  var isUniqueConstraintViolation = (err) => err instanceof Error && err.message.includes("UNIQUE constraint failed");
651
657
 
652
658
  // ../sqlite-shared/dist/config.js
659
+ var CONFIG_ROW_SQL = `SELECT content FROM records WHERE id = '_config'`;
653
660
  var insertConfigRecord = (exec, entityId, timezone) => {
654
661
  const now = Date.now();
655
662
  exec.run(`INSERT INTO records (id, type_id, created_at, updated_at, content, version)
656
663
  VALUES ('_config', '_config@1', ?, ?, ?, 1)`, [now, now, JSON.stringify({ entityId, timezone })]);
664
+ return { entityId, timezone };
657
665
  };
658
- var readStackConfig = (exec) => {
659
- const row = exec.get(`SELECT content FROM records WHERE id = '_config'`);
666
+ var tryReadStackConfig = (exec) => {
667
+ const row = exec.get(CONFIG_ROW_SQL);
660
668
  if (!row)
661
- throw new Error("Stack database is missing its config record.");
669
+ return null;
662
670
  const content = JSON.parse(row.content);
663
671
  return { entityId: content.entityId, timezone: content.timezone };
664
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
+ };
665
679
  var asStackQueryError = (query, run) => {
666
680
  try {
667
681
  return run();
@@ -1258,6 +1272,97 @@ var SharedSqlRecordLogic = class {
1258
1272
  return this.getIndexedFields(record.typeId).get(field);
1259
1273
  }
1260
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
+ };
1261
1366
  var lockPathFor = (dbPath) => `${dbPath}.lock`;
1262
1367
  var isProcessAlive = (pid) => {
1263
1368
  try {
@@ -1415,33 +1520,26 @@ var NativeTokenStore = class _NativeTokenStore {
1415
1520
  };
1416
1521
 
1417
1522
  // src/index.ts
1418
- var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter {
1419
- constructor(path) {
1523
+ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter extends SharedSqlRecordAdapter {
1524
+ constructor(path, db, exec, config) {
1525
+ super(exec, config);
1420
1526
  this.path = path;
1527
+ this.db = db;
1421
1528
  }
1422
1529
  path;
1423
- capabilities = {
1424
- filter: {
1425
- content: "path",
1426
- contentPresent: true,
1427
- search: true
1428
- },
1429
- sort: {
1430
- fields: ["createdAt", "updatedAt", "version"],
1431
- contentField: true
1432
- },
1433
- limits: {
1434
- attachmentBytes: null,
1435
- contentBytes: null
1436
- }
1437
- };
1438
- ownerEntityId;
1439
- timezone;
1440
1530
  db;
1441
- record;
1442
- wire() {
1443
- const exec = new NativeSqliteExecutor(this.db);
1444
- this.record = new SharedSqlRecordLogic({ exec });
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];
1445
1543
  }
1446
1544
  /**
1447
1545
  * Initialize a new stack database. Fails if the file already exists —
@@ -1453,18 +1551,9 @@ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter {
1453
1551
  `Cannot initialize: database already exists at "${opts.path}". Use NativeSQLiteRecordAdapter.open() instead.`
1454
1552
  );
1455
1553
  }
1456
- acquireLock(opts.path, opts.force);
1457
- const adapter = new _NativeSQLiteRecordAdapter(opts.path);
1458
- adapter.db = new DatabaseSync(opts.path);
1459
- adapter.db.exec(PRAGMA_FOREIGN_KEYS_ON);
1460
- adapter.db.exec(PRAGMA_JOURNAL_MODE_WAL);
1461
- adapter.db.exec(RECORD_SCHEMA_SQL);
1462
- adapter.db.exec(FTS5_SCHEMA_SQL);
1463
- adapter.wire();
1464
- insertConfigRecord(new NativeSqliteExecutor(adapter.db), opts.entityId, opts.timezone);
1465
- adapter.ownerEntityId = opts.entityId;
1466
- adapter.timezone = opts.timezone;
1467
- 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);
1468
1557
  }
1469
1558
  /**
1470
1559
  * Open an existing stack database. Fails if the file does not exist —
@@ -1476,81 +1565,8 @@ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter {
1476
1565
  `Cannot open: no database found at "${opts.path}". Use NativeSQLiteRecordAdapter.initialize() to create one.`
1477
1566
  );
1478
1567
  }
1479
- acquireLock(opts.path, opts.force);
1480
- const adapter = new _NativeSQLiteRecordAdapter(opts.path);
1481
- adapter.db = new DatabaseSync(opts.path);
1482
- adapter.db.exec(PRAGMA_FOREIGN_KEYS_ON);
1483
- adapter.db.exec(PRAGMA_JOURNAL_MODE_WAL);
1484
- adapter.db.exec(RECORD_SCHEMA_SQL);
1485
- adapter.db.exec(FTS5_SCHEMA_SQL);
1486
- adapter.wire();
1487
- const config = readStackConfig(new NativeSqliteExecutor(adapter.db));
1488
- adapter.ownerEntityId = config.entityId;
1489
- adapter.timezone = config.timezone;
1490
- return adapter;
1491
- }
1492
- // -------------------------------------------------------
1493
- // Records
1494
- // -------------------------------------------------------
1495
- createRecord(record) {
1496
- return this.record.createRecord(record);
1497
- }
1498
- getRecord(id) {
1499
- return this.record.getRecord(id);
1500
- }
1501
- mutateRecord(id, changes, opts) {
1502
- return this.record.mutateRecord(id, changes, opts);
1503
- }
1504
- deleteRecord(id, opts) {
1505
- return this.record.deleteRecord(id, opts);
1506
- }
1507
- undeleteRecord(id, opts) {
1508
- return this.record.undeleteRecord(id, opts);
1509
- }
1510
- restoreVersion(id, version, opts) {
1511
- return this.record.restoreVersion(id, version, opts);
1512
- }
1513
- commitMigration(id, toTypeId, content, opts) {
1514
- return this.record.commitMigration(id, toTypeId, content, opts);
1515
- }
1516
- queryRecords(query) {
1517
- return this.record.queryRecords(query);
1518
- }
1519
- deleteUnreferencedAttachmentRecords(fileId, metadataTypeIds) {
1520
- return this.record.deleteUnreferencedAttachmentRecords(fileId, metadataTypeIds);
1521
- }
1522
- // -------------------------------------------------------
1523
- // Versions
1524
- // -------------------------------------------------------
1525
- getVersions(id) {
1526
- return this.record.getVersions(id);
1527
- }
1528
- getVersion(id, version) {
1529
- return this.record.getVersion(id, version);
1530
- }
1531
- saveVersion(id, version) {
1532
- return this.record.saveVersion(id, version);
1533
- }
1534
- // -------------------------------------------------------
1535
- // Types
1536
- // -------------------------------------------------------
1537
- saveType(type) {
1538
- return this.record.saveType(type);
1539
- }
1540
- getType(id) {
1541
- return this.record.getType(id);
1542
- }
1543
- listTypes() {
1544
- return this.record.listTypes();
1545
- }
1546
- // -------------------------------------------------------
1547
- // Associations
1548
- // -------------------------------------------------------
1549
- associate(recordId, association, opts) {
1550
- return this.record.associate(recordId, association, opts);
1551
- }
1552
- dissociate(recordId, association, opts) {
1553
- 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));
1554
1570
  }
1555
1571
  // -------------------------------------------------------
1556
1572
  // Lifecycle