@basictech/react 0.7.0-beta.6 → 0.7.0

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/index.js CHANGED
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
6
11
  var __export = (target, all) => {
7
12
  for (var name in all)
8
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -15,12 +20,160 @@ var __copyProps = (to, from, except, desc) => {
15
20
  }
16
21
  return to;
17
22
  };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
18
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
32
 
33
+ // src/config.ts
34
+ var log;
35
+ var init_config = __esm({
36
+ "src/config.ts"() {
37
+ "use strict";
38
+ log = (...args) => {
39
+ try {
40
+ if (localStorage.getItem("basic_debug") === "true") {
41
+ console.log("[basic]", ...args);
42
+ }
43
+ } catch (e) {
44
+ }
45
+ };
46
+ }
47
+ });
48
+
49
+ // src/sync/syncProtocol.js
50
+ var syncProtocol_exports = {};
51
+ __export(syncProtocol_exports, {
52
+ syncProtocol: () => syncProtocol
53
+ });
54
+ var import_dexie, syncProtocol;
55
+ var init_syncProtocol = __esm({
56
+ "src/sync/syncProtocol.js"() {
57
+ "use strict";
58
+ "use client";
59
+ import_dexie = require("dexie");
60
+ init_config();
61
+ syncProtocol = function() {
62
+ log("Initializing syncProtocol");
63
+ var RECONNECT_DELAY = 5e3;
64
+ import_dexie.Dexie.Syncable.registerSyncProtocol("websocket", {
65
+ sync: function(context, url, options, baseRevision, syncedRevision, changes, partial, applyRemoteChanges, onChangesAccepted, onSuccess, onError) {
66
+ var requestId = 0;
67
+ var acceptCallbacks = {};
68
+ log("Connecting to", url);
69
+ var ws = new WebSocket(url);
70
+ function sendChanges(changes2, baseRevision2, partial2, onChangesAccepted2) {
71
+ log("sendChanges", changes2.length, baseRevision2);
72
+ ++requestId;
73
+ acceptCallbacks[requestId.toString()] = onChangesAccepted2;
74
+ ws.send(
75
+ JSON.stringify({
76
+ type: "changes",
77
+ changes: changes2,
78
+ partial: partial2,
79
+ baseRevision: baseRevision2,
80
+ requestId
81
+ })
82
+ );
83
+ }
84
+ ws.onopen = function(event) {
85
+ log("Opening socket - sending clientIdentity", context.clientIdentity);
86
+ ws.send(
87
+ JSON.stringify({
88
+ type: "clientIdentity",
89
+ clientIdentity: context.clientIdentity || null,
90
+ authToken: options.authToken,
91
+ schema: options.schema
92
+ })
93
+ );
94
+ };
95
+ ws.onerror = function(event) {
96
+ ws.close();
97
+ log("ws.onerror", event);
98
+ onError(event?.message, RECONNECT_DELAY);
99
+ };
100
+ ws.onclose = function(event) {
101
+ onError("Socket closed: " + event.reason, RECONNECT_DELAY);
102
+ };
103
+ var isFirstRound = true;
104
+ ws.onmessage = function(event) {
105
+ try {
106
+ var requestFromServer = JSON.parse(event.data);
107
+ log("requestFromServer", requestFromServer, { acceptCallback, isFirstRound });
108
+ if (requestFromServer.type == "clientIdentity") {
109
+ context.clientIdentity = requestFromServer.clientIdentity;
110
+ context.save();
111
+ sendChanges(changes, baseRevision, partial, onChangesAccepted);
112
+ ws.send(
113
+ JSON.stringify({
114
+ type: "subscribe",
115
+ syncedRevision
116
+ })
117
+ );
118
+ } else if (requestFromServer.type == "changes") {
119
+ applyRemoteChanges(
120
+ requestFromServer.changes,
121
+ requestFromServer.currentRevision,
122
+ requestFromServer.partial
123
+ );
124
+ if (isFirstRound && !requestFromServer.partial) {
125
+ onSuccess({
126
+ // Specify a react function that will react on additional client changes
127
+ react: function(changes2, baseRevision2, partial2, onChangesAccepted2) {
128
+ sendChanges(
129
+ changes2,
130
+ baseRevision2,
131
+ partial2,
132
+ onChangesAccepted2
133
+ );
134
+ },
135
+ // Specify a disconnect function that will close our socket so that we dont continue to monitor changes.
136
+ disconnect: function() {
137
+ ws.close();
138
+ }
139
+ });
140
+ isFirstRound = false;
141
+ }
142
+ } else if (requestFromServer.type == "ack") {
143
+ var requestId2 = requestFromServer.requestId;
144
+ var acceptCallback = acceptCallbacks[requestId2.toString()];
145
+ acceptCallback();
146
+ delete acceptCallbacks[requestId2.toString()];
147
+ } else if (requestFromServer.type == "error") {
148
+ var requestId2 = requestFromServer.requestId;
149
+ ws.close();
150
+ onError(requestFromServer.message, Infinity);
151
+ } else {
152
+ log("unknown message", requestFromServer);
153
+ ws.close();
154
+ onError("unknown message", Infinity);
155
+ }
156
+ } catch (e) {
157
+ ws.close();
158
+ log("caught error", e);
159
+ onError(e, Infinity);
160
+ }
161
+ };
162
+ }
163
+ });
164
+ };
165
+ }
166
+ });
167
+
20
168
  // src/index.ts
21
169
  var src_exports = {};
22
170
  __export(src_exports, {
23
171
  BasicProvider: () => BasicProvider,
172
+ NotAuthenticatedError: () => NotAuthenticatedError,
173
+ RemoteCollection: () => RemoteCollection,
174
+ RemoteDB: () => RemoteDB,
175
+ RemoteDBError: () => RemoteDBError,
176
+ STORAGE_KEYS: () => STORAGE_KEYS,
24
177
  useBasic: () => useBasic,
25
178
  useQuery: () => import_dexie_react_hooks.useLiveQuery
26
179
  });
@@ -33,131 +186,32 @@ var import_jwt_decode = require("jwt-decode");
33
186
  // src/sync/index.ts
34
187
  var import_uuid = require("uuid");
35
188
  var import_dexie2 = require("dexie");
36
- var import_dexie_syncable = require("dexie-syncable");
37
- var import_dexie_observable = require("dexie-observable");
38
-
39
- // src/sync/syncProtocol.js
40
- var import_dexie = require("dexie");
41
-
42
- // src/config.ts
43
- var log = (...args) => {
44
- try {
45
- if (localStorage.getItem("basic_debug") === "true") {
46
- console.log("[basic]", ...args);
47
- }
48
- } catch (e) {
49
- }
50
- };
51
-
52
- // src/sync/syncProtocol.js
53
- var syncProtocol = function() {
54
- log("Initializing syncProtocol");
55
- var RECONNECT_DELAY = 5e3;
56
- import_dexie.Dexie.Syncable.registerSyncProtocol("websocket", {
57
- sync: function(context, url, options, baseRevision, syncedRevision, changes, partial, applyRemoteChanges, onChangesAccepted, onSuccess, onError) {
58
- var requestId = 0;
59
- var acceptCallbacks = {};
60
- log("Connecting to", url);
61
- var ws = new WebSocket(url);
62
- function sendChanges(changes2, baseRevision2, partial2, onChangesAccepted2) {
63
- log("sendChanges", changes2.length, baseRevision2);
64
- ++requestId;
65
- acceptCallbacks[requestId.toString()] = onChangesAccepted2;
66
- ws.send(
67
- JSON.stringify({
68
- type: "changes",
69
- changes: changes2,
70
- partial: partial2,
71
- baseRevision: baseRevision2,
72
- requestId
73
- })
74
- );
75
- }
76
- ws.onopen = function(event) {
77
- log("Opening socket - sending clientIdentity", context.clientIdentity);
78
- ws.send(
79
- JSON.stringify({
80
- type: "clientIdentity",
81
- clientIdentity: context.clientIdentity || null,
82
- authToken: options.authToken,
83
- schema: options.schema
84
- })
85
- );
86
- };
87
- ws.onerror = function(event) {
88
- ws.close();
89
- log("ws.onerror", event);
90
- onError(event?.message, RECONNECT_DELAY);
91
- };
92
- ws.onclose = function(event) {
93
- onError("Socket closed: " + event.reason, RECONNECT_DELAY);
94
- };
95
- var isFirstRound = true;
96
- ws.onmessage = function(event) {
97
- try {
98
- var requestFromServer = JSON.parse(event.data);
99
- log("requestFromServer", requestFromServer, { acceptCallback, isFirstRound });
100
- if (requestFromServer.type == "clientIdentity") {
101
- context.clientIdentity = requestFromServer.clientIdentity;
102
- context.save();
103
- sendChanges(changes, baseRevision, partial, onChangesAccepted);
104
- ws.send(
105
- JSON.stringify({
106
- type: "subscribe",
107
- syncedRevision
108
- })
109
- );
110
- } else if (requestFromServer.type == "changes") {
111
- applyRemoteChanges(
112
- requestFromServer.changes,
113
- requestFromServer.currentRevision,
114
- requestFromServer.partial
115
- );
116
- if (isFirstRound && !requestFromServer.partial) {
117
- onSuccess({
118
- // Specify a react function that will react on additional client changes
119
- react: function(changes2, baseRevision2, partial2, onChangesAccepted2) {
120
- sendChanges(
121
- changes2,
122
- baseRevision2,
123
- partial2,
124
- onChangesAccepted2
125
- );
126
- },
127
- // Specify a disconnect function that will close our socket so that we dont continue to monitor changes.
128
- disconnect: function() {
129
- ws.close();
130
- }
131
- });
132
- isFirstRound = false;
133
- }
134
- } else if (requestFromServer.type == "ack") {
135
- var requestId2 = requestFromServer.requestId;
136
- var acceptCallback = acceptCallbacks[requestId2.toString()];
137
- acceptCallback();
138
- delete acceptCallbacks[requestId2.toString()];
139
- } else if (requestFromServer.type == "error") {
140
- var requestId2 = requestFromServer.requestId;
141
- ws.close();
142
- onError(requestFromServer.message, Infinity);
143
- } else {
144
- log("unknown message", requestFromServer);
145
- ws.close();
146
- onError("unknown message", Infinity);
147
- }
148
- } catch (e) {
149
- ws.close();
150
- log("caught error", e);
151
- onError(e, Infinity);
152
- }
153
- };
154
- }
155
- });
156
- };
157
-
158
- // src/sync/index.ts
189
+ init_config();
159
190
  var import_schema = require("@basictech/schema");
160
- syncProtocol();
191
+ var dexieExtensionsLoaded = false;
192
+ var initPromise = null;
193
+ async function initDexieExtensions() {
194
+ if (dexieExtensionsLoaded)
195
+ return;
196
+ if (typeof window === "undefined")
197
+ return;
198
+ if (initPromise)
199
+ return initPromise;
200
+ initPromise = (async () => {
201
+ try {
202
+ await import("dexie-syncable");
203
+ await import("dexie-observable");
204
+ const { syncProtocol: syncProtocol2 } = await Promise.resolve().then(() => (init_syncProtocol(), syncProtocol_exports));
205
+ syncProtocol2();
206
+ dexieExtensionsLoaded = true;
207
+ log("Dexie extensions loaded successfully");
208
+ } catch (error) {
209
+ console.error("Failed to load Dexie extensions:", error);
210
+ throw error;
211
+ }
212
+ })();
213
+ return initPromise;
214
+ }
161
215
  var BasicSync = class extends import_dexie2.Dexie {
162
216
  basic_schema;
163
217
  constructor(name, options) {
@@ -223,63 +277,388 @@ var BasicSync = class extends import_dexie2.Dexie {
223
277
  return this.syncable;
224
278
  }
225
279
  collection(name) {
280
+ if (this.basic_schema?.tables && !this.basic_schema.tables[name]) {
281
+ throw new Error(`Table "${name}" not found in schema`);
282
+ }
283
+ const table = this.table(name);
226
284
  return {
227
285
  /**
228
286
  * Returns the underlying Dexie table
229
287
  * @type {Dexie.Table}
230
288
  */
231
- ref: this.table(name),
289
+ ref: table,
232
290
  // --- WRITE ---- //
233
- add: (data) => {
291
+ /**
292
+ * Add a new record - returns the full object with generated id
293
+ */
294
+ add: async (data) => {
234
295
  const valid = (0, import_schema.validateData)(this.basic_schema, name, data);
235
296
  if (!valid.valid) {
236
297
  log("Invalid data", valid);
237
- return Promise.reject({ ...valid });
298
+ throw new Error(valid.message || "Data validation failed");
238
299
  }
239
- return this.table(name).add({
240
- id: (0, import_uuid.v7)(),
241
- ...data
242
- });
300
+ const id = (0, import_uuid.v7)();
301
+ const fullData = { id, ...data };
302
+ await table.add(fullData);
303
+ return fullData;
243
304
  },
244
- put: (data) => {
305
+ /**
306
+ * Put (upsert) a record - returns the full object
307
+ */
308
+ put: async (data) => {
309
+ if (!data.id) {
310
+ throw new Error("put() requires an id field");
311
+ }
245
312
  const valid = (0, import_schema.validateData)(this.basic_schema, name, data);
246
313
  if (!valid.valid) {
247
314
  log("Invalid data", valid);
248
- return Promise.reject({ ...valid });
315
+ throw new Error(valid.message || "Data validation failed");
249
316
  }
250
- return this.table(name).put({
251
- id: (0, import_uuid.v7)(),
252
- ...data
253
- });
317
+ await table.put(data);
318
+ return data;
254
319
  },
255
- update: (id, data) => {
320
+ /**
321
+ * Update an existing record - returns updated object or null
322
+ */
323
+ update: async (id, data) => {
324
+ if (!id) {
325
+ throw new Error("update() requires an id");
326
+ }
256
327
  const valid = (0, import_schema.validateData)(this.basic_schema, name, data, false);
257
328
  if (!valid.valid) {
258
329
  log("Invalid data", valid);
259
- return Promise.reject({ ...valid });
330
+ throw new Error(valid.message || "Data validation failed");
331
+ }
332
+ const updated = await table.update(id, data);
333
+ if (updated === 0) {
334
+ return null;
260
335
  }
261
- return this.table(name).update(id, data);
336
+ const record = await table.get(id);
337
+ return record || null;
262
338
  },
263
- delete: (id) => {
264
- return this.table(name).delete(id);
339
+ /**
340
+ * Delete a record - returns true if deleted, false if not found
341
+ */
342
+ delete: async (id) => {
343
+ if (!id) {
344
+ throw new Error("delete() requires an id");
345
+ }
346
+ const exists = await table.get(id);
347
+ if (!exists) {
348
+ return false;
349
+ }
350
+ await table.delete(id);
351
+ return true;
265
352
  },
266
353
  // --- READ ---- //
354
+ /**
355
+ * Get a single record by id - returns null if not found
356
+ */
267
357
  get: async (id) => {
268
- return this.table(name).get(id);
358
+ if (!id) {
359
+ throw new Error("get() requires an id");
360
+ }
361
+ const record = await table.get(id);
362
+ return record || null;
269
363
  },
364
+ /**
365
+ * Get all records in the collection
366
+ */
270
367
  getAll: async () => {
271
- return this.table(name).toArray();
368
+ return table.toArray();
272
369
  },
273
370
  // --- QUERY ---- //
274
- // TODO: lots to do here. simplifing creating querie, filtering/ordering/limit, and execute
275
- query: () => this.table(name),
276
- filter: (fn) => this.table(name).filter(fn).toArray()
371
+ /**
372
+ * Filter records using a predicate function
373
+ */
374
+ filter: async (fn) => {
375
+ return table.filter(fn).toArray();
376
+ },
377
+ /**
378
+ * Get the raw Dexie table for advanced queries
379
+ * @deprecated Use ref instead
380
+ */
381
+ query: () => table
277
382
  };
278
383
  }
279
384
  };
280
385
 
386
+ // src/core/db/types.ts
387
+ var RemoteDBError = class extends Error {
388
+ status;
389
+ response;
390
+ constructor(message, status, response) {
391
+ super(message);
392
+ this.name = "RemoteDBError";
393
+ this.status = status;
394
+ this.response = response;
395
+ }
396
+ };
397
+
398
+ // src/core/db/RemoteCollection.ts
399
+ var import_schema2 = require("@basictech/schema");
400
+ var NotAuthenticatedError = class extends Error {
401
+ constructor(message = "Not authenticated") {
402
+ super(message);
403
+ this.name = "NotAuthenticatedError";
404
+ }
405
+ };
406
+ var RemoteCollection = class {
407
+ tableName;
408
+ config;
409
+ constructor(tableName, config) {
410
+ this.tableName = tableName;
411
+ this.config = config;
412
+ }
413
+ log(...args) {
414
+ if (this.config.debug) {
415
+ console.log("[RemoteDB]", ...args);
416
+ }
417
+ }
418
+ /**
419
+ * Check if an error is a "not authenticated" error
420
+ */
421
+ isNotAuthenticatedError(error) {
422
+ if (error instanceof Error) {
423
+ const message = error.message.toLowerCase();
424
+ return message.includes("no token") || message.includes("not authenticated") || message.includes("please sign in");
425
+ }
426
+ return false;
427
+ }
428
+ /**
429
+ * Helper to make authenticated API requests
430
+ * Automatically retries once on 401 (token expired) by refreshing the token
431
+ */
432
+ async request(method, path, body, isRetry = false) {
433
+ const token = await this.config.getToken();
434
+ const url = `${this.config.serverUrl}${path}`;
435
+ this.log(`${method} ${url}`, body ? JSON.stringify(body) : "");
436
+ const response = await fetch(url, {
437
+ method,
438
+ headers: {
439
+ "Content-Type": "application/json",
440
+ "Authorization": `Bearer ${token}`
441
+ },
442
+ ...body ? { body: JSON.stringify(body) } : {}
443
+ });
444
+ const responseData = await response.json().catch(() => ({}));
445
+ if (!response.ok) {
446
+ if (response.status === 401 && !isRetry) {
447
+ this.log("Got 401, retrying with fresh token...");
448
+ return this.request(method, path, body, true);
449
+ }
450
+ if (this.config.debug) {
451
+ console.error(`[RemoteDB] Error ${response.status}:`, responseData);
452
+ }
453
+ if (response.status === 401 && this.config.onAuthError) {
454
+ this.config.onAuthError({
455
+ status: response.status,
456
+ message: "Authentication failed",
457
+ response: responseData
458
+ });
459
+ }
460
+ const errorMessage = responseData.message || responseData.error || responseData.detail || (typeof responseData === "string" ? responseData : `API request failed: ${response.status}`);
461
+ throw new RemoteDBError(errorMessage, response.status, responseData);
462
+ }
463
+ this.log("Response:", responseData);
464
+ return responseData;
465
+ }
466
+ /**
467
+ * Validate data against schema if available
468
+ */
469
+ validateData(data, checkRequired = true) {
470
+ if (this.config.schema) {
471
+ const result = (0, import_schema2.validateData)(this.config.schema, this.tableName, data, checkRequired);
472
+ if (!result.valid) {
473
+ throw new Error(result.message || "Data validation failed");
474
+ }
475
+ }
476
+ }
477
+ /**
478
+ * Get the base path for this collection
479
+ */
480
+ get basePath() {
481
+ return `/account/${this.config.projectId}/db/${this.tableName}`;
482
+ }
483
+ /**
484
+ * Add a new record to the collection
485
+ * The server generates the ID
486
+ * Requires authentication - throws NotAuthenticatedError if not signed in
487
+ */
488
+ async add(data) {
489
+ this.validateData(data, true);
490
+ try {
491
+ const result = await this.request(
492
+ "POST",
493
+ this.basePath,
494
+ { value: data }
495
+ );
496
+ return result.data;
497
+ } catch (error) {
498
+ if (this.isNotAuthenticatedError(error)) {
499
+ throw new NotAuthenticatedError("Sign in required to add items");
500
+ }
501
+ throw error;
502
+ }
503
+ }
504
+ /**
505
+ * Put (upsert) a record - requires id
506
+ * Requires authentication - throws NotAuthenticatedError if not signed in
507
+ */
508
+ async put(data) {
509
+ if (!data.id) {
510
+ throw new Error("put() requires an id field");
511
+ }
512
+ const { id, ...rest } = data;
513
+ this.validateData(rest, true);
514
+ try {
515
+ const result = await this.request(
516
+ "PUT",
517
+ `${this.basePath}/${id}`,
518
+ { value: rest }
519
+ );
520
+ return result.data || data;
521
+ } catch (error) {
522
+ if (this.isNotAuthenticatedError(error)) {
523
+ throw new NotAuthenticatedError("Sign in required to update items");
524
+ }
525
+ throw error;
526
+ }
527
+ }
528
+ /**
529
+ * Update an existing record by id
530
+ * Requires authentication - throws NotAuthenticatedError if not signed in
531
+ */
532
+ async update(id, data) {
533
+ if (!id) {
534
+ throw new Error("update() requires an id");
535
+ }
536
+ this.validateData(data, false);
537
+ try {
538
+ const result = await this.request(
539
+ "PATCH",
540
+ `${this.basePath}/${id}`,
541
+ { value: data }
542
+ );
543
+ return result.data || null;
544
+ } catch (error) {
545
+ if (error instanceof RemoteDBError && error.status === 404) {
546
+ return null;
547
+ }
548
+ if (this.isNotAuthenticatedError(error)) {
549
+ throw new NotAuthenticatedError("Sign in required to update items");
550
+ }
551
+ throw error;
552
+ }
553
+ }
554
+ /**
555
+ * Delete a record by id
556
+ * Requires authentication - throws NotAuthenticatedError if not signed in
557
+ */
558
+ async delete(id) {
559
+ if (!id) {
560
+ throw new Error("delete() requires an id");
561
+ }
562
+ try {
563
+ await this.request(
564
+ "DELETE",
565
+ `${this.basePath}/${id}`
566
+ );
567
+ return true;
568
+ } catch (error) {
569
+ if (error instanceof RemoteDBError && error.status === 404) {
570
+ return false;
571
+ }
572
+ if (this.isNotAuthenticatedError(error)) {
573
+ throw new NotAuthenticatedError("Sign in required to delete items");
574
+ }
575
+ throw error;
576
+ }
577
+ }
578
+ /**
579
+ * Get a single record by id
580
+ * Returns null if not authenticated (graceful degradation for read operations)
581
+ */
582
+ async get(id) {
583
+ if (!id) {
584
+ throw new Error("get() requires an id");
585
+ }
586
+ try {
587
+ const result = await this.request(
588
+ "GET",
589
+ `${this.basePath}?id=${id}`
590
+ );
591
+ return result.data?.[0] || null;
592
+ } catch (error) {
593
+ if (this.isNotAuthenticatedError(error)) {
594
+ this.log("Not authenticated - returning null for get()");
595
+ }
596
+ return null;
597
+ }
598
+ }
599
+ /**
600
+ * Get all records in the collection
601
+ * Returns empty array if not authenticated (graceful degradation for read operations)
602
+ */
603
+ async getAll() {
604
+ try {
605
+ const result = await this.request(
606
+ "GET",
607
+ this.basePath
608
+ );
609
+ return result.data || [];
610
+ } catch (error) {
611
+ if (this.isNotAuthenticatedError(error)) {
612
+ this.log("Not authenticated - returning empty array for getAll()");
613
+ return [];
614
+ }
615
+ throw error;
616
+ }
617
+ }
618
+ /**
619
+ * Filter records using a predicate function
620
+ * Note: This fetches all records and filters client-side
621
+ * Returns empty array if not authenticated (graceful degradation for read operations)
622
+ */
623
+ async filter(fn) {
624
+ const all = await this.getAll();
625
+ return all.filter(fn);
626
+ }
627
+ /**
628
+ * ref is not available for remote collections
629
+ */
630
+ ref = void 0;
631
+ };
632
+
633
+ // src/core/db/RemoteDB.ts
634
+ var RemoteDB = class {
635
+ config;
636
+ collections = /* @__PURE__ */ new Map();
637
+ constructor(config) {
638
+ this.config = config;
639
+ }
640
+ /**
641
+ * Get a collection by name
642
+ * Collections are cached for reuse
643
+ */
644
+ collection(name) {
645
+ if (this.collections.has(name)) {
646
+ return this.collections.get(name);
647
+ }
648
+ if (this.config.schema?.tables && !this.config.schema.tables[name]) {
649
+ throw new Error(`Table "${name}" not found in schema`);
650
+ }
651
+ const collection = new RemoteCollection(name, this.config);
652
+ this.collections.set(name, collection);
653
+ return collection;
654
+ }
655
+ };
656
+
657
+ // src/AuthContext.tsx
658
+ init_config();
659
+
281
660
  // package.json
282
- var version = "0.7.0-beta.4";
661
+ var version = "0.7.0-beta.6";
283
662
 
284
663
  // src/updater/versionUpdater.ts
285
664
  var VersionUpdater = class {
@@ -461,6 +840,7 @@ function clearCookie(name) {
461
840
  }
462
841
 
463
842
  // src/utils/network.ts
843
+ init_config();
464
844
  function isDevelopment(debug) {
465
845
  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;
466
846
  }
@@ -523,10 +903,11 @@ function getSyncStatus(statusCode) {
523
903
  }
524
904
 
525
905
  // src/utils/schema.ts
526
- var import_schema2 = require("@basictech/schema");
906
+ var import_schema3 = require("@basictech/schema");
907
+ init_config();
527
908
  async function getSchemaStatus(schema) {
528
909
  const projectId = schema.project_id;
529
- const valid = (0, import_schema2.validateSchema)(schema);
910
+ const valid = (0, import_schema3.validateSchema)(schema);
530
911
  if (!valid.valid) {
531
912
  console.warn("BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled");
532
913
  return {
@@ -565,7 +946,7 @@ async function getSchemaStatus(schema) {
565
946
  latest: latestSchema
566
947
  };
567
948
  } else if (latestSchema.version === schema.version) {
568
- const changes = (0, import_schema2.compareSchemas)(schema, latestSchema);
949
+ const changes = (0, import_schema3.compareSchemas)(schema, latestSchema);
569
950
  if (changes.valid) {
570
951
  return {
571
952
  valid: true,
@@ -589,7 +970,7 @@ async function getSchemaStatus(schema) {
589
970
  }
590
971
  }
591
972
  async function validateAndCheckSchema(schema) {
592
- const valid = (0, import_schema2.validateSchema)(schema);
973
+ const valid = (0, import_schema3.validateSchema)(schema);
593
974
  if (!valid.valid) {
594
975
  log("Basic Schema is invalid!", valid.errors);
595
976
  console.group("Schema Errors");
@@ -626,29 +1007,44 @@ var DEFAULT_AUTH_CONFIG = {
626
1007
  server_url: "https://api.basic.tech",
627
1008
  ws_url: "wss://pds.basic.id/ws"
628
1009
  };
1010
+ var noDb = {
1011
+ collection: () => {
1012
+ throw new Error("no basicdb found - initialization failed. double check your schema.");
1013
+ }
1014
+ };
629
1015
  var BasicContext = (0, import_react.createContext)({
630
- unicorn: "\u{1F984}",
631
- isAuthReady: false,
1016
+ // Auth state
1017
+ isReady: false,
632
1018
  isSignedIn: false,
633
1019
  user: null,
634
- signout: () => Promise.resolve(),
1020
+ // Auth actions
1021
+ signIn: () => Promise.resolve(),
1022
+ signOut: () => Promise.resolve(),
1023
+ signInWithCode: () => Promise.resolve({ success: false }),
1024
+ // Token management
1025
+ getToken: () => Promise.reject(new Error("no token")),
1026
+ getSignInUrl: () => Promise.resolve(""),
1027
+ // DB access
1028
+ db: noDb,
1029
+ dbStatus: "LOADING" /* LOADING */,
1030
+ dbMode: "sync",
1031
+ // Legacy aliases
1032
+ isAuthReady: false,
635
1033
  signin: () => Promise.resolve(),
636
- signinWithCode: () => new Promise(() => {
637
- }),
638
- getToken: () => new Promise(() => {
639
- }),
640
- getSignInLink: () => Promise.resolve(""),
641
- db: {},
642
- dbStatus: "LOADING" /* LOADING */
1034
+ signout: () => Promise.resolve(),
1035
+ signinWithCode: () => Promise.resolve({ success: false }),
1036
+ getSignInLink: () => Promise.resolve("")
643
1037
  });
644
1038
  function BasicProvider({
645
1039
  children,
646
- project_id,
1040
+ project_id: project_id_prop,
647
1041
  schema,
648
1042
  debug = false,
649
1043
  storage,
650
- auth
1044
+ auth,
1045
+ dbMode = "sync"
651
1046
  }) {
1047
+ const project_id = schema?.project_id || project_id_prop;
652
1048
  const [isAuthReady, setIsAuthReady] = (0, import_react.useState)(false);
653
1049
  const [isSignedIn, setIsSignedIn] = (0, import_react.useState)(false);
654
1050
  const [token, setToken] = (0, import_react.useState)(null);
@@ -660,6 +1056,7 @@ function BasicProvider({
660
1056
  const [isOnline, setIsOnline] = (0, import_react.useState)(navigator.onLine);
661
1057
  const [pendingRefresh, setPendingRefresh] = (0, import_react.useState)(false);
662
1058
  const syncRef = (0, import_react.useRef)(null);
1059
+ const remoteDbRef = (0, import_react.useRef)(null);
663
1060
  const storageAdapter = storage || new LocalStorageAdapter();
664
1061
  const authConfig = {
665
1062
  scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
@@ -699,9 +1096,10 @@ function BasicProvider({
699
1096
  };
700
1097
  }, [pendingRefresh, token]);
701
1098
  (0, import_react.useEffect)(() => {
702
- function initDb(options) {
1099
+ async function initSyncDb(options) {
703
1100
  if (!syncRef.current) {
704
- log("Initializing Basic DB");
1101
+ log("Initializing Basic Sync DB");
1102
+ await initDexieExtensions();
705
1103
  syncRef.current = new BasicSync("basicdb", { schema });
706
1104
  syncRef.current.syncable.on("statusChanged", (status, url) => {
707
1105
  setDbStatus(getSyncStatus(status));
@@ -714,6 +1112,33 @@ function BasicProvider({
714
1112
  setIsReady(true);
715
1113
  }
716
1114
  }
1115
+ function initRemoteDb() {
1116
+ if (!remoteDbRef.current) {
1117
+ if (!project_id) {
1118
+ setError({
1119
+ code: "missing_project_id",
1120
+ title: "Project ID Required",
1121
+ message: "Remote mode requires a project_id. Provide it via schema.project_id or the project_id prop."
1122
+ });
1123
+ setIsReady(true);
1124
+ return;
1125
+ }
1126
+ log("Initializing Basic Remote DB");
1127
+ remoteDbRef.current = new RemoteDB({
1128
+ serverUrl: authConfig.server_url,
1129
+ projectId: project_id,
1130
+ getToken,
1131
+ schema,
1132
+ debug,
1133
+ onAuthError: (error2) => {
1134
+ log("RemoteDB auth error:", error2);
1135
+ signout();
1136
+ }
1137
+ });
1138
+ setDbStatus("ONLINE" /* ONLINE */);
1139
+ setIsReady(true);
1140
+ }
1141
+ }
717
1142
  async function checkSchema() {
718
1143
  const result = await validateAndCheckSchema(schema);
719
1144
  if (!result.isValid) {
@@ -732,18 +1157,26 @@ function BasicProvider({
732
1157
  setIsReady(true);
733
1158
  return null;
734
1159
  }
735
- if (result.schemaStatus.valid) {
736
- initDb({ shouldConnect: true });
1160
+ if (dbMode === "remote") {
1161
+ initRemoteDb();
737
1162
  } else {
738
- log("Schema is invalid!", result.schemaStatus);
739
- initDb({ shouldConnect: false });
1163
+ if (result.schemaStatus.valid) {
1164
+ await initSyncDb({ shouldConnect: true });
1165
+ } else {
1166
+ log("Schema is invalid!", result.schemaStatus);
1167
+ await initSyncDb({ shouldConnect: false });
1168
+ }
740
1169
  }
741
1170
  checkForNewVersion();
742
1171
  }
743
1172
  if (schema) {
744
1173
  checkSchema();
745
1174
  } else {
746
- setIsReady(true);
1175
+ if (dbMode === "remote" && project_id) {
1176
+ initRemoteDb();
1177
+ } else {
1178
+ setIsReady(true);
1179
+ }
747
1180
  }
748
1181
  }, []);
749
1182
  (0, import_react.useEffect)(() => {
@@ -890,8 +1323,14 @@ function BasicProvider({
890
1323
  const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
891
1324
  if (isExpired) {
892
1325
  log("token is expired - refreshing ...");
1326
+ const refreshToken = token?.refresh_token;
1327
+ if (!refreshToken) {
1328
+ log("Error: No refresh token available for expired token");
1329
+ setIsAuthReady(true);
1330
+ return;
1331
+ }
893
1332
  try {
894
- const newToken = await fetchToken(token?.refresh_token || "", true);
1333
+ const newToken = await fetchToken(refreshToken, true);
895
1334
  fetchUser(newToken?.access_token || "");
896
1335
  } catch (error2) {
897
1336
  log("Failed to refresh token in checkToken:", error2);
@@ -1098,6 +1537,11 @@ function BasicProvider({
1098
1537
  return token?.access_token || "";
1099
1538
  };
1100
1539
  const fetchToken = async (codeOrRefreshToken, isRefreshToken = false) => {
1540
+ if (!codeOrRefreshToken || codeOrRefreshToken.trim() === "") {
1541
+ const errorMsg = isRefreshToken ? "Refresh token is empty or undefined" : "Authorization code is empty or undefined";
1542
+ log("Error:", errorMsg);
1543
+ throw new Error(errorMsg);
1544
+ }
1101
1545
  if (isRefreshToken && refreshPromiseRef.current) {
1102
1546
  log("Reusing in-flight refresh token request");
1103
1547
  return refreshPromiseRef.current;
@@ -1210,24 +1654,36 @@ function BasicProvider({
1210
1654
  }
1211
1655
  return refreshPromise;
1212
1656
  };
1213
- const noDb = {
1214
- collection: () => {
1215
- throw new Error("no basicdb found - initialization failed. double check your schema.");
1657
+ const getCurrentDb = () => {
1658
+ if (dbMode === "remote") {
1659
+ return remoteDbRef.current || noDb;
1216
1660
  }
1661
+ return syncRef.current || noDb;
1217
1662
  };
1218
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(BasicContext.Provider, { value: {
1219
- unicorn: "\u{1F984}",
1220
- isAuthReady,
1663
+ const contextValue = {
1664
+ // Auth state (new naming)
1665
+ isReady: isAuthReady,
1221
1666
  isSignedIn,
1222
1667
  user,
1223
- signout,
1668
+ // Auth actions (new camelCase naming)
1669
+ signIn: signin,
1670
+ signOut: signout,
1671
+ signInWithCode: signinWithCode,
1672
+ // Token management
1673
+ getToken,
1674
+ getSignInUrl: getSignInLink,
1675
+ // DB access
1676
+ db: getCurrentDb(),
1677
+ dbStatus,
1678
+ dbMode,
1679
+ // Legacy aliases (deprecated)
1680
+ isAuthReady,
1224
1681
  signin,
1682
+ signout,
1225
1683
  signinWithCode,
1226
- getToken,
1227
- getSignInLink,
1228
- db: syncRef.current ? syncRef.current : noDb,
1229
- dbStatus
1230
- }, children: [
1684
+ getSignInLink
1685
+ };
1686
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(BasicContext.Provider, { value: contextValue, children: [
1231
1687
  error && isDevMode() && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorDisplay, { error }),
1232
1688
  isReady && children
1233
1689
  ] });
@@ -1264,6 +1720,11 @@ var import_dexie_react_hooks = require("dexie-react-hooks");
1264
1720
  // Annotate the CommonJS export names for ESM import in node:
1265
1721
  0 && (module.exports = {
1266
1722
  BasicProvider,
1723
+ NotAuthenticatedError,
1724
+ RemoteCollection,
1725
+ RemoteDB,
1726
+ RemoteDBError,
1727
+ STORAGE_KEYS,
1267
1728
  useBasic,
1268
1729
  useQuery
1269
1730
  });