@basictech/react 0.7.0-beta.5 → 0.7.0-beta.7
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/.turbo/turbo-build.log +10 -10
- package/changelog.md +12 -0
- package/dist/index.d.mts +266 -13
- package/dist/index.d.ts +266 -13
- package/dist/index.js +645 -184
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +625 -172
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/readme.md +203 -209
- package/src/AuthContext.tsx +197 -54
- package/src/core/db/RemoteCollection.ts +294 -0
- package/src/core/db/RemoteDB.ts +40 -0
- package/src/core/db/index.ts +7 -0
- package/src/core/db/types.ts +128 -0
- package/src/index.ts +25 -9
- package/src/sync/index.ts +133 -54
- package/src/db.ts +0 -55
package/dist/index.mjs
CHANGED
|
@@ -1,135 +1,181 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// src/sync/syncProtocol.js
|
|
12
|
-
import { Dexie } from "dexie";
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
13
10
|
|
|
14
11
|
// src/config.ts
|
|
15
|
-
var log
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
var log;
|
|
13
|
+
var init_config = __esm({
|
|
14
|
+
"src/config.ts"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
log = (...args) => {
|
|
17
|
+
try {
|
|
18
|
+
if (localStorage.getItem("basic_debug") === "true") {
|
|
19
|
+
console.log("[basic]", ...args);
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
}
|
|
23
|
+
};
|
|
21
24
|
}
|
|
22
|
-
};
|
|
25
|
+
});
|
|
23
26
|
|
|
24
27
|
// src/sync/syncProtocol.js
|
|
25
|
-
var
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
log("Opening socket - sending clientIdentity", context.clientIdentity);
|
|
50
|
-
ws.send(
|
|
51
|
-
JSON.stringify({
|
|
52
|
-
type: "clientIdentity",
|
|
53
|
-
clientIdentity: context.clientIdentity || null,
|
|
54
|
-
authToken: options.authToken,
|
|
55
|
-
schema: options.schema
|
|
56
|
-
})
|
|
57
|
-
);
|
|
58
|
-
};
|
|
59
|
-
ws.onerror = function(event) {
|
|
60
|
-
ws.close();
|
|
61
|
-
log("ws.onerror", event);
|
|
62
|
-
onError(event?.message, RECONNECT_DELAY);
|
|
63
|
-
};
|
|
64
|
-
ws.onclose = function(event) {
|
|
65
|
-
onError("Socket closed: " + event.reason, RECONNECT_DELAY);
|
|
66
|
-
};
|
|
67
|
-
var isFirstRound = true;
|
|
68
|
-
ws.onmessage = function(event) {
|
|
69
|
-
try {
|
|
70
|
-
var requestFromServer = JSON.parse(event.data);
|
|
71
|
-
log("requestFromServer", requestFromServer, { acceptCallback, isFirstRound });
|
|
72
|
-
if (requestFromServer.type == "clientIdentity") {
|
|
73
|
-
context.clientIdentity = requestFromServer.clientIdentity;
|
|
74
|
-
context.save();
|
|
75
|
-
sendChanges(changes, baseRevision, partial, onChangesAccepted);
|
|
28
|
+
var syncProtocol_exports = {};
|
|
29
|
+
__export(syncProtocol_exports, {
|
|
30
|
+
syncProtocol: () => syncProtocol
|
|
31
|
+
});
|
|
32
|
+
import { Dexie } from "dexie";
|
|
33
|
+
var syncProtocol;
|
|
34
|
+
var init_syncProtocol = __esm({
|
|
35
|
+
"src/sync/syncProtocol.js"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
"use client";
|
|
38
|
+
init_config();
|
|
39
|
+
syncProtocol = function() {
|
|
40
|
+
log("Initializing syncProtocol");
|
|
41
|
+
var RECONNECT_DELAY = 5e3;
|
|
42
|
+
Dexie.Syncable.registerSyncProtocol("websocket", {
|
|
43
|
+
sync: function(context, url, options, baseRevision, syncedRevision, changes, partial, applyRemoteChanges, onChangesAccepted, onSuccess, onError) {
|
|
44
|
+
var requestId = 0;
|
|
45
|
+
var acceptCallbacks = {};
|
|
46
|
+
log("Connecting to", url);
|
|
47
|
+
var ws = new WebSocket(url);
|
|
48
|
+
function sendChanges(changes2, baseRevision2, partial2, onChangesAccepted2) {
|
|
49
|
+
log("sendChanges", changes2.length, baseRevision2);
|
|
50
|
+
++requestId;
|
|
51
|
+
acceptCallbacks[requestId.toString()] = onChangesAccepted2;
|
|
76
52
|
ws.send(
|
|
77
53
|
JSON.stringify({
|
|
78
|
-
type: "
|
|
79
|
-
|
|
54
|
+
type: "changes",
|
|
55
|
+
changes: changes2,
|
|
56
|
+
partial: partial2,
|
|
57
|
+
baseRevision: baseRevision2,
|
|
58
|
+
requestId
|
|
80
59
|
})
|
|
81
60
|
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
61
|
+
}
|
|
62
|
+
ws.onopen = function(event) {
|
|
63
|
+
log("Opening socket - sending clientIdentity", context.clientIdentity);
|
|
64
|
+
ws.send(
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
type: "clientIdentity",
|
|
67
|
+
clientIdentity: context.clientIdentity || null,
|
|
68
|
+
authToken: options.authToken,
|
|
69
|
+
schema: options.schema
|
|
70
|
+
})
|
|
87
71
|
);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
72
|
+
};
|
|
73
|
+
ws.onerror = function(event) {
|
|
74
|
+
ws.close();
|
|
75
|
+
log("ws.onerror", event);
|
|
76
|
+
onError(event?.message, RECONNECT_DELAY);
|
|
77
|
+
};
|
|
78
|
+
ws.onclose = function(event) {
|
|
79
|
+
onError("Socket closed: " + event.reason, RECONNECT_DELAY);
|
|
80
|
+
};
|
|
81
|
+
var isFirstRound = true;
|
|
82
|
+
ws.onmessage = function(event) {
|
|
83
|
+
try {
|
|
84
|
+
var requestFromServer = JSON.parse(event.data);
|
|
85
|
+
log("requestFromServer", requestFromServer, { acceptCallback, isFirstRound });
|
|
86
|
+
if (requestFromServer.type == "clientIdentity") {
|
|
87
|
+
context.clientIdentity = requestFromServer.clientIdentity;
|
|
88
|
+
context.save();
|
|
89
|
+
sendChanges(changes, baseRevision, partial, onChangesAccepted);
|
|
90
|
+
ws.send(
|
|
91
|
+
JSON.stringify({
|
|
92
|
+
type: "subscribe",
|
|
93
|
+
syncedRevision
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
} else if (requestFromServer.type == "changes") {
|
|
97
|
+
applyRemoteChanges(
|
|
98
|
+
requestFromServer.changes,
|
|
99
|
+
requestFromServer.currentRevision,
|
|
100
|
+
requestFromServer.partial
|
|
101
|
+
);
|
|
102
|
+
if (isFirstRound && !requestFromServer.partial) {
|
|
103
|
+
onSuccess({
|
|
104
|
+
// Specify a react function that will react on additional client changes
|
|
105
|
+
react: function(changes2, baseRevision2, partial2, onChangesAccepted2) {
|
|
106
|
+
sendChanges(
|
|
107
|
+
changes2,
|
|
108
|
+
baseRevision2,
|
|
109
|
+
partial2,
|
|
110
|
+
onChangesAccepted2
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
// Specify a disconnect function that will close our socket so that we dont continue to monitor changes.
|
|
114
|
+
disconnect: function() {
|
|
115
|
+
ws.close();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
isFirstRound = false;
|
|
102
119
|
}
|
|
103
|
-
})
|
|
104
|
-
|
|
120
|
+
} else if (requestFromServer.type == "ack") {
|
|
121
|
+
var requestId2 = requestFromServer.requestId;
|
|
122
|
+
var acceptCallback = acceptCallbacks[requestId2.toString()];
|
|
123
|
+
acceptCallback();
|
|
124
|
+
delete acceptCallbacks[requestId2.toString()];
|
|
125
|
+
} else if (requestFromServer.type == "error") {
|
|
126
|
+
var requestId2 = requestFromServer.requestId;
|
|
127
|
+
ws.close();
|
|
128
|
+
onError(requestFromServer.message, Infinity);
|
|
129
|
+
} else {
|
|
130
|
+
log("unknown message", requestFromServer);
|
|
131
|
+
ws.close();
|
|
132
|
+
onError("unknown message", Infinity);
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
ws.close();
|
|
136
|
+
log("caught error", e);
|
|
137
|
+
onError(e, Infinity);
|
|
105
138
|
}
|
|
106
|
-
}
|
|
107
|
-
var requestId2 = requestFromServer.requestId;
|
|
108
|
-
var acceptCallback = acceptCallbacks[requestId2.toString()];
|
|
109
|
-
acceptCallback();
|
|
110
|
-
delete acceptCallbacks[requestId2.toString()];
|
|
111
|
-
} else if (requestFromServer.type == "error") {
|
|
112
|
-
var requestId2 = requestFromServer.requestId;
|
|
113
|
-
ws.close();
|
|
114
|
-
onError(requestFromServer.message, Infinity);
|
|
115
|
-
} else {
|
|
116
|
-
log("unknown message", requestFromServer);
|
|
117
|
-
ws.close();
|
|
118
|
-
onError("unknown message", Infinity);
|
|
119
|
-
}
|
|
120
|
-
} catch (e) {
|
|
121
|
-
ws.close();
|
|
122
|
-
log("caught error", e);
|
|
123
|
-
onError(e, Infinity);
|
|
139
|
+
};
|
|
124
140
|
}
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
};
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// src/AuthContext.tsx
|
|
147
|
+
import { createContext, useContext, useEffect, useState, useRef } from "react";
|
|
148
|
+
import { jwtDecode } from "jwt-decode";
|
|
129
149
|
|
|
130
150
|
// src/sync/index.ts
|
|
151
|
+
init_config();
|
|
152
|
+
import { v7 as uuidv7 } from "uuid";
|
|
153
|
+
import { Dexie as Dexie2 } from "dexie";
|
|
131
154
|
import { validateData } from "@basictech/schema";
|
|
132
|
-
|
|
155
|
+
var dexieExtensionsLoaded = false;
|
|
156
|
+
var initPromise = null;
|
|
157
|
+
async function initDexieExtensions() {
|
|
158
|
+
if (dexieExtensionsLoaded)
|
|
159
|
+
return;
|
|
160
|
+
if (typeof window === "undefined")
|
|
161
|
+
return;
|
|
162
|
+
if (initPromise)
|
|
163
|
+
return initPromise;
|
|
164
|
+
initPromise = (async () => {
|
|
165
|
+
try {
|
|
166
|
+
await import("dexie-syncable");
|
|
167
|
+
await import("dexie-observable");
|
|
168
|
+
const { syncProtocol: syncProtocol2 } = await Promise.resolve().then(() => (init_syncProtocol(), syncProtocol_exports));
|
|
169
|
+
syncProtocol2();
|
|
170
|
+
dexieExtensionsLoaded = true;
|
|
171
|
+
log("Dexie extensions loaded successfully");
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error("Failed to load Dexie extensions:", error);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
})();
|
|
177
|
+
return initPromise;
|
|
178
|
+
}
|
|
133
179
|
var BasicSync = class extends Dexie2 {
|
|
134
180
|
basic_schema;
|
|
135
181
|
constructor(name, options) {
|
|
@@ -195,63 +241,388 @@ var BasicSync = class extends Dexie2 {
|
|
|
195
241
|
return this.syncable;
|
|
196
242
|
}
|
|
197
243
|
collection(name) {
|
|
244
|
+
if (this.basic_schema?.tables && !this.basic_schema.tables[name]) {
|
|
245
|
+
throw new Error(`Table "${name}" not found in schema`);
|
|
246
|
+
}
|
|
247
|
+
const table = this.table(name);
|
|
198
248
|
return {
|
|
199
249
|
/**
|
|
200
250
|
* Returns the underlying Dexie table
|
|
201
251
|
* @type {Dexie.Table}
|
|
202
252
|
*/
|
|
203
|
-
ref:
|
|
253
|
+
ref: table,
|
|
204
254
|
// --- WRITE ---- //
|
|
205
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Add a new record - returns the full object with generated id
|
|
257
|
+
*/
|
|
258
|
+
add: async (data) => {
|
|
206
259
|
const valid = validateData(this.basic_schema, name, data);
|
|
207
260
|
if (!valid.valid) {
|
|
208
261
|
log("Invalid data", valid);
|
|
209
|
-
|
|
262
|
+
throw new Error(valid.message || "Data validation failed");
|
|
210
263
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
264
|
+
const id = uuidv7();
|
|
265
|
+
const fullData = { id, ...data };
|
|
266
|
+
await table.add(fullData);
|
|
267
|
+
return fullData;
|
|
215
268
|
},
|
|
216
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Put (upsert) a record - returns the full object
|
|
271
|
+
*/
|
|
272
|
+
put: async (data) => {
|
|
273
|
+
if (!data.id) {
|
|
274
|
+
throw new Error("put() requires an id field");
|
|
275
|
+
}
|
|
217
276
|
const valid = validateData(this.basic_schema, name, data);
|
|
218
277
|
if (!valid.valid) {
|
|
219
278
|
log("Invalid data", valid);
|
|
220
|
-
|
|
279
|
+
throw new Error(valid.message || "Data validation failed");
|
|
221
280
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
...data
|
|
225
|
-
});
|
|
281
|
+
await table.put(data);
|
|
282
|
+
return data;
|
|
226
283
|
},
|
|
227
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Update an existing record - returns updated object or null
|
|
286
|
+
*/
|
|
287
|
+
update: async (id, data) => {
|
|
288
|
+
if (!id) {
|
|
289
|
+
throw new Error("update() requires an id");
|
|
290
|
+
}
|
|
228
291
|
const valid = validateData(this.basic_schema, name, data, false);
|
|
229
292
|
if (!valid.valid) {
|
|
230
293
|
log("Invalid data", valid);
|
|
231
|
-
|
|
294
|
+
throw new Error(valid.message || "Data validation failed");
|
|
232
295
|
}
|
|
233
|
-
|
|
296
|
+
const updated = await table.update(id, data);
|
|
297
|
+
if (updated === 0) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const record = await table.get(id);
|
|
301
|
+
return record || null;
|
|
234
302
|
},
|
|
235
|
-
|
|
236
|
-
|
|
303
|
+
/**
|
|
304
|
+
* Delete a record - returns true if deleted, false if not found
|
|
305
|
+
*/
|
|
306
|
+
delete: async (id) => {
|
|
307
|
+
if (!id) {
|
|
308
|
+
throw new Error("delete() requires an id");
|
|
309
|
+
}
|
|
310
|
+
const exists = await table.get(id);
|
|
311
|
+
if (!exists) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
await table.delete(id);
|
|
315
|
+
return true;
|
|
237
316
|
},
|
|
238
317
|
// --- READ ---- //
|
|
318
|
+
/**
|
|
319
|
+
* Get a single record by id - returns null if not found
|
|
320
|
+
*/
|
|
239
321
|
get: async (id) => {
|
|
240
|
-
|
|
322
|
+
if (!id) {
|
|
323
|
+
throw new Error("get() requires an id");
|
|
324
|
+
}
|
|
325
|
+
const record = await table.get(id);
|
|
326
|
+
return record || null;
|
|
241
327
|
},
|
|
328
|
+
/**
|
|
329
|
+
* Get all records in the collection
|
|
330
|
+
*/
|
|
242
331
|
getAll: async () => {
|
|
243
|
-
return
|
|
332
|
+
return table.toArray();
|
|
244
333
|
},
|
|
245
334
|
// --- QUERY ---- //
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Filter records using a predicate function
|
|
337
|
+
*/
|
|
338
|
+
filter: async (fn) => {
|
|
339
|
+
return table.filter(fn).toArray();
|
|
340
|
+
},
|
|
341
|
+
/**
|
|
342
|
+
* Get the raw Dexie table for advanced queries
|
|
343
|
+
* @deprecated Use ref instead
|
|
344
|
+
*/
|
|
345
|
+
query: () => table
|
|
249
346
|
};
|
|
250
347
|
}
|
|
251
348
|
};
|
|
252
349
|
|
|
350
|
+
// src/core/db/types.ts
|
|
351
|
+
var RemoteDBError = class extends Error {
|
|
352
|
+
status;
|
|
353
|
+
response;
|
|
354
|
+
constructor(message, status, response) {
|
|
355
|
+
super(message);
|
|
356
|
+
this.name = "RemoteDBError";
|
|
357
|
+
this.status = status;
|
|
358
|
+
this.response = response;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/core/db/RemoteCollection.ts
|
|
363
|
+
import { validateData as validateData2 } from "@basictech/schema";
|
|
364
|
+
var NotAuthenticatedError = class extends Error {
|
|
365
|
+
constructor(message = "Not authenticated") {
|
|
366
|
+
super(message);
|
|
367
|
+
this.name = "NotAuthenticatedError";
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
var RemoteCollection = class {
|
|
371
|
+
tableName;
|
|
372
|
+
config;
|
|
373
|
+
constructor(tableName, config) {
|
|
374
|
+
this.tableName = tableName;
|
|
375
|
+
this.config = config;
|
|
376
|
+
}
|
|
377
|
+
log(...args) {
|
|
378
|
+
if (this.config.debug) {
|
|
379
|
+
console.log("[RemoteDB]", ...args);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Check if an error is a "not authenticated" error
|
|
384
|
+
*/
|
|
385
|
+
isNotAuthenticatedError(error) {
|
|
386
|
+
if (error instanceof Error) {
|
|
387
|
+
const message = error.message.toLowerCase();
|
|
388
|
+
return message.includes("no token") || message.includes("not authenticated") || message.includes("please sign in");
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Helper to make authenticated API requests
|
|
394
|
+
* Automatically retries once on 401 (token expired) by refreshing the token
|
|
395
|
+
*/
|
|
396
|
+
async request(method, path, body, isRetry = false) {
|
|
397
|
+
const token = await this.config.getToken();
|
|
398
|
+
const url = `${this.config.serverUrl}${path}`;
|
|
399
|
+
this.log(`${method} ${url}`, body ? JSON.stringify(body) : "");
|
|
400
|
+
const response = await fetch(url, {
|
|
401
|
+
method,
|
|
402
|
+
headers: {
|
|
403
|
+
"Content-Type": "application/json",
|
|
404
|
+
"Authorization": `Bearer ${token}`
|
|
405
|
+
},
|
|
406
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
407
|
+
});
|
|
408
|
+
const responseData = await response.json().catch(() => ({}));
|
|
409
|
+
if (!response.ok) {
|
|
410
|
+
if (response.status === 401 && !isRetry) {
|
|
411
|
+
this.log("Got 401, retrying with fresh token...");
|
|
412
|
+
return this.request(method, path, body, true);
|
|
413
|
+
}
|
|
414
|
+
if (this.config.debug) {
|
|
415
|
+
console.error(`[RemoteDB] Error ${response.status}:`, responseData);
|
|
416
|
+
}
|
|
417
|
+
if (response.status === 401 && this.config.onAuthError) {
|
|
418
|
+
this.config.onAuthError({
|
|
419
|
+
status: response.status,
|
|
420
|
+
message: "Authentication failed",
|
|
421
|
+
response: responseData
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
const errorMessage = responseData.message || responseData.error || responseData.detail || (typeof responseData === "string" ? responseData : `API request failed: ${response.status}`);
|
|
425
|
+
throw new RemoteDBError(errorMessage, response.status, responseData);
|
|
426
|
+
}
|
|
427
|
+
this.log("Response:", responseData);
|
|
428
|
+
return responseData;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Validate data against schema if available
|
|
432
|
+
*/
|
|
433
|
+
validateData(data, checkRequired = true) {
|
|
434
|
+
if (this.config.schema) {
|
|
435
|
+
const result = validateData2(this.config.schema, this.tableName, data, checkRequired);
|
|
436
|
+
if (!result.valid) {
|
|
437
|
+
throw new Error(result.message || "Data validation failed");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get the base path for this collection
|
|
443
|
+
*/
|
|
444
|
+
get basePath() {
|
|
445
|
+
return `/account/${this.config.projectId}/db/${this.tableName}`;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Add a new record to the collection
|
|
449
|
+
* The server generates the ID
|
|
450
|
+
* Requires authentication - throws NotAuthenticatedError if not signed in
|
|
451
|
+
*/
|
|
452
|
+
async add(data) {
|
|
453
|
+
this.validateData(data, true);
|
|
454
|
+
try {
|
|
455
|
+
const result = await this.request(
|
|
456
|
+
"POST",
|
|
457
|
+
this.basePath,
|
|
458
|
+
{ value: data }
|
|
459
|
+
);
|
|
460
|
+
return result.data;
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (this.isNotAuthenticatedError(error)) {
|
|
463
|
+
throw new NotAuthenticatedError("Sign in required to add items");
|
|
464
|
+
}
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Put (upsert) a record - requires id
|
|
470
|
+
* Requires authentication - throws NotAuthenticatedError if not signed in
|
|
471
|
+
*/
|
|
472
|
+
async put(data) {
|
|
473
|
+
if (!data.id) {
|
|
474
|
+
throw new Error("put() requires an id field");
|
|
475
|
+
}
|
|
476
|
+
const { id, ...rest } = data;
|
|
477
|
+
this.validateData(rest, true);
|
|
478
|
+
try {
|
|
479
|
+
const result = await this.request(
|
|
480
|
+
"PUT",
|
|
481
|
+
`${this.basePath}/${id}`,
|
|
482
|
+
{ value: rest }
|
|
483
|
+
);
|
|
484
|
+
return result.data || data;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (this.isNotAuthenticatedError(error)) {
|
|
487
|
+
throw new NotAuthenticatedError("Sign in required to update items");
|
|
488
|
+
}
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Update an existing record by id
|
|
494
|
+
* Requires authentication - throws NotAuthenticatedError if not signed in
|
|
495
|
+
*/
|
|
496
|
+
async update(id, data) {
|
|
497
|
+
if (!id) {
|
|
498
|
+
throw new Error("update() requires an id");
|
|
499
|
+
}
|
|
500
|
+
this.validateData(data, false);
|
|
501
|
+
try {
|
|
502
|
+
const result = await this.request(
|
|
503
|
+
"PATCH",
|
|
504
|
+
`${this.basePath}/${id}`,
|
|
505
|
+
{ value: data }
|
|
506
|
+
);
|
|
507
|
+
return result.data || null;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (error instanceof RemoteDBError && error.status === 404) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
if (this.isNotAuthenticatedError(error)) {
|
|
513
|
+
throw new NotAuthenticatedError("Sign in required to update items");
|
|
514
|
+
}
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Delete a record by id
|
|
520
|
+
* Requires authentication - throws NotAuthenticatedError if not signed in
|
|
521
|
+
*/
|
|
522
|
+
async delete(id) {
|
|
523
|
+
if (!id) {
|
|
524
|
+
throw new Error("delete() requires an id");
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
await this.request(
|
|
528
|
+
"DELETE",
|
|
529
|
+
`${this.basePath}/${id}`
|
|
530
|
+
);
|
|
531
|
+
return true;
|
|
532
|
+
} catch (error) {
|
|
533
|
+
if (error instanceof RemoteDBError && error.status === 404) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
if (this.isNotAuthenticatedError(error)) {
|
|
537
|
+
throw new NotAuthenticatedError("Sign in required to delete items");
|
|
538
|
+
}
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Get a single record by id
|
|
544
|
+
* Returns null if not authenticated (graceful degradation for read operations)
|
|
545
|
+
*/
|
|
546
|
+
async get(id) {
|
|
547
|
+
if (!id) {
|
|
548
|
+
throw new Error("get() requires an id");
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
const result = await this.request(
|
|
552
|
+
"GET",
|
|
553
|
+
`${this.basePath}?id=${id}`
|
|
554
|
+
);
|
|
555
|
+
return result.data?.[0] || null;
|
|
556
|
+
} catch (error) {
|
|
557
|
+
if (this.isNotAuthenticatedError(error)) {
|
|
558
|
+
this.log("Not authenticated - returning null for get()");
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Get all records in the collection
|
|
565
|
+
* Returns empty array if not authenticated (graceful degradation for read operations)
|
|
566
|
+
*/
|
|
567
|
+
async getAll() {
|
|
568
|
+
try {
|
|
569
|
+
const result = await this.request(
|
|
570
|
+
"GET",
|
|
571
|
+
this.basePath
|
|
572
|
+
);
|
|
573
|
+
return result.data || [];
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (this.isNotAuthenticatedError(error)) {
|
|
576
|
+
this.log("Not authenticated - returning empty array for getAll()");
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Filter records using a predicate function
|
|
584
|
+
* Note: This fetches all records and filters client-side
|
|
585
|
+
* Returns empty array if not authenticated (graceful degradation for read operations)
|
|
586
|
+
*/
|
|
587
|
+
async filter(fn) {
|
|
588
|
+
const all = await this.getAll();
|
|
589
|
+
return all.filter(fn);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* ref is not available for remote collections
|
|
593
|
+
*/
|
|
594
|
+
ref = void 0;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// src/core/db/RemoteDB.ts
|
|
598
|
+
var RemoteDB = class {
|
|
599
|
+
config;
|
|
600
|
+
collections = /* @__PURE__ */ new Map();
|
|
601
|
+
constructor(config) {
|
|
602
|
+
this.config = config;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Get a collection by name
|
|
606
|
+
* Collections are cached for reuse
|
|
607
|
+
*/
|
|
608
|
+
collection(name) {
|
|
609
|
+
if (this.collections.has(name)) {
|
|
610
|
+
return this.collections.get(name);
|
|
611
|
+
}
|
|
612
|
+
if (this.config.schema?.tables && !this.config.schema.tables[name]) {
|
|
613
|
+
throw new Error(`Table "${name}" not found in schema`);
|
|
614
|
+
}
|
|
615
|
+
const collection = new RemoteCollection(name, this.config);
|
|
616
|
+
this.collections.set(name, collection);
|
|
617
|
+
return collection;
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// src/AuthContext.tsx
|
|
622
|
+
init_config();
|
|
623
|
+
|
|
253
624
|
// package.json
|
|
254
|
-
var version = "0.7.0-beta.
|
|
625
|
+
var version = "0.7.0-beta.6";
|
|
255
626
|
|
|
256
627
|
// src/updater/versionUpdater.ts
|
|
257
628
|
var VersionUpdater = class {
|
|
@@ -433,6 +804,7 @@ function clearCookie(name) {
|
|
|
433
804
|
}
|
|
434
805
|
|
|
435
806
|
// src/utils/network.ts
|
|
807
|
+
init_config();
|
|
436
808
|
function isDevelopment(debug) {
|
|
437
809
|
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname.includes("localhost") || window.location.hostname.includes("127.0.0.1") || window.location.hostname.includes(".local") || process.env.NODE_ENV === "development" || debug === true;
|
|
438
810
|
}
|
|
@@ -495,6 +867,7 @@ function getSyncStatus(statusCode) {
|
|
|
495
867
|
}
|
|
496
868
|
|
|
497
869
|
// src/utils/schema.ts
|
|
870
|
+
init_config();
|
|
498
871
|
import { validateSchema as validateSchema2, compareSchemas } from "@basictech/schema";
|
|
499
872
|
async function getSchemaStatus(schema) {
|
|
500
873
|
const projectId = schema.project_id;
|
|
@@ -598,29 +971,44 @@ var DEFAULT_AUTH_CONFIG = {
|
|
|
598
971
|
server_url: "https://api.basic.tech",
|
|
599
972
|
ws_url: "wss://pds.basic.id/ws"
|
|
600
973
|
};
|
|
974
|
+
var noDb = {
|
|
975
|
+
collection: () => {
|
|
976
|
+
throw new Error("no basicdb found - initialization failed. double check your schema.");
|
|
977
|
+
}
|
|
978
|
+
};
|
|
601
979
|
var BasicContext = createContext({
|
|
602
|
-
|
|
603
|
-
|
|
980
|
+
// Auth state
|
|
981
|
+
isReady: false,
|
|
604
982
|
isSignedIn: false,
|
|
605
983
|
user: null,
|
|
606
|
-
|
|
984
|
+
// Auth actions
|
|
985
|
+
signIn: () => Promise.resolve(),
|
|
986
|
+
signOut: () => Promise.resolve(),
|
|
987
|
+
signInWithCode: () => Promise.resolve({ success: false }),
|
|
988
|
+
// Token management
|
|
989
|
+
getToken: () => Promise.reject(new Error("no token")),
|
|
990
|
+
getSignInUrl: () => Promise.resolve(""),
|
|
991
|
+
// DB access
|
|
992
|
+
db: noDb,
|
|
993
|
+
dbStatus: "LOADING" /* LOADING */,
|
|
994
|
+
dbMode: "sync",
|
|
995
|
+
// Legacy aliases
|
|
996
|
+
isAuthReady: false,
|
|
607
997
|
signin: () => Promise.resolve(),
|
|
608
|
-
|
|
609
|
-
}),
|
|
610
|
-
|
|
611
|
-
}),
|
|
612
|
-
getSignInLink: () => Promise.resolve(""),
|
|
613
|
-
db: {},
|
|
614
|
-
dbStatus: "LOADING" /* LOADING */
|
|
998
|
+
signout: () => Promise.resolve(),
|
|
999
|
+
signinWithCode: () => Promise.resolve({ success: false }),
|
|
1000
|
+
getSignInLink: () => Promise.resolve("")
|
|
615
1001
|
});
|
|
616
1002
|
function BasicProvider({
|
|
617
1003
|
children,
|
|
618
|
-
project_id,
|
|
1004
|
+
project_id: project_id_prop,
|
|
619
1005
|
schema,
|
|
620
1006
|
debug = false,
|
|
621
1007
|
storage,
|
|
622
|
-
auth
|
|
1008
|
+
auth,
|
|
1009
|
+
dbMode = "sync"
|
|
623
1010
|
}) {
|
|
1011
|
+
const project_id = schema?.project_id || project_id_prop;
|
|
624
1012
|
const [isAuthReady, setIsAuthReady] = useState(false);
|
|
625
1013
|
const [isSignedIn, setIsSignedIn] = useState(false);
|
|
626
1014
|
const [token, setToken] = useState(null);
|
|
@@ -632,6 +1020,7 @@ function BasicProvider({
|
|
|
632
1020
|
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
633
1021
|
const [pendingRefresh, setPendingRefresh] = useState(false);
|
|
634
1022
|
const syncRef = useRef(null);
|
|
1023
|
+
const remoteDbRef = useRef(null);
|
|
635
1024
|
const storageAdapter = storage || new LocalStorageAdapter();
|
|
636
1025
|
const authConfig = {
|
|
637
1026
|
scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
|
|
@@ -671,9 +1060,10 @@ function BasicProvider({
|
|
|
671
1060
|
};
|
|
672
1061
|
}, [pendingRefresh, token]);
|
|
673
1062
|
useEffect(() => {
|
|
674
|
-
function
|
|
1063
|
+
async function initSyncDb(options) {
|
|
675
1064
|
if (!syncRef.current) {
|
|
676
|
-
log("Initializing Basic DB");
|
|
1065
|
+
log("Initializing Basic Sync DB");
|
|
1066
|
+
await initDexieExtensions();
|
|
677
1067
|
syncRef.current = new BasicSync("basicdb", { schema });
|
|
678
1068
|
syncRef.current.syncable.on("statusChanged", (status, url) => {
|
|
679
1069
|
setDbStatus(getSyncStatus(status));
|
|
@@ -686,6 +1076,33 @@ function BasicProvider({
|
|
|
686
1076
|
setIsReady(true);
|
|
687
1077
|
}
|
|
688
1078
|
}
|
|
1079
|
+
function initRemoteDb() {
|
|
1080
|
+
if (!remoteDbRef.current) {
|
|
1081
|
+
if (!project_id) {
|
|
1082
|
+
setError({
|
|
1083
|
+
code: "missing_project_id",
|
|
1084
|
+
title: "Project ID Required",
|
|
1085
|
+
message: "Remote mode requires a project_id. Provide it via schema.project_id or the project_id prop."
|
|
1086
|
+
});
|
|
1087
|
+
setIsReady(true);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
log("Initializing Basic Remote DB");
|
|
1091
|
+
remoteDbRef.current = new RemoteDB({
|
|
1092
|
+
serverUrl: authConfig.server_url,
|
|
1093
|
+
projectId: project_id,
|
|
1094
|
+
getToken,
|
|
1095
|
+
schema,
|
|
1096
|
+
debug,
|
|
1097
|
+
onAuthError: (error2) => {
|
|
1098
|
+
log("RemoteDB auth error:", error2);
|
|
1099
|
+
signout();
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
setDbStatus("ONLINE" /* ONLINE */);
|
|
1103
|
+
setIsReady(true);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
689
1106
|
async function checkSchema() {
|
|
690
1107
|
const result = await validateAndCheckSchema(schema);
|
|
691
1108
|
if (!result.isValid) {
|
|
@@ -704,18 +1121,26 @@ function BasicProvider({
|
|
|
704
1121
|
setIsReady(true);
|
|
705
1122
|
return null;
|
|
706
1123
|
}
|
|
707
|
-
if (
|
|
708
|
-
|
|
1124
|
+
if (dbMode === "remote") {
|
|
1125
|
+
initRemoteDb();
|
|
709
1126
|
} else {
|
|
710
|
-
|
|
711
|
-
|
|
1127
|
+
if (result.schemaStatus.valid) {
|
|
1128
|
+
await initSyncDb({ shouldConnect: true });
|
|
1129
|
+
} else {
|
|
1130
|
+
log("Schema is invalid!", result.schemaStatus);
|
|
1131
|
+
await initSyncDb({ shouldConnect: false });
|
|
1132
|
+
}
|
|
712
1133
|
}
|
|
713
1134
|
checkForNewVersion();
|
|
714
1135
|
}
|
|
715
1136
|
if (schema) {
|
|
716
1137
|
checkSchema();
|
|
717
1138
|
} else {
|
|
718
|
-
|
|
1139
|
+
if (dbMode === "remote" && project_id) {
|
|
1140
|
+
initRemoteDb();
|
|
1141
|
+
} else {
|
|
1142
|
+
setIsReady(true);
|
|
1143
|
+
}
|
|
719
1144
|
}
|
|
720
1145
|
}, []);
|
|
721
1146
|
useEffect(() => {
|
|
@@ -862,8 +1287,14 @@ function BasicProvider({
|
|
|
862
1287
|
const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
|
|
863
1288
|
if (isExpired) {
|
|
864
1289
|
log("token is expired - refreshing ...");
|
|
1290
|
+
const refreshToken = token?.refresh_token;
|
|
1291
|
+
if (!refreshToken) {
|
|
1292
|
+
log("Error: No refresh token available for expired token");
|
|
1293
|
+
setIsAuthReady(true);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
865
1296
|
try {
|
|
866
|
-
const newToken = await fetchToken(
|
|
1297
|
+
const newToken = await fetchToken(refreshToken, true);
|
|
867
1298
|
fetchUser(newToken?.access_token || "");
|
|
868
1299
|
} catch (error2) {
|
|
869
1300
|
log("Failed to refresh token in checkToken:", error2);
|
|
@@ -1070,6 +1501,11 @@ function BasicProvider({
|
|
|
1070
1501
|
return token?.access_token || "";
|
|
1071
1502
|
};
|
|
1072
1503
|
const fetchToken = async (codeOrRefreshToken, isRefreshToken = false) => {
|
|
1504
|
+
if (!codeOrRefreshToken || codeOrRefreshToken.trim() === "") {
|
|
1505
|
+
const errorMsg = isRefreshToken ? "Refresh token is empty or undefined" : "Authorization code is empty or undefined";
|
|
1506
|
+
log("Error:", errorMsg);
|
|
1507
|
+
throw new Error(errorMsg);
|
|
1508
|
+
}
|
|
1073
1509
|
if (isRefreshToken && refreshPromiseRef.current) {
|
|
1074
1510
|
log("Reusing in-flight refresh token request");
|
|
1075
1511
|
return refreshPromiseRef.current;
|
|
@@ -1182,24 +1618,36 @@ function BasicProvider({
|
|
|
1182
1618
|
}
|
|
1183
1619
|
return refreshPromise;
|
|
1184
1620
|
};
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1621
|
+
const getCurrentDb = () => {
|
|
1622
|
+
if (dbMode === "remote") {
|
|
1623
|
+
return remoteDbRef.current || noDb;
|
|
1188
1624
|
}
|
|
1625
|
+
return syncRef.current || noDb;
|
|
1189
1626
|
};
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
isAuthReady,
|
|
1627
|
+
const contextValue = {
|
|
1628
|
+
// Auth state (new naming)
|
|
1629
|
+
isReady: isAuthReady,
|
|
1193
1630
|
isSignedIn,
|
|
1194
1631
|
user,
|
|
1195
|
-
|
|
1632
|
+
// Auth actions (new camelCase naming)
|
|
1633
|
+
signIn: signin,
|
|
1634
|
+
signOut: signout,
|
|
1635
|
+
signInWithCode: signinWithCode,
|
|
1636
|
+
// Token management
|
|
1637
|
+
getToken,
|
|
1638
|
+
getSignInUrl: getSignInLink,
|
|
1639
|
+
// DB access
|
|
1640
|
+
db: getCurrentDb(),
|
|
1641
|
+
dbStatus,
|
|
1642
|
+
dbMode,
|
|
1643
|
+
// Legacy aliases (deprecated)
|
|
1644
|
+
isAuthReady,
|
|
1196
1645
|
signin,
|
|
1646
|
+
signout,
|
|
1197
1647
|
signinWithCode,
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
dbStatus
|
|
1202
|
-
}, children: [
|
|
1648
|
+
getSignInLink
|
|
1649
|
+
};
|
|
1650
|
+
return /* @__PURE__ */ jsxs(BasicContext.Provider, { value: contextValue, children: [
|
|
1203
1651
|
error && isDevMode() && /* @__PURE__ */ jsx(ErrorDisplay, { error }),
|
|
1204
1652
|
isReady && children
|
|
1205
1653
|
] });
|
|
@@ -1235,6 +1683,11 @@ function useBasic() {
|
|
|
1235
1683
|
import { useLiveQuery as useQuery } from "dexie-react-hooks";
|
|
1236
1684
|
export {
|
|
1237
1685
|
BasicProvider,
|
|
1686
|
+
NotAuthenticatedError,
|
|
1687
|
+
RemoteCollection,
|
|
1688
|
+
RemoteDB,
|
|
1689
|
+
RemoteDBError,
|
|
1690
|
+
STORAGE_KEYS,
|
|
1238
1691
|
useBasic,
|
|
1239
1692
|
useQuery
|
|
1240
1693
|
};
|