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