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/react/index.js
CHANGED
|
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
exports.SyncIndicator = exports.SyncInspector = void 0;
|
|
18
18
|
exports.clearLiveQueryCache = clearLiveQueryCache;
|
|
19
19
|
exports.useLiveQuery = useLiveQuery;
|
|
20
|
+
exports.useLiveQueries = useLiveQueries;
|
|
20
21
|
exports.useLiveRow = useLiveRow;
|
|
21
22
|
exports.useLiveInfiniteQuery = useLiveInfiniteQuery;
|
|
22
23
|
exports.useMutations = useMutations;
|
|
@@ -32,10 +33,14 @@ function areRowsEqual(a, b) {
|
|
|
32
33
|
if (aObj.id !== undefined && bObj.id !== undefined && aObj.id !== bObj.id) {
|
|
33
34
|
return false;
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const aTs = aObj.updated_at ?? aObj.updatedAt;
|
|
37
|
+
const bTs = bObj.updated_at ?? bObj.updatedAt;
|
|
38
|
+
if (aObj.id !== undefined &&
|
|
39
|
+
bObj.id !== undefined &&
|
|
40
|
+
aObj.id === bObj.id &&
|
|
41
|
+
aTs !== undefined &&
|
|
42
|
+
bTs !== undefined) {
|
|
43
|
+
return aTs === bTs;
|
|
39
44
|
}
|
|
40
45
|
const aKeys = Object.keys(aObj);
|
|
41
46
|
const bKeys = Object.keys(bObj);
|
|
@@ -75,9 +80,50 @@ function areRowArraysEqual(a, b) {
|
|
|
75
80
|
}
|
|
76
81
|
return true;
|
|
77
82
|
}
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
const MAX_QUERY_CACHE_SIZE = 100;
|
|
84
|
+
class LRUQueryCache {
|
|
85
|
+
map = new Map();
|
|
86
|
+
get(key) {
|
|
87
|
+
const val = this.map.get(key);
|
|
88
|
+
if (val !== undefined) {
|
|
89
|
+
this.map.delete(key);
|
|
90
|
+
this.map.set(key, val);
|
|
91
|
+
}
|
|
92
|
+
return val;
|
|
93
|
+
}
|
|
94
|
+
set(key, val) {
|
|
95
|
+
if (this.map.has(key)) {
|
|
96
|
+
this.map.delete(key);
|
|
97
|
+
}
|
|
98
|
+
else if (this.map.size >= MAX_QUERY_CACHE_SIZE) {
|
|
99
|
+
const oldestKey = this.map.keys().next().value;
|
|
100
|
+
if (oldestKey !== undefined) {
|
|
101
|
+
this.map.delete(oldestKey);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.map.set(key, val);
|
|
105
|
+
}
|
|
106
|
+
has(key) {
|
|
107
|
+
return this.map.has(key);
|
|
108
|
+
}
|
|
109
|
+
delete(key) {
|
|
110
|
+
return this.map.delete(key);
|
|
111
|
+
}
|
|
112
|
+
clear() {
|
|
113
|
+
this.map.clear();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// In-memory query result cache capped with LRU eviction to prevent memory leaks
|
|
117
|
+
const queryCache = new LRUQueryCache();
|
|
80
118
|
const inFlightQueries = new Map();
|
|
119
|
+
async function fetchDeduped(key, fn) {
|
|
120
|
+
let inFlight = inFlightQueries.get(key);
|
|
121
|
+
if (!inFlight) {
|
|
122
|
+
inFlight = fn().finally(() => inFlightQueries.delete(key));
|
|
123
|
+
inFlightQueries.set(key, inFlight);
|
|
124
|
+
}
|
|
125
|
+
return inFlight;
|
|
126
|
+
}
|
|
81
127
|
/**
|
|
82
128
|
* Clears the in-memory live query cache.
|
|
83
129
|
*/
|
|
@@ -119,27 +165,14 @@ function useLiveQuery(client, tableName, queryOrOptions) {
|
|
|
119
165
|
if (!client || !client.db)
|
|
120
166
|
return;
|
|
121
167
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
else if (typeof param === 'object' && param !== null) {
|
|
131
|
-
return await client.findMany(tableName, param);
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
return await client.findMany(tableName);
|
|
135
|
-
}
|
|
136
|
-
})();
|
|
137
|
-
inFlightQueries.set(cacheKey, inFlight);
|
|
138
|
-
inFlight.finally(() => {
|
|
139
|
-
inFlightQueries.delete(cacheKey);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
const res = (await inFlight) || [];
|
|
168
|
+
const res = await fetchDeduped(cacheKey, async () => {
|
|
169
|
+
const param = queryParamRef.current;
|
|
170
|
+
if (typeof param === 'function')
|
|
171
|
+
return (await param(client)) || [];
|
|
172
|
+
if (typeof param === 'object' && param !== null)
|
|
173
|
+
return await client.findMany(tableName, param);
|
|
174
|
+
return await client.findMany(tableName);
|
|
175
|
+
});
|
|
143
176
|
queryCache.set(cacheKey, res);
|
|
144
177
|
setData((prev) => (areRowArraysEqual(prev, res) ? prev : res));
|
|
145
178
|
}
|
|
@@ -171,6 +204,174 @@ function useLiveQuery(client, tableName, queryOrOptions) {
|
|
|
171
204
|
}, [client, tableName, queryKey, refetch, scheduleRefetch]);
|
|
172
205
|
return { data, refetch, loading };
|
|
173
206
|
}
|
|
207
|
+
function normalizeMultiQueries(input) {
|
|
208
|
+
const items = [];
|
|
209
|
+
if (Array.isArray(input)) {
|
|
210
|
+
for (const item of input) {
|
|
211
|
+
if (typeof item === 'string') {
|
|
212
|
+
items.push({
|
|
213
|
+
key: item,
|
|
214
|
+
table: item,
|
|
215
|
+
queryKey: 'all',
|
|
216
|
+
cacheKey: `${item}:all`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else if (typeof item === 'object' && item !== null && item.table) {
|
|
220
|
+
const { table, ...options } = item;
|
|
221
|
+
const qKey = JSON.stringify(options);
|
|
222
|
+
items.push({
|
|
223
|
+
key: table,
|
|
224
|
+
table,
|
|
225
|
+
queryKey: qKey,
|
|
226
|
+
cacheKey: `${table}:${qKey}`,
|
|
227
|
+
options,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (typeof input === 'object' && input !== null) {
|
|
233
|
+
for (const [key, spec] of Object.entries(input)) {
|
|
234
|
+
if (typeof spec === 'string') {
|
|
235
|
+
items.push({
|
|
236
|
+
key,
|
|
237
|
+
table: spec,
|
|
238
|
+
queryKey: 'all',
|
|
239
|
+
cacheKey: `${spec}:all`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else if (typeof spec === 'function') {
|
|
243
|
+
items.push({
|
|
244
|
+
key,
|
|
245
|
+
table: key,
|
|
246
|
+
queryKey: 'custom',
|
|
247
|
+
cacheKey: `${key}:custom`,
|
|
248
|
+
fn: spec,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
else if (typeof spec === 'object' && spec !== null) {
|
|
252
|
+
const table = spec.table || key;
|
|
253
|
+
const options = { ...spec };
|
|
254
|
+
delete options.table;
|
|
255
|
+
const qKey = JSON.stringify(options);
|
|
256
|
+
items.push({
|
|
257
|
+
key,
|
|
258
|
+
table,
|
|
259
|
+
queryKey: qKey,
|
|
260
|
+
cacheKey: `${table}:${qKey}`,
|
|
261
|
+
options,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return items;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Reactive multi-table live query hook that loads data from multiple tables concurrently.
|
|
270
|
+
*
|
|
271
|
+
* Benefits:
|
|
272
|
+
* - Parallel fetching: queries SQLite tables concurrently via Promise.all.
|
|
273
|
+
* - Single atomic React commit: coalesces multiple table results into a single state update,
|
|
274
|
+
* preventing cascading re-renders and UI freezing.
|
|
275
|
+
* - Multi-table event coordination: listens to mutations across all requested tables,
|
|
276
|
+
* coalescing updates so downstream calculations only run once.
|
|
277
|
+
* - Zero-latency cache read-through: returns instant cached rows on mount.
|
|
278
|
+
*
|
|
279
|
+
* Usage with dictionary:
|
|
280
|
+
* ```ts
|
|
281
|
+
* const { data, loading } = useLiveQueries(client, {
|
|
282
|
+
* projects: 'projects',
|
|
283
|
+
* tasks: { where: { project_id: '123' }, orderBy: 'created_at' },
|
|
284
|
+
* tags: { table: 'labels' }
|
|
285
|
+
* });
|
|
286
|
+
* // data.projects, data.tasks, data.tags
|
|
287
|
+
* ```
|
|
288
|
+
*
|
|
289
|
+
* Usage with array:
|
|
290
|
+
* ```ts
|
|
291
|
+
* const { data, loading } = useLiveQueries(client, ['projects', 'tasks', 'tags']);
|
|
292
|
+
* // data.projects, data.tasks, data.tags
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
function useLiveQueries(client, queries) {
|
|
296
|
+
const signature = (0, react_1.useMemo)(() => {
|
|
297
|
+
return JSON.stringify(queries, (_, val) => (typeof val === 'function' ? 'fn' : val));
|
|
298
|
+
}, [queries]);
|
|
299
|
+
const items = (0, react_1.useMemo)(() => normalizeMultiQueries(queries), [signature]);
|
|
300
|
+
const [data, setData] = (0, react_1.useState)(() => {
|
|
301
|
+
const initial = {};
|
|
302
|
+
for (const item of items) {
|
|
303
|
+
const cached = queryCache.get(item.cacheKey);
|
|
304
|
+
initial[item.key] = cached ? cached : [];
|
|
305
|
+
}
|
|
306
|
+
return initial;
|
|
307
|
+
});
|
|
308
|
+
const [loading, setLoading] = (0, react_1.useState)(() => {
|
|
309
|
+
if (items.length === 0)
|
|
310
|
+
return false;
|
|
311
|
+
for (const item of items) {
|
|
312
|
+
if (!queryCache.has(item.cacheKey))
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
});
|
|
317
|
+
const refetch = (0, react_1.useCallback)(async () => {
|
|
318
|
+
if (!client || !client.db || items.length === 0)
|
|
319
|
+
return;
|
|
320
|
+
try {
|
|
321
|
+
const results = await Promise.all(items.map(async (item) => {
|
|
322
|
+
try {
|
|
323
|
+
const rows = await fetchDeduped(item.cacheKey, async () => {
|
|
324
|
+
if (item.fn)
|
|
325
|
+
return (await item.fn(client)) || [];
|
|
326
|
+
if (item.options)
|
|
327
|
+
return await client.findMany(item.table, item.options);
|
|
328
|
+
return await client.findMany(item.table);
|
|
329
|
+
});
|
|
330
|
+
queryCache.set(item.cacheKey, rows);
|
|
331
|
+
return { key: item.key, rows };
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
const msg = String(err?.message || err || '');
|
|
335
|
+
if (msg.includes('no such column') || msg.includes('no such table')) {
|
|
336
|
+
return { key: item.key, rows: [] };
|
|
337
|
+
}
|
|
338
|
+
console.error(`Error in useLiveQueries("${item.table}"):`, err);
|
|
339
|
+
return { key: item.key, rows: [] };
|
|
340
|
+
}
|
|
341
|
+
}));
|
|
342
|
+
setData((prev) => {
|
|
343
|
+
let changed = false;
|
|
344
|
+
const next = { ...prev };
|
|
345
|
+
for (const res of results) {
|
|
346
|
+
if (!areRowArraysEqual(prev[res.key], res.rows)) {
|
|
347
|
+
next[res.key] = res.rows;
|
|
348
|
+
changed = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return changed ? next : prev;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
setLoading(false);
|
|
356
|
+
}
|
|
357
|
+
}, [client, items]);
|
|
358
|
+
const scheduleRefetch = (0, runner_js_1.useCoalescedRunner)(refetch);
|
|
359
|
+
(0, react_1.useEffect)(() => {
|
|
360
|
+
if (!client || !client.db || items.length === 0)
|
|
361
|
+
return;
|
|
362
|
+
refetch();
|
|
363
|
+
const tables = Array.from(new Set(items.map((i) => i.table).filter(Boolean)));
|
|
364
|
+
const unsubs = tables.map((tbl) => client.events.on(tbl, () => {
|
|
365
|
+
scheduleRefetch();
|
|
366
|
+
}));
|
|
367
|
+
return () => {
|
|
368
|
+
for (const unsub of unsubs) {
|
|
369
|
+
unsub();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}, [client, signature, refetch, scheduleRefetch]);
|
|
373
|
+
return { data, loading, refetch };
|
|
374
|
+
}
|
|
174
375
|
/**
|
|
175
376
|
* Reactive hook to query a single record by primary key with 0ms cache read-through.
|
|
176
377
|
* Automatically re-renders only when this specific row or table changes.
|
|
@@ -191,13 +392,10 @@ function useLiveRow(client, tableName, rowId, options) {
|
|
|
191
392
|
return;
|
|
192
393
|
}
|
|
193
394
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
inFlight.finally(() => inFlightQueries.delete(cacheKey));
|
|
199
|
-
}
|
|
200
|
-
const res = await inFlight;
|
|
395
|
+
const res = await fetchDeduped(cacheKey, async () => {
|
|
396
|
+
const r = await client.findById(tableName, rowId, options);
|
|
397
|
+
return r ? [r] : [];
|
|
398
|
+
});
|
|
201
399
|
const row = res.length > 0 ? res[0] : null;
|
|
202
400
|
if (row) {
|
|
203
401
|
queryCache.set(cacheKey, [row]);
|
package/dist/react/status.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface UseSyncStatusResult {
|
|
|
13
13
|
/**
|
|
14
14
|
* Consolidated hook that tracks real-time sync connectivity, sync worker activity
|
|
15
15
|
* (pushing/pulling/syncing), and pending items in the local SQLite outbox queue.
|
|
16
|
+
*
|
|
17
|
+
* Performance notes:
|
|
18
|
+
* - sync:status events update isOnline/isPushing/isPulling/pendingCount in-place (no DB query).
|
|
19
|
+
* - The 50-record outbox fetch (pendingRecords) only runs on mount and on explicit refresh().
|
|
20
|
+
* - sync:push / sync:pull only trigger a lightweight pendingCount update via sync:status.
|
|
16
21
|
*/
|
|
17
22
|
export declare function useSyncStatus(client: LocalFirstClient | null | undefined): UseSyncStatusResult;
|
|
18
23
|
/**
|
package/dist/react/status.js
CHANGED
|
@@ -7,6 +7,11 @@ const runner_js_1 = require("./runner.js");
|
|
|
7
7
|
/**
|
|
8
8
|
* Consolidated hook that tracks real-time sync connectivity, sync worker activity
|
|
9
9
|
* (pushing/pulling/syncing), and pending items in the local SQLite outbox queue.
|
|
10
|
+
*
|
|
11
|
+
* Performance notes:
|
|
12
|
+
* - sync:status events update isOnline/isPushing/isPulling/pendingCount in-place (no DB query).
|
|
13
|
+
* - The 50-record outbox fetch (pendingRecords) only runs on mount and on explicit refresh().
|
|
14
|
+
* - sync:push / sync:pull only trigger a lightweight pendingCount update via sync:status.
|
|
10
15
|
*/
|
|
11
16
|
function useSyncStatus(client) {
|
|
12
17
|
const [isOnline, setIsOnline] = (0, react_1.useState)(true);
|
|
@@ -44,33 +49,36 @@ function useSyncStatus(client) {
|
|
|
44
49
|
// Ignored if client is stopping or tables initializing
|
|
45
50
|
}
|
|
46
51
|
}, [client]);
|
|
47
|
-
const scheduleRefresh = (0, runner_js_1.useCoalescedRunner)(refresh);
|
|
48
52
|
(0, react_1.useEffect)(() => {
|
|
49
53
|
if (!client || !client.db)
|
|
50
54
|
return;
|
|
55
|
+
// Initial full fetch on mount
|
|
51
56
|
refresh();
|
|
57
|
+
// sync:status fires after every push/pull cycle — update scalar fields directly,
|
|
58
|
+
// no DB round-trip needed. pendingCount comes from the event payload.
|
|
52
59
|
const unsubStatus = client.events.on('sync:status', (e) => {
|
|
53
60
|
setIsOnline(e.isOnline);
|
|
54
61
|
setIsPushing(e.isPushing);
|
|
55
62
|
setIsPulling(e.isPulling);
|
|
56
63
|
setPendingCount(e.pendingCount);
|
|
57
64
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
// Only refresh lastSyncedAt after a completed pull (lightweight meta query only)
|
|
66
|
+
const unsubPull = client.events.on('sync:pull', async (e) => {
|
|
67
|
+
if (e?.success) {
|
|
68
|
+
try {
|
|
69
|
+
const ts = await client.getLastSyncedAt().catch(() => null);
|
|
70
|
+
if (ts !== null && ts !== undefined) {
|
|
71
|
+
setLastSyncedAt(ts > 0 ? ts : null);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch { /* ignored */ }
|
|
75
|
+
}
|
|
66
76
|
});
|
|
67
77
|
return () => {
|
|
68
78
|
unsubStatus();
|
|
69
|
-
unsubOutbox();
|
|
70
|
-
unsubPush();
|
|
71
79
|
unsubPull();
|
|
72
80
|
};
|
|
73
|
-
}, [client, refresh
|
|
81
|
+
}, [client, refresh]);
|
|
74
82
|
return {
|
|
75
83
|
isOnline,
|
|
76
84
|
isPushing,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface SchemaIndexDefinition {
|
|
2
|
+
table: string;
|
|
3
|
+
columns: string[];
|
|
4
|
+
isOneToOne: boolean;
|
|
5
|
+
sql: string;
|
|
6
|
+
referencedRelation?: string;
|
|
7
|
+
referencedColumns?: string[];
|
|
8
|
+
comment?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface GenerateSchemaIndexesOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Path to the Supabase database.types.ts file.
|
|
13
|
+
*/
|
|
14
|
+
typesPath?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Raw string content of the database.types.ts file.
|
|
17
|
+
*/
|
|
18
|
+
typesContent?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Target path where the generated TypeScript file should be written.
|
|
21
|
+
*/
|
|
22
|
+
outputPath?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Whether to also generate composite sync indexes for tables with business_id and deleted.
|
|
25
|
+
* Default: true
|
|
26
|
+
*/
|
|
27
|
+
includeBusinessSyncIndexes?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface GenerateSchemaIndexesResult {
|
|
30
|
+
indexes: SchemaIndexDefinition[];
|
|
31
|
+
code: string;
|
|
32
|
+
written: boolean;
|
|
33
|
+
outputPath?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parses raw Supabase database.types.ts content and extracts foreign-key index definitions.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseSchemaIndexes(content: string, options?: {
|
|
39
|
+
includeBusinessSyncIndexes?: boolean;
|
|
40
|
+
}): SchemaIndexDefinition[];
|
|
41
|
+
/**
|
|
42
|
+
* Generates TypeScript source code containing index definitions and an applicator helper.
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatSchemaIndexesCode(indexes: SchemaIndexDefinition[]): string;
|
|
45
|
+
/**
|
|
46
|
+
* Core generator function.
|
|
47
|
+
* Accepts the path or content of database.types.ts and generates schema indexes.
|
|
48
|
+
*/
|
|
49
|
+
export declare function generateSchemaIndexes(typesPathOrOptions: string | GenerateSchemaIndexesOptions, maybeOutputPath?: string): Promise<GenerateSchemaIndexesResult>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSchemaIndexes = parseSchemaIndexes;
|
|
4
|
+
exports.formatSchemaIndexesCode = formatSchemaIndexesCode;
|
|
5
|
+
exports.generateSchemaIndexes = generateSchemaIndexes;
|
|
6
|
+
async function getNodeModules() {
|
|
7
|
+
try {
|
|
8
|
+
const fsModName = 'node:fs';
|
|
9
|
+
const pathModName = 'node:path';
|
|
10
|
+
const fsMod = await import(/* @vite-ignore */ fsModName);
|
|
11
|
+
const pathMod = await import(/* @vite-ignore */ pathModName);
|
|
12
|
+
return {
|
|
13
|
+
fs: (fsMod.default || fsMod),
|
|
14
|
+
path: (pathMod.default || pathMod),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
const req = typeof require !== 'undefined' ? require : null;
|
|
19
|
+
if (req) {
|
|
20
|
+
return {
|
|
21
|
+
fs: req('fs'),
|
|
22
|
+
path: req('path'),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
throw new Error('Node.js filesystem (fs) and path modules are not available in this environment.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parses raw Supabase database.types.ts content and extracts foreign-key index definitions.
|
|
30
|
+
*/
|
|
31
|
+
function parseSchemaIndexes(content, options = {}) {
|
|
32
|
+
const lines = content.split(/\r?\n/);
|
|
33
|
+
const includeBusiness = options.includeBusinessSyncIndexes ?? true;
|
|
34
|
+
let currentTable = null;
|
|
35
|
+
let inRow = false;
|
|
36
|
+
let inRelationships = false;
|
|
37
|
+
let relLines = [];
|
|
38
|
+
const tableColumns = new Map();
|
|
39
|
+
const relationships = [];
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
// Detect table definition: " <table_name>: {"
|
|
42
|
+
const tableMatch = line.match(/^\s{6}([a-z0-9_]+):\s*\{/);
|
|
43
|
+
if (tableMatch && !['Row', 'Insert', 'Update'].includes(tableMatch[1])) {
|
|
44
|
+
currentTable = tableMatch[1];
|
|
45
|
+
if (!tableColumns.has(currentTable)) {
|
|
46
|
+
tableColumns.set(currentTable, new Set());
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Track column names in Row: { ... }
|
|
50
|
+
if (line.includes('Row: {')) {
|
|
51
|
+
inRow = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (inRow) {
|
|
55
|
+
if (line.trim() === '};' || line.trim() === '}') {
|
|
56
|
+
inRow = false;
|
|
57
|
+
}
|
|
58
|
+
else if (currentTable) {
|
|
59
|
+
const colMatch = line.match(/^\s{10}([a-z0-9_]+):/);
|
|
60
|
+
if (colMatch) {
|
|
61
|
+
tableColumns.get(currentTable)?.add(colMatch[1]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Skip empty relationships
|
|
66
|
+
if (line.includes('Relationships: [];') || line.includes('Relationships: []')) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (line.includes('Relationships: [')) {
|
|
70
|
+
inRelationships = true;
|
|
71
|
+
relLines = [];
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (inRelationships) {
|
|
75
|
+
if (line.trim() === '];' || line.trim() === ']') {
|
|
76
|
+
inRelationships = false;
|
|
77
|
+
if (currentTable) {
|
|
78
|
+
const block = relLines.join('\n');
|
|
79
|
+
const itemRegex = /foreignKeyName:\s*['"]([^'"]+)['"][\s\S]*?columns:\s*\[([^\]]+)\][\s\S]*?isOneToOne:\s*(true|false)[\s\S]*?referencedRelation:\s*['"]([^'"]+)['"](?:[\s\S]*?referencedColumns:\s*\[([^\]]+)\])?/g;
|
|
80
|
+
const matches = [...block.matchAll(itemRegex)];
|
|
81
|
+
for (const m of matches) {
|
|
82
|
+
const columns = m[2].split(',').map(c => c.trim().replace(/['"]/g, ''));
|
|
83
|
+
const isOneToOne = m[3] === 'true';
|
|
84
|
+
const referencedRelation = m[4];
|
|
85
|
+
const referencedColumns = m[5]
|
|
86
|
+
? m[5].split(',').map(c => c.trim().replace(/['"]/g, ''))
|
|
87
|
+
: undefined;
|
|
88
|
+
const isUnique = isOneToOne ? 'UNIQUE ' : '';
|
|
89
|
+
const colSql = columns.map(c => `"${c}"`).join(', ');
|
|
90
|
+
const colSlug = columns.join('_');
|
|
91
|
+
const indexName = `idx_${currentTable}_${colSlug}`;
|
|
92
|
+
relationships.push({
|
|
93
|
+
table: currentTable,
|
|
94
|
+
columns,
|
|
95
|
+
isOneToOne,
|
|
96
|
+
referencedRelation,
|
|
97
|
+
referencedColumns,
|
|
98
|
+
sql: `CREATE ${isUnique}INDEX IF NOT EXISTS "${indexName}" ON "${currentTable}" (${colSql});`,
|
|
99
|
+
comment: referencedRelation
|
|
100
|
+
? `References ${referencedRelation}${referencedColumns ? `(${referencedColumns.join(', ')})` : ''}`
|
|
101
|
+
: undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
relLines.push(line);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Optionally append composite business sync indexes
|
|
111
|
+
if (includeBusiness) {
|
|
112
|
+
for (const [table, cols] of tableColumns.entries()) {
|
|
113
|
+
if (cols.has('business_id') && cols.has('deleted')) {
|
|
114
|
+
const idxName = `idx_${table}_biz_del`;
|
|
115
|
+
if (!relationships.some(r => r.table === table &&
|
|
116
|
+
r.columns.length === 2 &&
|
|
117
|
+
r.columns.includes('business_id') &&
|
|
118
|
+
r.columns.includes('deleted'))) {
|
|
119
|
+
relationships.push({
|
|
120
|
+
table,
|
|
121
|
+
columns: ['business_id', 'deleted'],
|
|
122
|
+
isOneToOne: false,
|
|
123
|
+
sql: `CREATE INDEX IF NOT EXISTS "${idxName}" ON "${table}" ("business_id", "deleted");`,
|
|
124
|
+
comment: 'Composite business partition index for zero-latency local queries',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return relationships;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Generates TypeScript source code containing index definitions and an applicator helper.
|
|
134
|
+
*/
|
|
135
|
+
function formatSchemaIndexesCode(indexes) {
|
|
136
|
+
const sqlStatements = indexes.map(i => i.sql).join('\n');
|
|
137
|
+
return `/**
|
|
138
|
+
* AUTO-GENERATED FILE - DO NOT EDIT MANUALLY.
|
|
139
|
+
* Generated by local-lite schema index generator.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* SQLite index statements extracted from Supabase database schema.
|
|
144
|
+
* Total indexes: ${indexes.length}
|
|
145
|
+
*/
|
|
146
|
+
export const SCHEMA_INDEXES: readonly string[] = \`
|
|
147
|
+
${sqlStatements}
|
|
148
|
+
\`.trim().split('\\n');
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Applies all schema indexes into SQLite to ensure zero-latency local reads.
|
|
152
|
+
* Executes each statement safely, ignoring errors if tables are not yet created.
|
|
153
|
+
*/
|
|
154
|
+
export async function ensureSchemaIndexes(db: {
|
|
155
|
+
execute: (query: string, params?: any[]) => Promise<any>;
|
|
156
|
+
}): Promise<void> {
|
|
157
|
+
for (const sql of SCHEMA_INDEXES) {
|
|
158
|
+
try {
|
|
159
|
+
await db.execute(sql);
|
|
160
|
+
} catch {
|
|
161
|
+
// Safe fallback if table has not been created yet
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Core generator function.
|
|
169
|
+
* Accepts the path or content of database.types.ts and generates schema indexes.
|
|
170
|
+
*/
|
|
171
|
+
async function generateSchemaIndexes(typesPathOrOptions, maybeOutputPath) {
|
|
172
|
+
const options = typeof typesPathOrOptions === 'string'
|
|
173
|
+
? { typesPath: typesPathOrOptions, outputPath: maybeOutputPath }
|
|
174
|
+
: typesPathOrOptions;
|
|
175
|
+
let content = options.typesContent;
|
|
176
|
+
if (!content && options.typesPath) {
|
|
177
|
+
const { fs, path } = await getNodeModules();
|
|
178
|
+
const resolvedPath = path.resolve(options.typesPath);
|
|
179
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
180
|
+
throw new Error(`Types file not found at: ${resolvedPath}`);
|
|
181
|
+
}
|
|
182
|
+
content = fs.readFileSync(resolvedPath, 'utf8');
|
|
183
|
+
}
|
|
184
|
+
if (!content) {
|
|
185
|
+
throw new Error('Either typesPath or typesContent must be provided to generateSchemaIndexes.');
|
|
186
|
+
}
|
|
187
|
+
const indexes = parseSchemaIndexes(content, {
|
|
188
|
+
includeBusinessSyncIndexes: options.includeBusinessSyncIndexes,
|
|
189
|
+
});
|
|
190
|
+
const code = formatSchemaIndexesCode(indexes);
|
|
191
|
+
let written = false;
|
|
192
|
+
let resolvedOutputPath;
|
|
193
|
+
if (options.outputPath) {
|
|
194
|
+
const { fs, path } = await getNodeModules();
|
|
195
|
+
resolvedOutputPath = path.resolve(options.outputPath);
|
|
196
|
+
const parentDir = path.dirname(resolvedOutputPath);
|
|
197
|
+
if (!fs.existsSync(parentDir)) {
|
|
198
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
fs.writeFileSync(resolvedOutputPath, code, 'utf8');
|
|
201
|
+
written = true;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
indexes,
|
|
205
|
+
code,
|
|
206
|
+
written,
|
|
207
|
+
outputPath: resolvedOutputPath,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './generator.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
__exportStar(require("./generator.js"), exports);
|
package/dist/utils/uuid.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from 'uuid';
|
|
2
2
|
/**
|
|
3
3
|
* Generates an RFC4122 v4 compliant UUID.
|
|
4
|
-
* Uses native crypto.randomUUID() when
|
|
4
|
+
* Uses native crypto.randomUUID() when available (detected once at import), with uuid.v4 fallback.
|
|
5
5
|
*/
|
|
6
6
|
export declare function generateUUID(): string;
|
|
7
7
|
export { uuidv4 };
|