local-lite 0.0.4 → 0.0.6
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/adapters/local/better-sqlite3.d.ts +4 -1
- package/dist/adapters/local/better-sqlite3.js +34 -14
- package/dist/adapters/local/expo-sqlite.d.ts +10 -2
- package/dist/adapters/local/expo-sqlite.js +36 -20
- package/dist/adapters/local/node-sqlite.d.ts +4 -1
- package/dist/adapters/local/node-sqlite.js +29 -12
- package/dist/adapters/remote/firestore.js +39 -32
- package/dist/adapters/remote/supabase.d.ts +7 -2
- package/dist/adapters/remote/supabase.js +192 -85
- package/dist/config/types.d.ts +1 -1
- package/dist/core/client.d.ts +52 -15
- package/dist/core/client.js +195 -208
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/outbox-store.d.ts +5 -0
- package/dist/core/outbox-store.js +29 -4
- package/dist/core/query-builder.d.ts +120 -0
- package/dist/core/query-builder.js +441 -0
- package/dist/core/query.js +28 -32
- package/dist/core/schema-manager.d.ts +4 -15
- package/dist/core/schema-manager.js +48 -103
- package/dist/core/sync-worker.js +7 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/react/SyncInspector.js +0 -70
- package/dist/react/index.d.ts +39 -0
- package/dist/react/index.js +232 -34
- package/dist/react/status.d.ts +5 -0
- package/dist/react/status.js +20 -12
- package/dist/schema/generator.d.ts +49 -0
- package/dist/schema/generator.js +209 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +17 -0
- package/dist/utils/uuid.d.ts +1 -1
- package/dist/utils/uuid.js +7 -16
- package/package.json +7 -2
package/dist/core/client.js
CHANGED
|
@@ -10,6 +10,44 @@ const schema_manager_js_1 = require("./schema-manager.js");
|
|
|
10
10
|
const uuid_js_1 = require("../utils/uuid.js");
|
|
11
11
|
const sql_js_1 = require("../utils/sql.js");
|
|
12
12
|
const query_js_1 = require("./query.js");
|
|
13
|
+
const query_builder_js_1 = require("./query-builder.js");
|
|
14
|
+
async function executeFindById(db, tableName, id, primaryKey, hydrateRow, includeDeleted) {
|
|
15
|
+
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
16
|
+
const safePk = (0, sql_js_1.escapeIdentifier)(primaryKey);
|
|
17
|
+
try {
|
|
18
|
+
const rows = await db.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} = ? LIMIT 1`, [id]);
|
|
19
|
+
if (!rows || rows.length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
const row = hydrateRow(tableName, rows[0]);
|
|
22
|
+
return !includeDeleted && (0, query_js_1.isDeletedRecord)(row) ? null : row;
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const msg = String(err?.message || err || '');
|
|
26
|
+
if (msg.includes('no such table') || msg.includes('no such column'))
|
|
27
|
+
return null;
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function executeFindByIds(db, tableName, ids, primaryKey, hydrateRow, includeDeleted) {
|
|
32
|
+
if (!ids || ids.length === 0)
|
|
33
|
+
return [];
|
|
34
|
+
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
35
|
+
const safePk = (0, sql_js_1.escapeIdentifier)(primaryKey);
|
|
36
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
37
|
+
try {
|
|
38
|
+
const rows = await db.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} IN (${placeholders})`, ids);
|
|
39
|
+
if (!rows || rows.length === 0)
|
|
40
|
+
return [];
|
|
41
|
+
const hydrated = rows.map((r) => hydrateRow(tableName, r));
|
|
42
|
+
return !includeDeleted ? hydrated.filter((r) => !(0, query_js_1.isDeletedRecord)(r)) : hydrated;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const msg = String(err?.message || err || '');
|
|
46
|
+
if (msg.includes('no such table') || msg.includes('no such column'))
|
|
47
|
+
return [];
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
13
51
|
class TransactionalClient {
|
|
14
52
|
parent;
|
|
15
53
|
tx;
|
|
@@ -73,44 +111,20 @@ class TransactionalClient {
|
|
|
73
111
|
await this.mutate(tableName, rowId, 'DELETE', null, options);
|
|
74
112
|
}
|
|
75
113
|
async findById(tableName, id, options) {
|
|
76
|
-
|
|
77
|
-
const safePk = (0, sql_js_1.escapeIdentifier)(this.parent.getPrimaryKey());
|
|
78
|
-
try {
|
|
79
|
-
const rows = await this.tx.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} = ? LIMIT 1`, [id]);
|
|
80
|
-
if (!rows || rows.length === 0)
|
|
81
|
-
return null;
|
|
82
|
-
const row = this.parent.hydrateRow(tableName, rows[0]);
|
|
83
|
-
if (!options?.includeDeleted && (0, query_js_1.isDeletedRecord)(row)) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
return row;
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
114
|
+
return executeFindById(this.tx, tableName, id, this.parent.getPrimaryKey(), (t, r) => this.parent.hydrateRow(t, r), options?.includeDeleted);
|
|
91
115
|
}
|
|
92
116
|
async findByIds(tableName, ids, options) {
|
|
93
|
-
|
|
94
|
-
return [];
|
|
95
|
-
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
96
|
-
const safePk = (0, sql_js_1.escapeIdentifier)(this.parent.getPrimaryKey());
|
|
97
|
-
const placeholders = ids.map(() => '?').join(', ');
|
|
98
|
-
try {
|
|
99
|
-
const rows = await this.tx.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} IN (${placeholders})`, ids);
|
|
100
|
-
if (!rows || rows.length === 0)
|
|
101
|
-
return [];
|
|
102
|
-
const hydrated = rows.map((r) => this.parent.hydrateRow(tableName, r));
|
|
103
|
-
if (!options?.includeDeleted) {
|
|
104
|
-
return hydrated.filter((r) => !(0, query_js_1.isDeletedRecord)(r));
|
|
105
|
-
}
|
|
106
|
-
return hydrated;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return [];
|
|
110
|
-
}
|
|
117
|
+
return executeFindByIds(this.tx, tableName, ids, this.parent.getPrimaryKey(), (t, r) => this.parent.hydrateRow(t, r), options?.includeDeleted);
|
|
111
118
|
}
|
|
112
119
|
async findMany(tableName, options) {
|
|
113
|
-
|
|
120
|
+
const rows = await this.from(tableName).applyOptions(options).execute();
|
|
121
|
+
return (0, query_js_1.applyQueryOptions)(rows, options);
|
|
122
|
+
}
|
|
123
|
+
async query(sql, params = []) {
|
|
124
|
+
return (await this.tx.execute(sql, params));
|
|
125
|
+
}
|
|
126
|
+
from(tableName) {
|
|
127
|
+
return new query_builder_js_1.QueryBuilder(this, tableName);
|
|
114
128
|
}
|
|
115
129
|
get rawTx() {
|
|
116
130
|
return this.tx;
|
|
@@ -266,6 +280,15 @@ class LocalFirstClient {
|
|
|
266
280
|
await this.initialization;
|
|
267
281
|
}
|
|
268
282
|
async initializeStorage() {
|
|
283
|
+
try {
|
|
284
|
+
await this.db.execute('PRAGMA journal_mode = WAL;');
|
|
285
|
+
await this.db.execute('PRAGMA synchronous = NORMAL;');
|
|
286
|
+
await this.db.execute('PRAGMA busy_timeout = 5000;');
|
|
287
|
+
await this.db.execute('PRAGMA cache_size = -64000;');
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Ignored for drivers, mock DBs, or in-memory modes that do not support PRAGMAs
|
|
291
|
+
}
|
|
269
292
|
await outbox_store_js_1.OutboxStore.ensureSchema(this.db);
|
|
270
293
|
await meta_store_js_1.MetaStore.ensureSchema(this.db);
|
|
271
294
|
await outbox_store_js_1.OutboxStore.resetStaleProcessing(this.db);
|
|
@@ -336,6 +359,7 @@ class LocalFirstClient {
|
|
|
336
359
|
const generatedIds = [];
|
|
337
360
|
const queuedEvents = [];
|
|
338
361
|
const queuedOutboxIds = [];
|
|
362
|
+
const pendingOutboxOps = [];
|
|
339
363
|
const executeInTx = async (tx) => {
|
|
340
364
|
for (const m of mutations) {
|
|
341
365
|
const pk = m.primaryKey ?? options.primaryKey ?? this.primaryKey;
|
|
@@ -352,8 +376,22 @@ class LocalFirstClient {
|
|
|
352
376
|
const skipOutbox = m.skipOutbox ?? options.skipOutbox ?? false;
|
|
353
377
|
if (!skipOutbox) {
|
|
354
378
|
const timestamp = m.customTimestamp ?? options.customTimestamp ?? Date.now();
|
|
355
|
-
|
|
356
|
-
|
|
379
|
+
if (this.coalesceOutbox && m.operation !== 'INSERT') {
|
|
380
|
+
const outboxId = await this.recordOutboxMutation(tx, m.tableName, rowId, m.operation, payload, timestamp);
|
|
381
|
+
if (outboxId) {
|
|
382
|
+
queuedOutboxIds.push(outboxId);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const outboxId = (0, uuid_js_1.generateUUID)();
|
|
387
|
+
pendingOutboxOps.push({
|
|
388
|
+
id: outboxId,
|
|
389
|
+
tableName: m.tableName,
|
|
390
|
+
rowId,
|
|
391
|
+
operation: m.operation,
|
|
392
|
+
payload,
|
|
393
|
+
timestamp,
|
|
394
|
+
});
|
|
357
395
|
queuedOutboxIds.push(outboxId);
|
|
358
396
|
}
|
|
359
397
|
}
|
|
@@ -367,6 +405,9 @@ class LocalFirstClient {
|
|
|
367
405
|
},
|
|
368
406
|
});
|
|
369
407
|
}
|
|
408
|
+
if (pendingOutboxOps.length > 0) {
|
|
409
|
+
await outbox_store_js_1.OutboxStore.insertRecords(tx, pendingOutboxOps);
|
|
410
|
+
}
|
|
370
411
|
};
|
|
371
412
|
if (options.tx) {
|
|
372
413
|
await executeInTx(options.tx);
|
|
@@ -492,142 +533,137 @@ class LocalFirstClient {
|
|
|
492
533
|
* Pushes simple equality where clauses down to SQLite for high performance, with memory fallback.
|
|
493
534
|
* Gracefully returns [] if table or columns have not been populated yet on empty schemas.
|
|
494
535
|
*
|
|
536
|
+
/**
|
|
537
|
+
* Executes a raw relational SQL query with parameters directly against SQLite.
|
|
538
|
+
* Enables complex multi-table JOINs, GROUP BY aggregations (COUNT, SUM, AVG), CTEs, and window functions.
|
|
539
|
+
*/
|
|
540
|
+
async query(sql, params = []) {
|
|
541
|
+
return (await this.db.execute(sql, params));
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Returns a fluent SQL QueryBuilder for the specified table.
|
|
545
|
+
* Allows writing clean JavaScript method chains (select, join, leftJoin, where, orderBy, groupBy)
|
|
546
|
+
* that automatically compile into optimized native SQLite queries.
|
|
547
|
+
*
|
|
495
548
|
* Example:
|
|
496
|
-
* const
|
|
497
|
-
*
|
|
549
|
+
* const activeRentals = await client
|
|
550
|
+
* .from('contracts')
|
|
551
|
+
* .join('houses', 'contracts.house_id', 'houses.id')
|
|
552
|
+
* .leftJoin('tenants', 'contracts.tenant_id', 'tenants.id')
|
|
553
|
+
* .where({ 'contracts.business_id': bizId, 'contracts.status': 'ACTIVE' })
|
|
554
|
+
* .select('contracts.id', 'contracts.rent_amount', 'houses.house_number', 'tenants.name')
|
|
555
|
+
* .execute();
|
|
498
556
|
*/
|
|
499
|
-
|
|
500
|
-
return
|
|
557
|
+
from(tableName) {
|
|
558
|
+
return new query_builder_js_1.QueryBuilder(this, tableName);
|
|
501
559
|
}
|
|
502
560
|
/**
|
|
503
|
-
*
|
|
561
|
+
* Executes a native SQLite COUNT(*) query and returns the count directly.
|
|
504
562
|
*/
|
|
505
|
-
async
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
whereClauses.push(`${safeCol} >= ?`);
|
|
554
|
-
params.push(ops.$gte);
|
|
555
|
-
}
|
|
556
|
-
if (ops.$lt !== undefined) {
|
|
557
|
-
whereClauses.push(`${safeCol} < ?`);
|
|
558
|
-
params.push(ops.$lt);
|
|
559
|
-
}
|
|
560
|
-
if (ops.$lte !== undefined) {
|
|
561
|
-
whereClauses.push(`${safeCol} <= ?`);
|
|
562
|
-
params.push(ops.$lte);
|
|
563
|
-
}
|
|
564
|
-
if (Array.isArray(ops.$in) && ops.$in.length > 0) {
|
|
565
|
-
const placeholders = ops.$in.map(() => '?').join(', ');
|
|
566
|
-
whereClauses.push(`${safeCol} IN (${placeholders})`);
|
|
567
|
-
params.push(...ops.$in.map((v) => (typeof v === 'boolean' ? (v ? 1 : 0) : v)));
|
|
568
|
-
}
|
|
569
|
-
if (Array.isArray(ops.$nin) && ops.$nin.length > 0) {
|
|
570
|
-
const placeholders = ops.$nin.map(() => '?').join(', ');
|
|
571
|
-
whereClauses.push(`${safeCol} NOT IN (${placeholders})`);
|
|
572
|
-
params.push(...ops.$nin.map((v) => (typeof v === 'boolean' ? (v ? 1 : 0) : v)));
|
|
573
|
-
}
|
|
574
|
-
if (ops.$null !== undefined) {
|
|
575
|
-
whereClauses.push(ops.$null ? `${safeCol} IS NULL` : `${safeCol} IS NOT NULL`);
|
|
576
|
-
}
|
|
577
|
-
if (ops.$contains !== undefined) {
|
|
578
|
-
whereClauses.push(`${safeCol} LIKE ?`);
|
|
579
|
-
params.push(`%${ops.$contains}%`);
|
|
580
|
-
}
|
|
581
|
-
if (ops.$startsWith !== undefined) {
|
|
582
|
-
whereClauses.push(`${safeCol} LIKE ?`);
|
|
583
|
-
params.push(`${ops.$startsWith}%`);
|
|
584
|
-
}
|
|
585
|
-
if (ops.$endsWith !== undefined) {
|
|
586
|
-
whereClauses.push(`${safeCol} LIKE ?`);
|
|
587
|
-
params.push(`%${ops.$endsWith}`);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
563
|
+
async count(tableName, options) {
|
|
564
|
+
const qb = this.from(tableName);
|
|
565
|
+
if (options?.where)
|
|
566
|
+
qb.where(options.where);
|
|
567
|
+
return qb.count();
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Executes a native SQLite SUM(column) query and returns the sum directly.
|
|
571
|
+
*/
|
|
572
|
+
async sum(tableName, column, options) {
|
|
573
|
+
const qb = this.from(tableName);
|
|
574
|
+
if (options?.where)
|
|
575
|
+
qb.where(options.where);
|
|
576
|
+
return qb.sum(column);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Executes a native SQLite AVG(column) query and returns the average directly.
|
|
580
|
+
*/
|
|
581
|
+
async avg(tableName, column, options) {
|
|
582
|
+
const qb = this.from(tableName);
|
|
583
|
+
if (options?.where)
|
|
584
|
+
qb.where(options.where);
|
|
585
|
+
return qb.avg(column);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Executes native multi-metric SQLite aggregation in a single query.
|
|
589
|
+
*
|
|
590
|
+
* Example:
|
|
591
|
+
* const stats = await client.aggregate('receipts', {
|
|
592
|
+
* where: { business_id: bizId },
|
|
593
|
+
* count: true,
|
|
594
|
+
* sum: ['amount', 'refunded_amount'],
|
|
595
|
+
* avg: ['amount']
|
|
596
|
+
* });
|
|
597
|
+
*/
|
|
598
|
+
async aggregate(tableName, spec) {
|
|
599
|
+
const projections = [];
|
|
600
|
+
if (spec.count) {
|
|
601
|
+
projections.push('COUNT(*) AS "_count"');
|
|
602
|
+
}
|
|
603
|
+
if (spec.sum) {
|
|
604
|
+
for (const col of spec.sum) {
|
|
605
|
+
projections.push(`COALESCE(SUM("${col.replace(/"/g, '""')}"), 0) AS "sum_${col}"`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (spec.avg) {
|
|
609
|
+
for (const col of spec.avg) {
|
|
610
|
+
projections.push(`AVG("${col.replace(/"/g, '""')}") AS "avg_${col}"`);
|
|
590
611
|
}
|
|
591
|
-
|
|
592
|
-
|
|
612
|
+
}
|
|
613
|
+
if (spec.min) {
|
|
614
|
+
for (const col of spec.min) {
|
|
615
|
+
projections.push(`MIN("${col.replace(/"/g, '""')}") AS "min_${col}"`);
|
|
593
616
|
}
|
|
594
617
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const dir = options.order && String(options.order).toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
599
|
-
sql += ` ORDER BY ${safeCol} ${dir}`;
|
|
600
|
-
}
|
|
601
|
-
// Push down LIMIT & OFFSET
|
|
602
|
-
if (typeof options?.limit === 'number' && options.limit > 0) {
|
|
603
|
-
sql += ` LIMIT ?`;
|
|
604
|
-
params.push(options.limit);
|
|
605
|
-
if (typeof options.offset === 'number' && options.offset > 0) {
|
|
606
|
-
sql += ` OFFSET ?`;
|
|
607
|
-
params.push(options.offset);
|
|
618
|
+
if (spec.max) {
|
|
619
|
+
for (const col of spec.max) {
|
|
620
|
+
projections.push(`MAX("${col.replace(/"/g, '""')}") AS "max_${col}"`);
|
|
608
621
|
}
|
|
609
622
|
}
|
|
610
|
-
|
|
611
|
-
|
|
623
|
+
if (projections.length === 0) {
|
|
624
|
+
return {};
|
|
612
625
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
626
|
+
const qb = this.from(tableName).select(...projections);
|
|
627
|
+
if (spec.where) {
|
|
628
|
+
qb.where(spec.where);
|
|
629
|
+
}
|
|
630
|
+
const rows = await qb.execute();
|
|
631
|
+
const row = rows[0] || {};
|
|
632
|
+
const result = {};
|
|
633
|
+
if (spec.count) {
|
|
634
|
+
result.count = typeof row._count === 'number' ? row._count : Number(row._count) || 0;
|
|
635
|
+
}
|
|
636
|
+
if (spec.sum) {
|
|
637
|
+
result.sum = {};
|
|
638
|
+
for (const col of spec.sum) {
|
|
639
|
+
const val = row[`sum_${col}`];
|
|
640
|
+
result.sum[col] = typeof val === 'number' ? val : Number(val) || 0;
|
|
617
641
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
642
|
+
}
|
|
643
|
+
if (spec.avg) {
|
|
644
|
+
result.avg = {};
|
|
645
|
+
for (const col of spec.avg) {
|
|
646
|
+
const val = row[`avg_${col}`];
|
|
647
|
+
result.avg[col] = val === null || val === undefined ? null : Number(val) || 0;
|
|
621
648
|
}
|
|
622
|
-
|
|
623
|
-
|
|
649
|
+
}
|
|
650
|
+
if (spec.min) {
|
|
651
|
+
result.min = {};
|
|
652
|
+
for (const col of spec.min) {
|
|
653
|
+
result.min[col] = row[`min_${col}`];
|
|
624
654
|
}
|
|
625
655
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
656
|
+
if (spec.max) {
|
|
657
|
+
result.max = {};
|
|
658
|
+
for (const col of spec.max) {
|
|
659
|
+
result.max[col] = row[`max_${col}`];
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
async findMany(tableName, options) {
|
|
665
|
+
const rows = await this.from(tableName).applyOptions(options).execute();
|
|
666
|
+
return (0, query_js_1.applyQueryOptions)(rows, options);
|
|
631
667
|
}
|
|
632
668
|
/**
|
|
633
669
|
* Finds a single record by primary key.
|
|
@@ -636,59 +672,10 @@ class LocalFirstClient {
|
|
|
636
672
|
* const tenant = await client.findById('tenants', 'ten-1');
|
|
637
673
|
*/
|
|
638
674
|
async findById(tableName, id, options) {
|
|
639
|
-
|
|
640
|
-
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
641
|
-
const safePk = (0, sql_js_1.escapeIdentifier)(this.primaryKey);
|
|
642
|
-
try {
|
|
643
|
-
const rows = await this.db.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} = ? LIMIT 1`, [id]);
|
|
644
|
-
if (!rows || rows.length === 0)
|
|
645
|
-
return null;
|
|
646
|
-
const row = this.hydrateRow(tableName, rows[0]);
|
|
647
|
-
if (!options?.includeDeleted && (0, query_js_1.isDeletedRecord)(row)) {
|
|
648
|
-
return null;
|
|
649
|
-
}
|
|
650
|
-
return row;
|
|
651
|
-
}
|
|
652
|
-
catch (err) {
|
|
653
|
-
const msg = String(err?.message || err || '');
|
|
654
|
-
if (msg.includes('no such table') || msg.includes('no such column')) {
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
throw err;
|
|
658
|
-
}
|
|
675
|
+
return executeFindById(this.db, tableName, id, this.primaryKey, (t, r) => this.hydrateRow(t, r), options?.includeDeleted);
|
|
659
676
|
}
|
|
660
|
-
/**
|
|
661
|
-
* Finds multiple records by primary keys in a single batched SQL query.
|
|
662
|
-
* Eliminates the N-roundtrip bridge overhead of multiple findById calls.
|
|
663
|
-
* Automatically omits soft-deleted records unless includeDeleted: true.
|
|
664
|
-
*
|
|
665
|
-
* Example:
|
|
666
|
-
* const tenants = await client.findByIds('tenants', ['ten-1', 'ten-2', 'ten-3']);
|
|
667
|
-
*/
|
|
668
677
|
async findByIds(tableName, ids, options) {
|
|
669
|
-
|
|
670
|
-
return [];
|
|
671
|
-
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
672
|
-
const safePk = (0, sql_js_1.escapeIdentifier)(this.primaryKey);
|
|
673
|
-
const placeholders = ids.map(() => '?').join(', ');
|
|
674
|
-
try {
|
|
675
|
-
const rows = await this.db.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} IN (${placeholders})`, ids);
|
|
676
|
-
if (!rows || rows.length === 0)
|
|
677
|
-
return [];
|
|
678
|
-
const hasJsonConfig = Boolean(this.jsonColumns[tableName] || this.autoHydrateJSON);
|
|
679
|
-
const hydrated = hasJsonConfig ? rows.map((r) => this.hydrateRow(tableName, r)) : rows;
|
|
680
|
-
if (!options?.includeDeleted) {
|
|
681
|
-
return hydrated.filter((r) => !(0, query_js_1.isDeletedRecord)(r));
|
|
682
|
-
}
|
|
683
|
-
return hydrated;
|
|
684
|
-
}
|
|
685
|
-
catch (err) {
|
|
686
|
-
const msg = String(err?.message || err || '');
|
|
687
|
-
if (msg.includes('no such table') || msg.includes('no such column')) {
|
|
688
|
-
return [];
|
|
689
|
-
}
|
|
690
|
-
throw err;
|
|
691
|
-
}
|
|
678
|
+
return executeFindByIds(this.db, tableName, ids, this.primaryKey, (t, r) => this.hydrateRow(t, r), options?.includeDeleted);
|
|
692
679
|
}
|
|
693
680
|
/**
|
|
694
681
|
* Tracks an existing mutation into sync_outbox without modifying the table.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './outbox-store.js';
|
|
|
7
7
|
export * from './meta-store.js';
|
|
8
8
|
export * from './table-manager.js';
|
|
9
9
|
export * from './schema-manager.js';
|
|
10
|
+
export * from './query-builder.js';
|
|
10
11
|
export * from '../reactive/event-emitter.js';
|
|
11
12
|
export * from '../adapters/network/index.js';
|
|
12
13
|
export type { LocalDBAdapter, LocalTransaction } from '../adapters/local/index.js';
|
package/dist/core/index.js
CHANGED
|
@@ -24,6 +24,7 @@ __exportStar(require("./outbox-store.js"), exports);
|
|
|
24
24
|
__exportStar(require("./meta-store.js"), exports);
|
|
25
25
|
__exportStar(require("./table-manager.js"), exports);
|
|
26
26
|
__exportStar(require("./schema-manager.js"), exports);
|
|
27
|
+
__exportStar(require("./query-builder.js"), exports);
|
|
27
28
|
__exportStar(require("../reactive/event-emitter.js"), exports);
|
|
28
29
|
__exportStar(require("../adapters/network/index.js"), exports);
|
|
29
30
|
__exportStar(require("../utils/logger.js"), exports);
|
|
@@ -21,6 +21,11 @@ export declare class OutboxStore {
|
|
|
21
21
|
* Finds the latest pending outbox record for a given table and row.
|
|
22
22
|
*/
|
|
23
23
|
static findPendingByRow(tx: LocalTransaction, tableName: string, rowId: string): Promise<OutboxRecord | undefined>;
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves active (non-FAILED) pending outbox records indexed by `${table_name}:${row_id}`.
|
|
26
|
+
* Enables O(1) in-memory conflict lookup during delta ingestion, eliminating N+1 queries.
|
|
27
|
+
*/
|
|
28
|
+
static getActivePendingMap(tx: LocalTransaction): Promise<Map<string, OutboxRecord>>;
|
|
24
29
|
/**
|
|
25
30
|
* Marks a batch of outbox IDs as 'PROCESSING'.
|
|
26
31
|
*/
|
|
@@ -36,6 +36,7 @@ class OutboxStore {
|
|
|
36
36
|
// Indexes for the hot paths: pending scans (push), per-row conflict lookups (pull),
|
|
37
37
|
// and status counts (telemetry). Executed separately: many drivers prepare one statement.
|
|
38
38
|
await db.execute(`CREATE INDEX IF NOT EXISTS idx_sync_outbox_status_ts ON sync_outbox (status, timestamp);`);
|
|
39
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_sync_outbox_status_retry ON sync_outbox (status, retry_count, timestamp);`);
|
|
39
40
|
await db.execute(`CREATE INDEX IF NOT EXISTS idx_sync_outbox_row ON sync_outbox (table_name, row_id, timestamp);`);
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
@@ -107,6 +108,30 @@ class OutboxStore {
|
|
|
107
108
|
return undefined;
|
|
108
109
|
return mapOutboxRow(rows[0]);
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Retrieves active (non-FAILED) pending outbox records indexed by `${table_name}:${row_id}`.
|
|
113
|
+
* Enables O(1) in-memory conflict lookup during delta ingestion, eliminating N+1 queries.
|
|
114
|
+
*/
|
|
115
|
+
static async getActivePendingMap(tx) {
|
|
116
|
+
const map = new Map();
|
|
117
|
+
try {
|
|
118
|
+
const rows = await tx.execute(`SELECT * FROM sync_outbox WHERE status != 'FAILED' ORDER BY timestamp DESC`);
|
|
119
|
+
if (!rows || rows.length === 0)
|
|
120
|
+
return map;
|
|
121
|
+
for (const r of rows) {
|
|
122
|
+
const record = mapOutboxRow(r);
|
|
123
|
+
const key = `${record.tableName}:${record.rowId}`;
|
|
124
|
+
// Preserve the latest record for each table_name:row_id
|
|
125
|
+
if (!map.has(key)) {
|
|
126
|
+
map.set(key, record);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return map;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return map;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
110
135
|
/**
|
|
111
136
|
* Marks a batch of outbox IDs as 'PROCESSING'.
|
|
112
137
|
*/
|
|
@@ -216,8 +241,8 @@ class OutboxStore {
|
|
|
216
241
|
return ids.length;
|
|
217
242
|
}
|
|
218
243
|
else {
|
|
219
|
-
const
|
|
220
|
-
const count =
|
|
244
|
+
const rows = await db.execute(`SELECT COUNT(*) AS count FROM sync_outbox WHERE status = 'FAILED'`);
|
|
245
|
+
const count = Number(rows?.[0]?.count ?? (Array.isArray(rows) ? rows.length : 0));
|
|
221
246
|
if (count > 0) {
|
|
222
247
|
await db.execute(`UPDATE sync_outbox SET status = 'PENDING', retry_count = 0, last_error = NULL WHERE status = 'FAILED'`);
|
|
223
248
|
}
|
|
@@ -239,8 +264,8 @@ class OutboxStore {
|
|
|
239
264
|
return ids.length;
|
|
240
265
|
}
|
|
241
266
|
else {
|
|
242
|
-
const
|
|
243
|
-
const count =
|
|
267
|
+
const rows = await db.execute(`SELECT COUNT(*) AS count FROM sync_outbox WHERE status = 'FAILED'`);
|
|
268
|
+
const count = Number(rows?.[0]?.count ?? (Array.isArray(rows) ? rows.length : 0));
|
|
244
269
|
if (count > 0) {
|
|
245
270
|
await db.execute(`DELETE FROM sync_outbox WHERE status = 'FAILED'`);
|
|
246
271
|
}
|