@pol-studios/powersync 1.0.0 → 1.0.2

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 (51) hide show
  1. package/dist/attachments/index.js +1 -1
  2. package/dist/{chunk-4FJVBR3X.js → chunk-3AYXHQ4W.js} +24 -13
  3. package/dist/chunk-3AYXHQ4W.js.map +1 -0
  4. package/dist/{chunk-BJ36QDFN.js → chunk-7EMDVIZX.js} +1 -1
  5. package/dist/chunk-7EMDVIZX.js.map +1 -0
  6. package/dist/chunk-C2RSTGDC.js +726 -0
  7. package/dist/chunk-C2RSTGDC.js.map +1 -0
  8. package/dist/{chunk-NPNBGCRC.js → chunk-EJ23MXPQ.js} +1 -1
  9. package/dist/{chunk-NPNBGCRC.js.map → chunk-EJ23MXPQ.js.map} +1 -1
  10. package/dist/{chunk-CHRTN5PF.js → chunk-FPTDATY5.js} +1 -1
  11. package/dist/chunk-FPTDATY5.js.map +1 -0
  12. package/dist/chunk-GMFDCVMZ.js +1285 -0
  13. package/dist/chunk-GMFDCVMZ.js.map +1 -0
  14. package/dist/chunk-OLHGI472.js +1 -0
  15. package/dist/{chunk-CFCK2LHI.js → chunk-OTJXIRWX.js} +45 -40
  16. package/dist/chunk-OTJXIRWX.js.map +1 -0
  17. package/dist/{chunk-GBGATW2S.js → chunk-V6LJ6MR2.js} +86 -95
  18. package/dist/chunk-V6LJ6MR2.js.map +1 -0
  19. package/dist/chunk-VJCL2SWD.js +1 -0
  20. package/dist/chunk-VJCL2SWD.js.map +1 -0
  21. package/dist/connector/index.d.ts +1 -2
  22. package/dist/connector/index.js +3 -6
  23. package/dist/core/index.js +2 -2
  24. package/dist/{supabase-connector-D14-kl5v.d.ts → index-Cb-NI0Ct.d.ts} +159 -2
  25. package/dist/index.d.ts +2 -3
  26. package/dist/index.js +12 -13
  27. package/dist/index.native.d.ts +1 -2
  28. package/dist/index.native.js +13 -14
  29. package/dist/index.web.d.ts +1 -2
  30. package/dist/index.web.js +13 -14
  31. package/dist/platform/index.js.map +1 -1
  32. package/dist/platform/index.native.js +1 -1
  33. package/dist/platform/index.web.js +1 -1
  34. package/dist/provider/index.d.ts +6 -1
  35. package/dist/provider/index.js +6 -6
  36. package/dist/sync/index.js +3 -3
  37. package/package.json +35 -10
  38. package/dist/chunk-4FJVBR3X.js.map +0 -1
  39. package/dist/chunk-7BPTGEVG.js +0 -1
  40. package/dist/chunk-BJ36QDFN.js.map +0 -1
  41. package/dist/chunk-CFCK2LHI.js.map +0 -1
  42. package/dist/chunk-CHRTN5PF.js.map +0 -1
  43. package/dist/chunk-FLHDT4TS.js +0 -327
  44. package/dist/chunk-FLHDT4TS.js.map +0 -1
  45. package/dist/chunk-GBGATW2S.js.map +0 -1
  46. package/dist/chunk-Q3LFFMRR.js +0 -925
  47. package/dist/chunk-Q3LFFMRR.js.map +0 -1
  48. package/dist/chunk-T225XEML.js +0 -298
  49. package/dist/chunk-T225XEML.js.map +0 -1
  50. package/dist/index-nae7nzib.d.ts +0 -147
  51. /package/dist/{chunk-7BPTGEVG.js.map → chunk-OLHGI472.js.map} +0 -0
@@ -0,0 +1,726 @@
1
+ import {
2
+ classifySupabaseError
3
+ } from "./chunk-FPTDATY5.js";
4
+
5
+ // src/connector/types.ts
6
+ var defaultSchemaRouter = () => "public";
7
+
8
+ // src/conflicts/detect.ts
9
+ var TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
10
+ function validateTableName(table) {
11
+ if (!TABLE_NAME_REGEX.test(table)) {
12
+ throw new Error(`Invalid table name: ${table}`);
13
+ }
14
+ }
15
+ var DEFAULT_IGNORED_FIELDS = ["updatedAt", "createdAt", "_version", "id"];
16
+ async function detectConflicts(table, recordId, localVersion, serverVersion, pendingChanges, supabase, config) {
17
+ const ignoredFields = /* @__PURE__ */ new Set([...DEFAULT_IGNORED_FIELDS, ...config?.ignoredFields ?? []]);
18
+ const filteredPendingChanges = {};
19
+ for (const [field, value] of Object.entries(pendingChanges)) {
20
+ if (!ignoredFields.has(field)) {
21
+ filteredPendingChanges[field] = value;
22
+ }
23
+ }
24
+ if (localVersion === serverVersion) {
25
+ return {
26
+ hasConflict: false,
27
+ conflicts: [],
28
+ nonConflictingChanges: Object.keys(filteredPendingChanges),
29
+ table,
30
+ recordId
31
+ };
32
+ }
33
+ const {
34
+ data: auditLogs,
35
+ error
36
+ } = await supabase.schema("core").from("AuditLog").select("oldRecord, newRecord, changeBy, changeAt").eq("tableName", table).eq("recordId_text", recordId).order("changeAt", {
37
+ ascending: false
38
+ }).limit(20);
39
+ if (error) {
40
+ console.warn("[detectConflicts] Failed to query AuditLog:", error);
41
+ return {
42
+ hasConflict: false,
43
+ conflicts: [],
44
+ nonConflictingChanges: Object.keys(filteredPendingChanges),
45
+ table,
46
+ recordId
47
+ };
48
+ }
49
+ const serverChanges = /* @__PURE__ */ new Map();
50
+ for (const log of auditLogs ?? []) {
51
+ const oldRec = log.oldRecord;
52
+ const newRec = log.newRecord;
53
+ if (!oldRec || !newRec) continue;
54
+ for (const [field, newValue] of Object.entries(newRec)) {
55
+ if (ignoredFields.has(field)) continue;
56
+ if (oldRec[field] !== newValue && !serverChanges.has(field)) {
57
+ serverChanges.set(field, {
58
+ newValue,
59
+ changedBy: log.changeBy,
60
+ changedAt: new Date(log.changeAt)
61
+ });
62
+ }
63
+ }
64
+ }
65
+ const conflicts = [];
66
+ const nonConflictingChanges = [];
67
+ for (const [field, localValue] of Object.entries(filteredPendingChanges)) {
68
+ if (serverChanges.has(field)) {
69
+ const serverChange = serverChanges.get(field);
70
+ conflicts.push({
71
+ field,
72
+ localValue,
73
+ serverValue: serverChange.newValue,
74
+ changedBy: serverChange.changedBy,
75
+ changedAt: serverChange.changedAt
76
+ });
77
+ } else {
78
+ nonConflictingChanges.push(field);
79
+ }
80
+ }
81
+ return {
82
+ hasConflict: conflicts.length > 0,
83
+ conflicts,
84
+ nonConflictingChanges,
85
+ table,
86
+ recordId
87
+ };
88
+ }
89
+ async function hasVersionColumn(table, db) {
90
+ try {
91
+ validateTableName(table);
92
+ const result = await db.getAll(`PRAGMA table_info("${table}")`);
93
+ return result.some((col) => col.name === "_version");
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+ async function fetchServerVersion(table, recordId, schema, supabase) {
99
+ const query = schema === "public" ? supabase.from(table) : supabase.schema(schema).from(table);
100
+ const {
101
+ data,
102
+ error
103
+ } = await query.select("_version").eq("id", recordId).single();
104
+ if (error || !data) {
105
+ return null;
106
+ }
107
+ return data._version ?? null;
108
+ }
109
+ async function getLocalVersion(table, recordId, db) {
110
+ validateTableName(table);
111
+ const result = await db.get(`SELECT _version FROM "${table}" WHERE id = ?`, [recordId]);
112
+ return result?._version ?? null;
113
+ }
114
+
115
+ // src/connector/supabase-connector.ts
116
+ var SupabaseConnector = class {
117
+ supabase;
118
+ powerSyncUrl;
119
+ schemaRouter;
120
+ crudHandler;
121
+ logger;
122
+ onTransactionSuccess;
123
+ onTransactionFailure;
124
+ onTransactionComplete;
125
+ shouldUploadFn;
126
+ // Conflict detection configuration
127
+ conflictDetection;
128
+ conflictHandler;
129
+ conflictBus;
130
+ // Cache for version column existence checks (table -> hasVersionColumn)
131
+ versionColumnCache = /* @__PURE__ */ new Map();
132
+ // Active project IDs for scoped sync (optional feature)
133
+ activeProjectIds = [];
134
+ // Store resolutions for retry - when PowerSync retries, we apply stored resolutions
135
+ // instead of re-detecting conflicts that the user already resolved
136
+ resolvedConflicts = /* @__PURE__ */ new Map();
137
+ // Cleanup function for resolution listener subscription
138
+ unsubscribeResolution;
139
+ constructor(options) {
140
+ this.supabase = options.supabaseClient;
141
+ this.powerSyncUrl = options.powerSyncUrl;
142
+ this.schemaRouter = options.schemaRouter ?? defaultSchemaRouter;
143
+ this.crudHandler = options.crudHandler;
144
+ this.logger = options.logger;
145
+ this.onTransactionSuccess = options.onTransactionSuccess;
146
+ this.onTransactionFailure = options.onTransactionFailure;
147
+ this.onTransactionComplete = options.onTransactionComplete;
148
+ this.shouldUploadFn = options.shouldUpload;
149
+ this.conflictDetection = options.conflictDetection;
150
+ this.conflictHandler = options.conflictHandler;
151
+ this.conflictBus = options.conflictBus;
152
+ if (this.conflictBus) {
153
+ this.unsubscribeResolution = this.conflictBus.onResolution((table, recordId, resolution) => {
154
+ const key = `${table}:${recordId}`;
155
+ if (__DEV__) {
156
+ console.log("[Connector] Storing resolution for retry:", {
157
+ table,
158
+ recordId,
159
+ key,
160
+ resolution
161
+ });
162
+ }
163
+ this.resolvedConflicts.set(key, resolution);
164
+ });
165
+ }
166
+ }
167
+ /**
168
+ * Clean up resources (unsubscribe from event listeners).
169
+ * Call this when the connector is no longer needed.
170
+ */
171
+ destroy() {
172
+ if (this.unsubscribeResolution) {
173
+ this.unsubscribeResolution();
174
+ this.unsubscribeResolution = void 0;
175
+ }
176
+ this.resolvedConflicts.clear();
177
+ }
178
+ /**
179
+ * Set the active project IDs for scoped sync.
180
+ * Call this when user selects/opens projects.
181
+ */
182
+ setActiveProjectIds(projectIds) {
183
+ this.activeProjectIds = projectIds;
184
+ }
185
+ /**
186
+ * Get the current active project IDs.
187
+ */
188
+ getActiveProjectIds() {
189
+ return this.activeProjectIds;
190
+ }
191
+ /**
192
+ * Get credentials for PowerSync connection.
193
+ * Uses Supabase session token.
194
+ *
195
+ * Note: Token refresh is handled by Supabase's startAutoRefresh() which must be
196
+ * called on app initialization. getSession() returns the auto-refreshed token.
197
+ */
198
+ async fetchCredentials() {
199
+ this.logger?.debug("[Connector] Fetching credentials...");
200
+ const {
201
+ data: {
202
+ session
203
+ },
204
+ error
205
+ } = await this.supabase.auth.getSession();
206
+ if (error) {
207
+ this.logger?.error("[Connector] Auth error:", error);
208
+ throw new Error(`Failed to get Supabase session: ${error.message}`);
209
+ }
210
+ if (!session) {
211
+ this.logger?.error("[Connector] No active session");
212
+ throw new Error("No active Supabase session");
213
+ }
214
+ this.logger?.debug("[Connector] Credentials fetched, token expires at:", session.expires_at);
215
+ return {
216
+ endpoint: this.powerSyncUrl,
217
+ token: session.access_token,
218
+ expiresAt: session.expires_at ? new Date(session.expires_at * 1e3) : void 0
219
+ };
220
+ }
221
+ /**
222
+ * Upload local changes to Supabase.
223
+ * Called automatically by PowerSync when there are pending uploads.
224
+ *
225
+ * When conflict detection is enabled:
226
+ * 1. Checks if table has _version column (cached)
227
+ * 2. If yes, compares local vs server version
228
+ * 3. On version mismatch, queries AuditLog for field conflicts
229
+ * 4. If conflicts found, calls handler or publishes to conflict bus
230
+ * 5. Applies resolution or skips entry based on handler response
231
+ */
232
+ async uploadData(database) {
233
+ if (this.shouldUploadFn && !this.shouldUploadFn()) {
234
+ if (__DEV__) {
235
+ console.log("[Connector] Upload skipped - sync mode does not allow uploads");
236
+ }
237
+ this.logger?.debug("[Connector] Upload skipped - sync mode does not allow uploads");
238
+ return;
239
+ }
240
+ if (__DEV__) {
241
+ console.log("[Connector] uploadData called, fetching next CRUD transaction...");
242
+ }
243
+ const transaction = await database.getNextCrudTransaction();
244
+ if (!transaction) {
245
+ if (__DEV__) {
246
+ console.log("[Connector] No pending CRUD transaction found");
247
+ }
248
+ return;
249
+ }
250
+ if (__DEV__) {
251
+ console.log("[Connector] Transaction fetched:", {
252
+ crudCount: transaction.crud.length,
253
+ entries: transaction.crud.map((e) => ({
254
+ table: e.table,
255
+ op: e.op,
256
+ id: e.id,
257
+ opDataKeys: e.opData ? Object.keys(e.opData) : []
258
+ }))
259
+ });
260
+ }
261
+ const conflictDetectionEnabled = this.conflictDetection?.enabled !== false;
262
+ if (!conflictDetectionEnabled) {
263
+ await this.processTransaction(transaction, database);
264
+ return;
265
+ }
266
+ const {
267
+ crud
268
+ } = transaction;
269
+ const skipTables = new Set(this.conflictDetection?.skipTables ?? []);
270
+ const entriesToProcess = [];
271
+ const entriesQueuedForUI = [];
272
+ const entriesDiscarded = [];
273
+ const partialResolutions = [];
274
+ for (const entry of crud) {
275
+ if (entry.op === "DELETE") {
276
+ entriesToProcess.push(entry);
277
+ continue;
278
+ }
279
+ if (skipTables.has(entry.table)) {
280
+ entriesToProcess.push(entry);
281
+ continue;
282
+ }
283
+ const resolutionKey = `${entry.table}:${entry.id}`;
284
+ const existingResolution = this.resolvedConflicts.get(resolutionKey);
285
+ if (existingResolution) {
286
+ if (__DEV__) {
287
+ console.log("[Connector] Applying stored resolution for retry:", {
288
+ table: entry.table,
289
+ id: entry.id,
290
+ key: resolutionKey,
291
+ resolution: existingResolution
292
+ });
293
+ }
294
+ this.resolvedConflicts.delete(resolutionKey);
295
+ switch (existingResolution.action) {
296
+ case "overwrite":
297
+ entriesToProcess.push(entry);
298
+ break;
299
+ case "keep-server":
300
+ entriesDiscarded.push(entry);
301
+ break;
302
+ case "partial":
303
+ const partialEntry = {
304
+ ...entry,
305
+ opData: this.filterFields(entry.opData ?? {}, existingResolution.fields)
306
+ };
307
+ entriesToProcess.push(partialEntry);
308
+ break;
309
+ }
310
+ continue;
311
+ }
312
+ const hasVersion = await this.checkVersionColumn(entry.table, database);
313
+ if (!hasVersion) {
314
+ entriesToProcess.push(entry);
315
+ continue;
316
+ }
317
+ const localVersion = await getLocalVersion(entry.table, entry.id, database);
318
+ const schema = this.schemaRouter(entry.table);
319
+ const serverVersion = await fetchServerVersion(entry.table, entry.id, schema, this.supabase);
320
+ if (localVersion === null || serverVersion === null) {
321
+ entriesToProcess.push(entry);
322
+ continue;
323
+ }
324
+ const conflictResult = await detectConflicts(entry.table, entry.id, localVersion, serverVersion, entry.opData ?? {}, this.supabase, this.conflictDetection);
325
+ if (!conflictResult.hasConflict) {
326
+ entriesToProcess.push(entry);
327
+ continue;
328
+ }
329
+ this.conflictBus?.emitConflict(conflictResult);
330
+ if (this.conflictHandler) {
331
+ const resolution = await this.conflictHandler.onConflict(conflictResult);
332
+ if (resolution === null) {
333
+ entriesQueuedForUI.push(entry);
334
+ if (__DEV__) {
335
+ console.log("[Connector] Conflict queued for UI resolution:", {
336
+ table: entry.table,
337
+ id: entry.id,
338
+ conflicts: conflictResult.conflicts.map((c) => c.field)
339
+ });
340
+ }
341
+ continue;
342
+ }
343
+ switch (resolution.action) {
344
+ case "overwrite":
345
+ entriesToProcess.push(entry);
346
+ break;
347
+ case "keep-server":
348
+ entriesDiscarded.push(entry);
349
+ if (__DEV__) {
350
+ console.log("[Connector] Conflict resolved with keep-server, discarding local changes:", {
351
+ table: entry.table,
352
+ id: entry.id
353
+ });
354
+ }
355
+ break;
356
+ case "partial":
357
+ const partialEntry = {
358
+ ...entry,
359
+ opData: this.filterFields(entry.opData ?? {}, resolution.fields)
360
+ };
361
+ entriesToProcess.push(partialEntry);
362
+ partialResolutions.push({
363
+ originalConflict: conflictResult,
364
+ syncedFields: resolution.fields
365
+ });
366
+ break;
367
+ }
368
+ } else {
369
+ console.warn("[Connector] Conflict detected but no handler:", {
370
+ table: entry.table,
371
+ id: entry.id,
372
+ conflicts: conflictResult.conflicts
373
+ });
374
+ entriesToProcess.push(entry);
375
+ }
376
+ }
377
+ if (entriesQueuedForUI.length > 0) {
378
+ if (__DEV__) {
379
+ console.log("[Connector] Entries queued for UI resolution, leaving in queue:", {
380
+ queuedForUI: entriesQueuedForUI.length,
381
+ discarded: entriesDiscarded.length,
382
+ toProcess: entriesToProcess.length
383
+ });
384
+ }
385
+ this.onTransactionComplete?.(entriesQueuedForUI);
386
+ return;
387
+ }
388
+ if (entriesToProcess.length === 0 && entriesDiscarded.length > 0) {
389
+ if (__DEV__) {
390
+ console.log("[Connector] All entries resolved with keep-server, completing transaction to discard local changes");
391
+ }
392
+ try {
393
+ await transaction.complete();
394
+ this.onTransactionSuccess?.(entriesDiscarded);
395
+ this.onTransactionComplete?.(entriesDiscarded);
396
+ } catch (error) {
397
+ const classified = classifySupabaseError(error);
398
+ this.onTransactionFailure?.(entriesDiscarded, error instanceof Error ? error : new Error(String(error)), classified);
399
+ throw error;
400
+ }
401
+ return;
402
+ }
403
+ try {
404
+ for (const entry of entriesToProcess) {
405
+ if (__DEV__) {
406
+ console.log("[Connector] Processing CRUD entry:", {
407
+ table: entry.table,
408
+ op: entry.op,
409
+ id: entry.id,
410
+ opData: entry.opData
411
+ });
412
+ }
413
+ await this.processCrudEntry(entry);
414
+ }
415
+ if (__DEV__) {
416
+ console.log("[Connector] All CRUD entries processed, completing transaction...");
417
+ }
418
+ await transaction.complete();
419
+ if (__DEV__) {
420
+ console.log("[Connector] Transaction completed successfully:", {
421
+ entriesCount: entriesToProcess.length,
422
+ discardedCount: entriesDiscarded.length
423
+ });
424
+ }
425
+ if (this.conflictBus && partialResolutions.length > 0) {
426
+ for (const {
427
+ originalConflict,
428
+ syncedFields
429
+ } of partialResolutions) {
430
+ const syncedFieldSet = new Set(syncedFields);
431
+ const remainingConflicts = originalConflict.conflicts.filter((c) => !syncedFieldSet.has(c.field));
432
+ if (remainingConflicts.length > 0) {
433
+ if (__DEV__) {
434
+ console.log("[Connector] Re-emitting conflict for remaining fields:", {
435
+ table: originalConflict.table,
436
+ recordId: originalConflict.recordId,
437
+ syncedFields,
438
+ remainingConflictFields: remainingConflicts.map((c) => c.field)
439
+ });
440
+ }
441
+ this.conflictBus.emitConflict({
442
+ ...originalConflict,
443
+ conflicts: remainingConflicts,
444
+ // All remaining are conflicts now - clear nonConflictingChanges since
445
+ // the non-conflicting ones were already synced in the partial resolution
446
+ nonConflictingChanges: []
447
+ });
448
+ }
449
+ }
450
+ }
451
+ this.onTransactionSuccess?.(entriesToProcess);
452
+ this.onTransactionComplete?.(entriesToProcess);
453
+ } catch (error) {
454
+ const classified = classifySupabaseError(error);
455
+ console.error("[PowerSync Connector] Upload FAILED:", {
456
+ errorMessage: error instanceof Error ? error.message : String(error),
457
+ errorKeys: error && typeof error === "object" ? Object.keys(error) : [],
458
+ errorObject: JSON.stringify(error, null, 2),
459
+ classified,
460
+ isPermanent: classified.isPermanent,
461
+ entries: entriesToProcess.map((e) => ({
462
+ table: e.table,
463
+ op: e.op,
464
+ id: e.id
465
+ }))
466
+ });
467
+ this.logger?.error("[Connector] Upload error:", {
468
+ error,
469
+ classified,
470
+ entries: entriesToProcess.map((e) => ({
471
+ table: e.table,
472
+ op: e.op,
473
+ id: e.id
474
+ }))
475
+ });
476
+ this.onTransactionFailure?.(entriesToProcess, error instanceof Error ? error : new Error(String(error)), classified);
477
+ throw error;
478
+ }
479
+ }
480
+ /**
481
+ * Process a transaction without conflict detection.
482
+ * Used when conflict detection is disabled.
483
+ */
484
+ async processTransaction(transaction, _database) {
485
+ try {
486
+ for (const entry of transaction.crud) {
487
+ if (__DEV__) {
488
+ console.log("[Connector] Processing CRUD entry:", {
489
+ table: entry.table,
490
+ op: entry.op,
491
+ id: entry.id,
492
+ opData: entry.opData
493
+ });
494
+ }
495
+ await this.processCrudEntry(entry);
496
+ }
497
+ if (__DEV__) {
498
+ console.log("[Connector] All CRUD entries processed, completing transaction...");
499
+ }
500
+ await transaction.complete();
501
+ if (__DEV__) {
502
+ console.log("[Connector] Transaction completed successfully:", {
503
+ entriesCount: transaction.crud.length
504
+ });
505
+ }
506
+ this.onTransactionSuccess?.(transaction.crud);
507
+ this.onTransactionComplete?.(transaction.crud);
508
+ } catch (error) {
509
+ const classified = classifySupabaseError(error);
510
+ console.error("[PowerSync Connector] Upload FAILED:", {
511
+ errorMessage: error instanceof Error ? error.message : String(error),
512
+ errorKeys: error && typeof error === "object" ? Object.keys(error) : [],
513
+ errorObject: JSON.stringify(error, null, 2),
514
+ classified,
515
+ isPermanent: classified.isPermanent,
516
+ entries: transaction.crud.map((e) => ({
517
+ table: e.table,
518
+ op: e.op,
519
+ id: e.id
520
+ }))
521
+ });
522
+ this.logger?.error("[Connector] Upload error:", {
523
+ error,
524
+ classified,
525
+ entries: transaction.crud.map((e) => ({
526
+ table: e.table,
527
+ op: e.op,
528
+ id: e.id
529
+ }))
530
+ });
531
+ this.onTransactionFailure?.(transaction.crud, error instanceof Error ? error : new Error(String(error)), classified);
532
+ throw error;
533
+ }
534
+ }
535
+ /**
536
+ * Check if a table has a _version column (cached).
537
+ */
538
+ async checkVersionColumn(table, db) {
539
+ if (this.versionColumnCache.has(table)) {
540
+ return this.versionColumnCache.get(table);
541
+ }
542
+ const hasVersion = await hasVersionColumn(table, db);
543
+ this.versionColumnCache.set(table, hasVersion);
544
+ return hasVersion;
545
+ }
546
+ /**
547
+ * Filter opData to only include specified fields.
548
+ * Used for partial sync resolution.
549
+ */
550
+ filterFields(opData, fields) {
551
+ const fieldSet = new Set(fields);
552
+ const filtered = {};
553
+ for (const [key, value] of Object.entries(opData)) {
554
+ if (fieldSet.has(key)) {
555
+ filtered[key] = value;
556
+ }
557
+ }
558
+ return filtered;
559
+ }
560
+ /**
561
+ * Process a single CRUD operation.
562
+ *
563
+ * UUID-native tables (public schema, post-migration) use `id` as the UUID column.
564
+ * Core schema tables (Profile, Comment, CommentSection) still use a separate `uuid` column.
565
+ */
566
+ /**
567
+ * Process a single CRUD operation.
568
+ *
569
+ * All synced tables use `id` as their UUID primary key column.
570
+ */
571
+ async processCrudEntry(entry) {
572
+ const table = entry.table;
573
+ const id = entry.id;
574
+ const schema = this.schemaRouter(table);
575
+ if (this.crudHandler) {
576
+ let handled = false;
577
+ switch (entry.op) {
578
+ case "PUT" /* PUT */:
579
+ handled = await this.crudHandler.handlePut?.(entry, this.supabase, schema) ?? false;
580
+ break;
581
+ case "PATCH" /* PATCH */:
582
+ handled = await this.crudHandler.handlePatch?.(entry, this.supabase, schema) ?? false;
583
+ break;
584
+ case "DELETE" /* DELETE */:
585
+ handled = await this.crudHandler.handleDelete?.(entry, this.supabase, schema) ?? false;
586
+ break;
587
+ }
588
+ if (handled) {
589
+ this.logger?.debug(`[Connector] Custom handler processed ${entry.op} for ${schema}.${table}`);
590
+ return;
591
+ }
592
+ }
593
+ const query = schema === "public" ? this.supabase.from(table) : this.supabase.schema(schema).from(table);
594
+ switch (entry.op) {
595
+ case "PUT" /* PUT */:
596
+ if (__DEV__) {
597
+ console.log("[Connector] Executing PUT/UPSERT:", {
598
+ schema,
599
+ table,
600
+ id,
601
+ data: {
602
+ id,
603
+ ...entry.opData
604
+ }
605
+ });
606
+ }
607
+ const {
608
+ data: upsertData,
609
+ error: upsertError
610
+ } = await query.upsert({
611
+ id,
612
+ ...entry.opData
613
+ }, {
614
+ onConflict: "id"
615
+ }).select();
616
+ if (upsertError) {
617
+ if (__DEV__) {
618
+ console.error("[Connector] PUT/UPSERT FAILED:", {
619
+ schema,
620
+ table,
621
+ id,
622
+ error: upsertError,
623
+ errorMessage: upsertError.message,
624
+ errorCode: upsertError.code,
625
+ errorDetails: upsertError.details,
626
+ errorHint: upsertError.hint
627
+ });
628
+ }
629
+ throw new Error(`Upsert failed for ${schema}.${table}: ${upsertError.message}`);
630
+ }
631
+ if (__DEV__) {
632
+ console.log("[Connector] PUT/UPSERT SUCCESS:", {
633
+ schema,
634
+ table,
635
+ id,
636
+ responseData: upsertData
637
+ });
638
+ }
639
+ break;
640
+ case "PATCH" /* PATCH */:
641
+ if (__DEV__) {
642
+ console.log("[Connector] Executing PATCH/UPDATE:", {
643
+ schema,
644
+ table,
645
+ id,
646
+ opData: entry.opData
647
+ });
648
+ }
649
+ const {
650
+ data: updateData,
651
+ error: updateError
652
+ } = await query.update(entry.opData).eq("id", id).select();
653
+ if (updateError) {
654
+ if (__DEV__) {
655
+ console.error("[Connector] PATCH/UPDATE FAILED:", {
656
+ schema,
657
+ table,
658
+ id,
659
+ error: updateError,
660
+ errorMessage: updateError.message,
661
+ errorCode: updateError.code,
662
+ errorDetails: updateError.details,
663
+ errorHint: updateError.hint
664
+ });
665
+ }
666
+ throw new Error(`Update failed for ${schema}.${table}: ${updateError.message}`);
667
+ }
668
+ if (__DEV__) {
669
+ console.log("[Connector] PATCH/UPDATE SUCCESS:", {
670
+ schema,
671
+ table,
672
+ id,
673
+ responseData: updateData
674
+ });
675
+ }
676
+ break;
677
+ case "DELETE" /* DELETE */:
678
+ if (__DEV__) {
679
+ console.log("[Connector] Executing DELETE:", {
680
+ schema,
681
+ table,
682
+ id
683
+ });
684
+ }
685
+ const {
686
+ data: deleteData,
687
+ error: deleteError
688
+ } = await query.delete().eq("id", id).select();
689
+ if (deleteError) {
690
+ if (__DEV__) {
691
+ console.error("[Connector] DELETE FAILED:", {
692
+ schema,
693
+ table,
694
+ id,
695
+ error: deleteError,
696
+ errorMessage: deleteError.message,
697
+ errorCode: deleteError.code,
698
+ errorDetails: deleteError.details,
699
+ errorHint: deleteError.hint
700
+ });
701
+ }
702
+ throw new Error(`Delete failed for ${schema}.${table}: ${deleteError.message}`);
703
+ }
704
+ if (__DEV__) {
705
+ console.log("[Connector] DELETE SUCCESS:", {
706
+ schema,
707
+ table,
708
+ id,
709
+ responseData: deleteData
710
+ });
711
+ }
712
+ break;
713
+ }
714
+ this.logger?.debug(`[Connector] Processed ${entry.op} for ${schema}.${table} (id: ${id})`);
715
+ }
716
+ };
717
+
718
+ export {
719
+ defaultSchemaRouter,
720
+ detectConflicts,
721
+ hasVersionColumn,
722
+ fetchServerVersion,
723
+ getLocalVersion,
724
+ SupabaseConnector
725
+ };
726
+ //# sourceMappingURL=chunk-C2RSTGDC.js.map