local-lite 0.0.3 → 0.0.5
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/README.md +421 -362
- package/dist/adapters/local/better-sqlite3.d.ts +2 -0
- package/dist/adapters/local/better-sqlite3.js +46 -25
- package/dist/adapters/local/expo-sqlite.d.ts +5 -1
- package/dist/adapters/local/expo-sqlite.js +40 -7
- package/dist/adapters/local/index.d.ts +1 -0
- package/dist/adapters/local/node-sqlite.d.ts +2 -0
- package/dist/adapters/local/node-sqlite.js +45 -22
- package/dist/adapters/local/op-sqlite.d.ts +1 -0
- package/dist/adapters/local/op-sqlite.js +6 -0
- package/dist/adapters/local/sql-js.d.ts +1 -0
- package/dist/adapters/local/sql-js.js +5 -0
- package/dist/adapters/network/index.js +33 -0
- package/dist/adapters/remote/convex.d.ts +2 -2
- package/dist/adapters/remote/convex.js +8 -12
- package/dist/adapters/remote/demo.d.ts +1 -1
- package/dist/adapters/remote/demo.js +3 -0
- package/dist/adapters/remote/firestore.d.ts +3 -2
- package/dist/adapters/remote/firestore.js +69 -46
- package/dist/adapters/remote/index.d.ts +6 -4
- package/dist/adapters/remote/supabase.d.ts +34 -6
- package/dist/adapters/remote/supabase.js +340 -79
- package/dist/config/index.js +7 -10
- package/dist/config/resolve-local.js +12 -1
- package/dist/config/resolve-remote.js +33 -28
- package/dist/config/types.d.ts +30 -5
- package/dist/core/client.d.ts +133 -5
- package/dist/core/client.js +431 -46
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.js +30 -0
- package/dist/core/outbox-store.d.ts +8 -4
- package/dist/core/outbox-store.js +84 -65
- package/dist/core/query-builder.d.ts +119 -0
- package/dist/core/query-builder.js +372 -0
- package/dist/core/query.d.ts +8 -0
- package/dist/core/query.js +37 -0
- package/dist/core/schema-manager.d.ts +9 -0
- package/dist/core/schema-manager.js +103 -0
- package/dist/core/sync-worker.d.ts +38 -1
- package/dist/core/sync-worker.js +351 -104
- package/dist/core/table-manager.js +26 -29
- package/dist/core/types.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/react/BenchmarkModal.d.ts +32 -0
- package/dist/react/BenchmarkModal.js +777 -0
- package/dist/react/SyncInspector.d.ts +31 -0
- package/dist/react/SyncInspector.js +585 -0
- package/dist/react/index.d.ts +89 -18
- package/dist/react/index.js +450 -105
- package/dist/react/runner.d.ts +7 -0
- package/dist/react/runner.js +42 -0
- package/dist/react/status.d.ts +24 -0
- package/dist/react/status.js +123 -0
- package/dist/react/theme.d.ts +18 -0
- package/dist/react/theme.js +46 -0
- 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/logger.d.ts +4 -0
- package/dist/utils/logger.js +20 -0
- package/package.json +143 -133
package/dist/core/client.js
CHANGED
|
@@ -10,6 +10,7 @@ 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");
|
|
13
14
|
class TransactionalClient {
|
|
14
15
|
parent;
|
|
15
16
|
tx;
|
|
@@ -23,22 +24,16 @@ class TransactionalClient {
|
|
|
23
24
|
}
|
|
24
25
|
async mutate(tableName, rowId, operation, payload, options = {}) {
|
|
25
26
|
const pk = options.primaryKey ?? this.parent.getPrimaryKey();
|
|
26
|
-
const outboxId = (0, uuid_js_1.generateUUID)();
|
|
27
27
|
const timestamp = options.customTimestamp ?? Date.now();
|
|
28
28
|
await table_manager_js_1.TableManager.applyMutation(this.tx, tableName, rowId, operation, payload, pk, {
|
|
29
29
|
autoSchema: this.parent.autoSchema,
|
|
30
30
|
schemaManager: this.parent.schemaManager,
|
|
31
31
|
});
|
|
32
32
|
if (!options.skipOutbox) {
|
|
33
|
-
await
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
operation,
|
|
38
|
-
payload,
|
|
39
|
-
timestamp,
|
|
40
|
-
});
|
|
41
|
-
this.queuedOutboxIds.push(outboxId);
|
|
33
|
+
const outboxId = await this.parent.recordOutboxMutation(this.tx, tableName, rowId, operation, payload, timestamp);
|
|
34
|
+
if (outboxId) {
|
|
35
|
+
this.queuedOutboxIds.push(outboxId);
|
|
36
|
+
}
|
|
42
37
|
}
|
|
43
38
|
this.queuedTableEvents.push({
|
|
44
39
|
tableName,
|
|
@@ -70,9 +65,61 @@ class TransactionalClient {
|
|
|
70
65
|
async update(tableName, rowId, payload, options = {}) {
|
|
71
66
|
await this.mutate(tableName, rowId, 'UPDATE', payload, options);
|
|
72
67
|
}
|
|
68
|
+
async updateMany(tableName, updates, options = {}) {
|
|
69
|
+
for (const u of updates) {
|
|
70
|
+
await this.update(tableName, u.id, u.payload, options);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
73
|
async delete(tableName, rowId, options = {}) {
|
|
74
74
|
await this.mutate(tableName, rowId, 'DELETE', null, options);
|
|
75
75
|
}
|
|
76
|
+
async findById(tableName, id, options) {
|
|
77
|
+
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
78
|
+
const safePk = (0, sql_js_1.escapeIdentifier)(this.parent.getPrimaryKey());
|
|
79
|
+
try {
|
|
80
|
+
const rows = await this.tx.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} = ? LIMIT 1`, [id]);
|
|
81
|
+
if (!rows || rows.length === 0)
|
|
82
|
+
return null;
|
|
83
|
+
const row = this.parent.hydrateRow(tableName, rows[0]);
|
|
84
|
+
if (!options?.includeDeleted && (0, query_js_1.isDeletedRecord)(row)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return row;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async findByIds(tableName, ids, options) {
|
|
94
|
+
if (!ids || ids.length === 0)
|
|
95
|
+
return [];
|
|
96
|
+
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
97
|
+
const safePk = (0, sql_js_1.escapeIdentifier)(this.parent.getPrimaryKey());
|
|
98
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
99
|
+
try {
|
|
100
|
+
const rows = await this.tx.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} IN (${placeholders})`, ids);
|
|
101
|
+
if (!rows || rows.length === 0)
|
|
102
|
+
return [];
|
|
103
|
+
const hydrated = rows.map((r) => this.parent.hydrateRow(tableName, r));
|
|
104
|
+
if (!options?.includeDeleted) {
|
|
105
|
+
return hydrated.filter((r) => !(0, query_js_1.isDeletedRecord)(r));
|
|
106
|
+
}
|
|
107
|
+
return hydrated;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async findMany(tableName, options) {
|
|
114
|
+
const rows = await this.from(tableName).applyOptions(options).execute();
|
|
115
|
+
return (0, query_js_1.applyQueryOptions)(rows, options);
|
|
116
|
+
}
|
|
117
|
+
async query(sql, params = []) {
|
|
118
|
+
return (await this.tx.execute(sql, params));
|
|
119
|
+
}
|
|
120
|
+
from(tableName) {
|
|
121
|
+
return new query_builder_js_1.QueryBuilder(this, tableName);
|
|
122
|
+
}
|
|
76
123
|
get rawTx() {
|
|
77
124
|
return this.tx;
|
|
78
125
|
}
|
|
@@ -85,15 +132,26 @@ class LocalFirstClient {
|
|
|
85
132
|
syncWorker;
|
|
86
133
|
schemaManager;
|
|
87
134
|
autoSchema;
|
|
135
|
+
coalesceOutbox;
|
|
88
136
|
tables;
|
|
137
|
+
jsonColumns;
|
|
138
|
+
autoHydrateJSON;
|
|
89
139
|
primaryKey;
|
|
140
|
+
migrations;
|
|
141
|
+
autoStart;
|
|
142
|
+
initialization;
|
|
90
143
|
constructor(config) {
|
|
91
144
|
this.db = config.local;
|
|
92
145
|
this.remote = config.remote;
|
|
93
146
|
this.events = new event_emitter_js_1.EventEmitter();
|
|
94
147
|
this.primaryKey = config.primaryKey ?? 'id';
|
|
95
148
|
this.autoSchema = config.autoSchema ?? false;
|
|
149
|
+
this.coalesceOutbox = config.coalesceOutbox ?? true;
|
|
96
150
|
this.tables = config.tables ?? [];
|
|
151
|
+
this.migrations = config.migrations ?? [];
|
|
152
|
+
this.autoStart = config.autoStart ?? true;
|
|
153
|
+
this.jsonColumns = config.jsonColumns ?? {};
|
|
154
|
+
this.autoHydrateJSON = config.autoHydrateJSON ?? false;
|
|
97
155
|
this.schemaManager = new schema_manager_js_1.SchemaManager();
|
|
98
156
|
const workerOptions = {
|
|
99
157
|
network: config.network,
|
|
@@ -107,26 +165,133 @@ class LocalFirstClient {
|
|
|
107
165
|
logLevel: config.logLevel,
|
|
108
166
|
autoSchema: this.autoSchema,
|
|
109
167
|
schemaManager: this.schemaManager,
|
|
168
|
+
autoSync: this.autoStart,
|
|
169
|
+
syncScope: config.syncScope,
|
|
170
|
+
autoPauseOnBackground: config.autoPauseOnBackground,
|
|
110
171
|
};
|
|
111
172
|
this.syncWorker = new sync_worker_js_1.SyncWorker(this.db, this.remote, this.events, workerOptions);
|
|
112
173
|
}
|
|
113
174
|
getPrimaryKey() {
|
|
114
175
|
return this.primaryKey;
|
|
115
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Automatically deserializes JSON string columns into JavaScript objects/arrays.
|
|
179
|
+
*/
|
|
180
|
+
hydrateRow(tableName, row) {
|
|
181
|
+
if (!row || typeof row !== 'object')
|
|
182
|
+
return row;
|
|
183
|
+
const cols = this.jsonColumns[tableName];
|
|
184
|
+
if (cols && cols.length > 0) {
|
|
185
|
+
const copy = { ...row };
|
|
186
|
+
for (const col of cols) {
|
|
187
|
+
if (typeof copy[col] === 'string') {
|
|
188
|
+
const val = copy[col].trim();
|
|
189
|
+
if ((val.startsWith('{') && val.endsWith('}')) || (val.startsWith('[') && val.endsWith(']'))) {
|
|
190
|
+
try {
|
|
191
|
+
copy[col] = JSON.parse(val);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// keep string if not valid JSON
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return copy;
|
|
200
|
+
}
|
|
201
|
+
if (this.autoHydrateJSON) {
|
|
202
|
+
const copy = { ...row };
|
|
203
|
+
for (const [key, val] of Object.entries(copy)) {
|
|
204
|
+
if (typeof val === 'string') {
|
|
205
|
+
const trimmed = val.trim();
|
|
206
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
207
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
208
|
+
try {
|
|
209
|
+
copy[key] = JSON.parse(trimmed);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// keep string
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return copy;
|
|
218
|
+
}
|
|
219
|
+
return row;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Writes a mutation to the outbox table, optionally coalescing with existing PENDING records for the same row.
|
|
223
|
+
* Returns the affected outboxId, or undefined if an offline INSERT was deleted offline before pushing.
|
|
224
|
+
*/
|
|
225
|
+
async recordOutboxMutation(tx, tableName, rowId, operation, payload, timestamp) {
|
|
226
|
+
if (this.coalesceOutbox) {
|
|
227
|
+
const existing = await outbox_store_js_1.OutboxStore.findPendingByRow(tx, tableName, rowId);
|
|
228
|
+
if (existing && existing.status === 'PENDING') {
|
|
229
|
+
if (operation === 'DELETE') {
|
|
230
|
+
if (existing.operation === 'INSERT') {
|
|
231
|
+
// Created offline and deleted offline before ever being pushed to server: clean up outbox completely
|
|
232
|
+
await outbox_store_js_1.OutboxStore.deleteRecord(tx, existing.id);
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
await outbox_store_js_1.OutboxStore.updateOperation(tx, existing.id, 'DELETE', null, timestamp);
|
|
237
|
+
return existing.id;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else if (operation === 'UPDATE') {
|
|
241
|
+
const merged = { ...(existing.payload || {}), ...(payload || {}) };
|
|
242
|
+
await outbox_store_js_1.OutboxStore.updatePayload(tx, existing.id, merged, timestamp);
|
|
243
|
+
return existing.id;
|
|
244
|
+
}
|
|
245
|
+
else if (operation === 'INSERT') {
|
|
246
|
+
await outbox_store_js_1.OutboxStore.updateOperation(tx, existing.id, 'INSERT', payload, timestamp);
|
|
247
|
+
return existing.id;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const outboxId = (0, uuid_js_1.generateUUID)();
|
|
252
|
+
await outbox_store_js_1.OutboxStore.insertRecord(tx, {
|
|
253
|
+
id: outboxId,
|
|
254
|
+
tableName,
|
|
255
|
+
rowId,
|
|
256
|
+
operation,
|
|
257
|
+
payload,
|
|
258
|
+
timestamp,
|
|
259
|
+
});
|
|
260
|
+
return outboxId;
|
|
261
|
+
}
|
|
116
262
|
/**
|
|
117
263
|
* Initializes the client: creates sync_outbox schema, ensures autoSchema tables if enabled,
|
|
118
|
-
* and starts
|
|
264
|
+
* applies migrations, and optionally starts automatic sync. Concurrent/repeated calls
|
|
265
|
+
* share initialization; failures can be retried. After stop(), explicitly start the worker.
|
|
119
266
|
*/
|
|
120
267
|
async initialize() {
|
|
268
|
+
if (!this.initialization) {
|
|
269
|
+
this.initialization = this.initializeStorage().catch((error) => {
|
|
270
|
+
this.initialization = undefined;
|
|
271
|
+
throw error;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
await this.initialization;
|
|
275
|
+
}
|
|
276
|
+
async initializeStorage() {
|
|
121
277
|
await outbox_store_js_1.OutboxStore.ensureSchema(this.db);
|
|
122
278
|
await meta_store_js_1.MetaStore.ensureSchema(this.db);
|
|
123
279
|
await outbox_store_js_1.OutboxStore.resetStaleProcessing(this.db);
|
|
280
|
+
if (this.migrations.length > 0) {
|
|
281
|
+
await this.db.transaction(async (tx) => {
|
|
282
|
+
for (const query of this.migrations) {
|
|
283
|
+
if (query.trim())
|
|
284
|
+
await tx.execute(query);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
124
288
|
if (this.autoSchema && this.tables.length > 0) {
|
|
125
289
|
for (const table of this.tables) {
|
|
126
290
|
await this.schemaManager.ensureTable(this.db, table, this.primaryKey);
|
|
127
291
|
}
|
|
128
292
|
}
|
|
129
|
-
this.
|
|
293
|
+
if (this.autoStart)
|
|
294
|
+
this.syncWorker.start();
|
|
130
295
|
}
|
|
131
296
|
/**
|
|
132
297
|
* Performs an atomic dual-write: mutates the local data table AND writes
|
|
@@ -134,8 +299,8 @@ class LocalFirstClient {
|
|
|
134
299
|
*/
|
|
135
300
|
async mutate(tableName, rowId, operation, payload, options = {}) {
|
|
136
301
|
const pk = options.primaryKey ?? this.primaryKey;
|
|
137
|
-
const outboxId = (0, uuid_js_1.generateUUID)();
|
|
138
302
|
const timestamp = options.customTimestamp ?? Date.now();
|
|
303
|
+
let affectedOutboxId;
|
|
139
304
|
const executeInTransaction = async (tx) => {
|
|
140
305
|
// 1. Mutate local table
|
|
141
306
|
await table_manager_js_1.TableManager.applyMutation(tx, tableName, rowId, operation, payload, pk, {
|
|
@@ -144,14 +309,7 @@ class LocalFirstClient {
|
|
|
144
309
|
});
|
|
145
310
|
// 2. Insert into sync_outbox (if not skipped)
|
|
146
311
|
if (!options.skipOutbox) {
|
|
147
|
-
await
|
|
148
|
-
id: outboxId,
|
|
149
|
-
tableName,
|
|
150
|
-
rowId,
|
|
151
|
-
operation,
|
|
152
|
-
payload,
|
|
153
|
-
timestamp,
|
|
154
|
-
});
|
|
312
|
+
affectedOutboxId = await this.recordOutboxMutation(tx, tableName, rowId, operation, payload, timestamp);
|
|
155
313
|
}
|
|
156
314
|
};
|
|
157
315
|
if (options.tx) {
|
|
@@ -168,7 +326,10 @@ class LocalFirstClient {
|
|
|
168
326
|
source: 'local',
|
|
169
327
|
});
|
|
170
328
|
if (!options.skipOutbox) {
|
|
171
|
-
this.events.emit('sync_outbox', {
|
|
329
|
+
this.events.emit('sync_outbox', {
|
|
330
|
+
operation: affectedOutboxId ? 'INSERT' : 'DELETE',
|
|
331
|
+
rowId: affectedOutboxId || rowId,
|
|
332
|
+
});
|
|
172
333
|
// 4. Trigger immediate debounced push
|
|
173
334
|
this.syncWorker.schedulePush(20);
|
|
174
335
|
}
|
|
@@ -198,17 +359,11 @@ class LocalFirstClient {
|
|
|
198
359
|
});
|
|
199
360
|
const skipOutbox = m.skipOutbox ?? options.skipOutbox ?? false;
|
|
200
361
|
if (!skipOutbox) {
|
|
201
|
-
const outboxId = (0, uuid_js_1.generateUUID)();
|
|
202
362
|
const timestamp = m.customTimestamp ?? options.customTimestamp ?? Date.now();
|
|
203
|
-
await
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
operation: m.operation,
|
|
208
|
-
payload,
|
|
209
|
-
timestamp,
|
|
210
|
-
});
|
|
211
|
-
queuedOutboxIds.push(outboxId);
|
|
363
|
+
const outboxId = await this.recordOutboxMutation(tx, m.tableName, rowId, m.operation, payload, timestamp);
|
|
364
|
+
if (outboxId) {
|
|
365
|
+
queuedOutboxIds.push(outboxId);
|
|
366
|
+
}
|
|
212
367
|
}
|
|
213
368
|
queuedEvents.push({
|
|
214
369
|
tableName: m.tableName,
|
|
@@ -309,6 +464,27 @@ class LocalFirstClient {
|
|
|
309
464
|
async update(tableName, rowId, payload, options = {}) {
|
|
310
465
|
await this.mutate(tableName, rowId, 'UPDATE', payload, options);
|
|
311
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* Updates multiple records atomically in a single database transaction.
|
|
469
|
+
* Emits reactive change events and queues outbox entries together.
|
|
470
|
+
*
|
|
471
|
+
* Example:
|
|
472
|
+
* await client.updateMany('todos', [
|
|
473
|
+
* { id: 'todo-1', payload: { completed: 1 } },
|
|
474
|
+
* { id: 'todo-2', payload: { completed: 1 } },
|
|
475
|
+
* ]);
|
|
476
|
+
*/
|
|
477
|
+
async updateMany(tableName, updates, options = {}) {
|
|
478
|
+
if (!updates || updates.length === 0)
|
|
479
|
+
return;
|
|
480
|
+
const mutations = updates.map((u) => ({
|
|
481
|
+
tableName,
|
|
482
|
+
rowId: u.id,
|
|
483
|
+
operation: 'UPDATE',
|
|
484
|
+
payload: u.payload,
|
|
485
|
+
}));
|
|
486
|
+
await this.batch(mutations, options);
|
|
487
|
+
}
|
|
312
488
|
/**
|
|
313
489
|
* Convenience helper to delete a record by ID.
|
|
314
490
|
*
|
|
@@ -321,39 +497,201 @@ class LocalFirstClient {
|
|
|
321
497
|
/**
|
|
322
498
|
* Reads records from a local SQLite table without requiring manual SQL.
|
|
323
499
|
* Supports where filtering with rich operators ($in, $gt, $contains, $or, etc.), order, limit, and offset.
|
|
500
|
+
* Pushes simple equality where clauses down to SQLite for high performance, with memory fallback.
|
|
324
501
|
* Gracefully returns [] if table or columns have not been populated yet on empty schemas.
|
|
325
502
|
*
|
|
503
|
+
/**
|
|
504
|
+
* Executes a raw relational SQL query with parameters directly against SQLite.
|
|
505
|
+
* Enables complex multi-table JOINs, GROUP BY aggregations (COUNT, SUM, AVG), CTEs, and window functions.
|
|
506
|
+
*/
|
|
507
|
+
async query(sql, params = []) {
|
|
508
|
+
return (await this.db.execute(sql, params));
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Returns a fluent SQL QueryBuilder for the specified table.
|
|
512
|
+
* Allows writing clean JavaScript method chains (select, join, leftJoin, where, orderBy, groupBy)
|
|
513
|
+
* that automatically compile into optimized native SQLite queries.
|
|
514
|
+
*
|
|
515
|
+
* Example:
|
|
516
|
+
* const activeRentals = await client
|
|
517
|
+
* .from('contracts')
|
|
518
|
+
* .join('houses', 'contracts.house_id', 'houses.id')
|
|
519
|
+
* .leftJoin('tenants', 'contracts.tenant_id', 'tenants.id')
|
|
520
|
+
* .where({ 'contracts.business_id': bizId, 'contracts.status': 'ACTIVE' })
|
|
521
|
+
* .select('contracts.id', 'contracts.rent_amount', 'houses.house_number', 'tenants.name')
|
|
522
|
+
* .execute();
|
|
523
|
+
*/
|
|
524
|
+
from(tableName) {
|
|
525
|
+
return new query_builder_js_1.QueryBuilder(this, tableName);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Executes a native SQLite COUNT(*) query and returns the count directly.
|
|
529
|
+
*/
|
|
530
|
+
async count(tableName, options) {
|
|
531
|
+
const qb = this.from(tableName);
|
|
532
|
+
if (options?.where)
|
|
533
|
+
qb.where(options.where);
|
|
534
|
+
return qb.count();
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Executes a native SQLite SUM(column) query and returns the sum directly.
|
|
538
|
+
*/
|
|
539
|
+
async sum(tableName, column, options) {
|
|
540
|
+
const qb = this.from(tableName);
|
|
541
|
+
if (options?.where)
|
|
542
|
+
qb.where(options.where);
|
|
543
|
+
return qb.sum(column);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Executes a native SQLite AVG(column) query and returns the average directly.
|
|
547
|
+
*/
|
|
548
|
+
async avg(tableName, column, options) {
|
|
549
|
+
const qb = this.from(tableName);
|
|
550
|
+
if (options?.where)
|
|
551
|
+
qb.where(options.where);
|
|
552
|
+
return qb.avg(column);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Executes native multi-metric SQLite aggregation in a single query.
|
|
556
|
+
*
|
|
326
557
|
* Example:
|
|
327
|
-
* const
|
|
328
|
-
*
|
|
558
|
+
* const stats = await client.aggregate('receipts', {
|
|
559
|
+
* where: { business_id: bizId },
|
|
560
|
+
* count: true,
|
|
561
|
+
* sum: ['amount', 'refunded_amount'],
|
|
562
|
+
* avg: ['amount']
|
|
563
|
+
* });
|
|
329
564
|
*/
|
|
565
|
+
async aggregate(tableName, spec) {
|
|
566
|
+
const projections = [];
|
|
567
|
+
if (spec.count) {
|
|
568
|
+
projections.push('COUNT(*) AS "_count"');
|
|
569
|
+
}
|
|
570
|
+
if (spec.sum) {
|
|
571
|
+
for (const col of spec.sum) {
|
|
572
|
+
projections.push(`COALESCE(SUM("${col.replace(/"/g, '""')}"), 0) AS "sum_${col}"`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (spec.avg) {
|
|
576
|
+
for (const col of spec.avg) {
|
|
577
|
+
projections.push(`AVG("${col.replace(/"/g, '""')}") AS "avg_${col}"`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (spec.min) {
|
|
581
|
+
for (const col of spec.min) {
|
|
582
|
+
projections.push(`MIN("${col.replace(/"/g, '""')}") AS "min_${col}"`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (spec.max) {
|
|
586
|
+
for (const col of spec.max) {
|
|
587
|
+
projections.push(`MAX("${col.replace(/"/g, '""')}") AS "max_${col}"`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (projections.length === 0) {
|
|
591
|
+
return {};
|
|
592
|
+
}
|
|
593
|
+
const qb = this.from(tableName).select(...projections);
|
|
594
|
+
if (spec.where) {
|
|
595
|
+
qb.where(spec.where);
|
|
596
|
+
}
|
|
597
|
+
const rows = await qb.execute();
|
|
598
|
+
const row = rows[0] || {};
|
|
599
|
+
const result = {};
|
|
600
|
+
if (spec.count) {
|
|
601
|
+
result.count = typeof row._count === 'number' ? row._count : Number(row._count) || 0;
|
|
602
|
+
}
|
|
603
|
+
if (spec.sum) {
|
|
604
|
+
result.sum = {};
|
|
605
|
+
for (const col of spec.sum) {
|
|
606
|
+
const val = row[`sum_${col}`];
|
|
607
|
+
result.sum[col] = typeof val === 'number' ? val : Number(val) || 0;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (spec.avg) {
|
|
611
|
+
result.avg = {};
|
|
612
|
+
for (const col of spec.avg) {
|
|
613
|
+
const val = row[`avg_${col}`];
|
|
614
|
+
result.avg[col] = val === null || val === undefined ? null : Number(val) || 0;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (spec.min) {
|
|
618
|
+
result.min = {};
|
|
619
|
+
for (const col of spec.min) {
|
|
620
|
+
result.min[col] = row[`min_${col}`];
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (spec.max) {
|
|
624
|
+
result.max = {};
|
|
625
|
+
for (const col of spec.max) {
|
|
626
|
+
result.max[col] = row[`max_${col}`];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
330
631
|
async findMany(tableName, options) {
|
|
632
|
+
const rows = await this.from(tableName).applyOptions(options).execute();
|
|
633
|
+
return (0, query_js_1.applyQueryOptions)(rows, options);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Finds a single record by primary key.
|
|
637
|
+
*
|
|
638
|
+
* Example:
|
|
639
|
+
* const tenant = await client.findById('tenants', 'ten-1');
|
|
640
|
+
*/
|
|
641
|
+
async findById(tableName, id, options) {
|
|
642
|
+
// Primary-key lookup in SQLite instead of loading the whole table into JS.
|
|
331
643
|
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
332
|
-
|
|
644
|
+
const safePk = (0, sql_js_1.escapeIdentifier)(this.primaryKey);
|
|
333
645
|
try {
|
|
334
|
-
rows = await this.db.execute(`SELECT * FROM ${safeTable}
|
|
646
|
+
const rows = await this.db.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} = ? LIMIT 1`, [id]);
|
|
647
|
+
if (!rows || rows.length === 0)
|
|
648
|
+
return null;
|
|
649
|
+
const row = this.hydrateRow(tableName, rows[0]);
|
|
650
|
+
if (!options?.includeDeleted && (0, query_js_1.isDeletedRecord)(row)) {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
return row;
|
|
335
654
|
}
|
|
336
655
|
catch (err) {
|
|
337
656
|
const msg = String(err?.message || err || '');
|
|
338
657
|
if (msg.includes('no such table') || msg.includes('no such column')) {
|
|
339
|
-
return
|
|
658
|
+
return null;
|
|
340
659
|
}
|
|
341
660
|
throw err;
|
|
342
661
|
}
|
|
343
|
-
return (0, query_js_1.applyQueryOptions)(rows, options);
|
|
344
662
|
}
|
|
345
663
|
/**
|
|
346
|
-
* Finds a single
|
|
664
|
+
* Finds multiple records by primary keys in a single batched SQL query.
|
|
665
|
+
* Eliminates the N-roundtrip bridge overhead of multiple findById calls.
|
|
666
|
+
* Automatically omits soft-deleted records unless includeDeleted: true.
|
|
347
667
|
*
|
|
348
668
|
* Example:
|
|
349
|
-
* const
|
|
669
|
+
* const tenants = await client.findByIds('tenants', ['ten-1', 'ten-2', 'ten-3']);
|
|
350
670
|
*/
|
|
351
|
-
async
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
671
|
+
async findByIds(tableName, ids, options) {
|
|
672
|
+
if (!ids || ids.length === 0)
|
|
673
|
+
return [];
|
|
674
|
+
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
675
|
+
const safePk = (0, sql_js_1.escapeIdentifier)(this.primaryKey);
|
|
676
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
677
|
+
try {
|
|
678
|
+
const rows = await this.db.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} IN (${placeholders})`, ids);
|
|
679
|
+
if (!rows || rows.length === 0)
|
|
680
|
+
return [];
|
|
681
|
+
const hasJsonConfig = Boolean(this.jsonColumns[tableName] || this.autoHydrateJSON);
|
|
682
|
+
const hydrated = hasJsonConfig ? rows.map((r) => this.hydrateRow(tableName, r)) : rows;
|
|
683
|
+
if (!options?.includeDeleted) {
|
|
684
|
+
return hydrated.filter((r) => !(0, query_js_1.isDeletedRecord)(r));
|
|
685
|
+
}
|
|
686
|
+
return hydrated;
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
const msg = String(err?.message || err || '');
|
|
690
|
+
if (msg.includes('no such table') || msg.includes('no such column')) {
|
|
691
|
+
return [];
|
|
692
|
+
}
|
|
693
|
+
throw err;
|
|
694
|
+
}
|
|
357
695
|
}
|
|
358
696
|
/**
|
|
359
697
|
* Tracks an existing mutation into sync_outbox without modifying the table.
|
|
@@ -435,11 +773,58 @@ class LocalFirstClient {
|
|
|
435
773
|
async resetSyncCursor() {
|
|
436
774
|
await this.syncWorker.resetSyncCursor();
|
|
437
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* Dynamically sets or updates the active sync scope.
|
|
778
|
+
*/
|
|
779
|
+
setSyncScope(scope) {
|
|
780
|
+
this.syncWorker.setSyncScope(scope);
|
|
781
|
+
}
|
|
438
782
|
/**
|
|
439
783
|
* Stops all active intervals, background sync loops, and network listeners.
|
|
440
784
|
*/
|
|
441
785
|
stop() {
|
|
442
786
|
this.syncWorker.stop();
|
|
443
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* Stops background sync workers, cleans up remote listeners,
|
|
790
|
+
* and closes the local database adapter connection (releasing native locks).
|
|
791
|
+
*/
|
|
792
|
+
async close() {
|
|
793
|
+
this.stop();
|
|
794
|
+
if (typeof this.remote.destroy === 'function') {
|
|
795
|
+
this.remote.destroy();
|
|
796
|
+
}
|
|
797
|
+
if (typeof this.db.close === 'function') {
|
|
798
|
+
await this.db.close();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Resets local state by stopping sync and deleting or dropping all managed tables,
|
|
803
|
+
* clearing the sync_outbox and _sync_meta stores, and resetting the sync cursor.
|
|
804
|
+
*/
|
|
805
|
+
async reset(options = {}) {
|
|
806
|
+
this.stop();
|
|
807
|
+
const tablesToReset = Array.from(new Set([...this.tables, 'sync_outbox', 'sync_meta']));
|
|
808
|
+
for (const table of tablesToReset) {
|
|
809
|
+
try {
|
|
810
|
+
if (options.dropTables) {
|
|
811
|
+
await this.db.execute(`DROP TABLE IF EXISTS "${table}"`);
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
await this.db.execute(`DELETE FROM "${table}"`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
// Table may not exist yet; safe to ignore
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (options.dropTables) {
|
|
822
|
+
this.initialization = undefined;
|
|
823
|
+
// Dropped tables must be re-created by autoSchema on next write.
|
|
824
|
+
this.schemaManager.clearCache();
|
|
825
|
+
}
|
|
826
|
+
await this.resetSyncCursor();
|
|
827
|
+
this.events.emit('reset', { dropTables: options.dropTables ?? false });
|
|
828
|
+
}
|
|
444
829
|
}
|
|
445
830
|
exports.LocalFirstClient = LocalFirstClient;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './client.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export * from './query.js';
|
|
4
|
+
export * from './sync-worker.js';
|
|
5
|
+
export * from './conflict.js';
|
|
6
|
+
export * from './outbox-store.js';
|
|
7
|
+
export * from './meta-store.js';
|
|
8
|
+
export * from './table-manager.js';
|
|
9
|
+
export * from './schema-manager.js';
|
|
10
|
+
export * from './query-builder.js';
|
|
11
|
+
export * from '../reactive/event-emitter.js';
|
|
12
|
+
export * from '../adapters/network/index.js';
|
|
13
|
+
export type { LocalDBAdapter, LocalTransaction } from '../adapters/local/index.js';
|
|
14
|
+
export type { RemoteDBAdapter } from '../adapters/remote/index.js';
|
|
15
|
+
export * from '../utils/logger.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// Provider-independent entry point: no config resolvers, UI, or backend implementations.
|
|
18
|
+
__exportStar(require("./client.js"), exports);
|
|
19
|
+
__exportStar(require("./types.js"), exports);
|
|
20
|
+
__exportStar(require("./query.js"), exports);
|
|
21
|
+
__exportStar(require("./sync-worker.js"), exports);
|
|
22
|
+
__exportStar(require("./conflict.js"), exports);
|
|
23
|
+
__exportStar(require("./outbox-store.js"), exports);
|
|
24
|
+
__exportStar(require("./meta-store.js"), exports);
|
|
25
|
+
__exportStar(require("./table-manager.js"), exports);
|
|
26
|
+
__exportStar(require("./schema-manager.js"), exports);
|
|
27
|
+
__exportStar(require("./query-builder.js"), exports);
|
|
28
|
+
__exportStar(require("../reactive/event-emitter.js"), exports);
|
|
29
|
+
__exportStar(require("../adapters/network/index.js"), exports);
|
|
30
|
+
__exportStar(require("../utils/logger.js"), exports);
|