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.
Files changed (36) hide show
  1. package/dist/adapters/local/better-sqlite3.d.ts +4 -1
  2. package/dist/adapters/local/better-sqlite3.js +34 -14
  3. package/dist/adapters/local/expo-sqlite.d.ts +10 -2
  4. package/dist/adapters/local/expo-sqlite.js +36 -20
  5. package/dist/adapters/local/node-sqlite.d.ts +4 -1
  6. package/dist/adapters/local/node-sqlite.js +29 -12
  7. package/dist/adapters/remote/firestore.js +39 -32
  8. package/dist/adapters/remote/supabase.d.ts +7 -2
  9. package/dist/adapters/remote/supabase.js +192 -85
  10. package/dist/config/types.d.ts +1 -1
  11. package/dist/core/client.d.ts +52 -15
  12. package/dist/core/client.js +195 -208
  13. package/dist/core/index.d.ts +1 -0
  14. package/dist/core/index.js +1 -0
  15. package/dist/core/outbox-store.d.ts +5 -0
  16. package/dist/core/outbox-store.js +29 -4
  17. package/dist/core/query-builder.d.ts +120 -0
  18. package/dist/core/query-builder.js +441 -0
  19. package/dist/core/query.js +28 -32
  20. package/dist/core/schema-manager.d.ts +4 -15
  21. package/dist/core/schema-manager.js +48 -103
  22. package/dist/core/sync-worker.js +7 -2
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/react/SyncInspector.js +0 -70
  26. package/dist/react/index.d.ts +39 -0
  27. package/dist/react/index.js +232 -34
  28. package/dist/react/status.d.ts +5 -0
  29. package/dist/react/status.js +20 -12
  30. package/dist/schema/generator.d.ts +49 -0
  31. package/dist/schema/generator.js +209 -0
  32. package/dist/schema/index.d.ts +1 -0
  33. package/dist/schema/index.js +17 -0
  34. package/dist/utils/uuid.d.ts +1 -1
  35. package/dist/utils/uuid.js +7 -16
  36. package/package.json +7 -2
@@ -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
- if (aObj.updated_at !== undefined &&
36
- bObj.updated_at !== undefined &&
37
- aObj.updated_at !== bObj.updated_at) {
38
- return false;
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
- // In-memory query result cache to eliminate initial render latency and flickering
79
- const queryCache = new Map();
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
- // De-duplicate simultaneous identical in-flight queries
123
- let inFlight = inFlightQueries.get(cacheKey);
124
- if (!inFlight) {
125
- inFlight = (async () => {
126
- const param = queryParamRef.current;
127
- if (typeof param === 'function') {
128
- return (await param(client)) || [];
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
- let inFlight = inFlightQueries.get(cacheKey);
195
- if (!inFlight) {
196
- inFlight = client.findById(tableName, rowId, options).then((r) => (r ? [r] : []));
197
- inFlightQueries.set(cacheKey, inFlight);
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]);
@@ -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
  /**
@@ -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
- const unsubOutbox = client.events.on('sync_outbox', () => {
59
- scheduleRefresh();
60
- });
61
- const unsubPush = client.events.on('sync:push', () => {
62
- scheduleRefresh();
63
- });
64
- const unsubPull = client.events.on('sync:pull', () => {
65
- scheduleRefresh();
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, scheduleRefresh]);
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);
@@ -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 supported, with uuid.v4 fallback.
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 };