@basictech/react 0.7.0-beta.0 → 0.7.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +12 -5
- package/dist/index.d.mts +121 -5
- package/dist/index.d.ts +121 -5
- package/dist/index.js +978 -244
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +977 -244
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/readme.md +127 -1
- package/src/AuthContext.tsx +427 -347
- package/src/index.ts +6 -31
- package/src/updater/updateMigrations.ts +22 -0
- package/src/updater/versionUpdater.ts +160 -0
- package/src/utils/network.ts +82 -0
- package/src/utils/schema.ts +120 -0
- package/src/utils/storage.ts +62 -0
package/dist/index.mjs
CHANGED
|
@@ -250,78 +250,607 @@ var BasicSync = class extends Dexie2 {
|
|
|
250
250
|
}
|
|
251
251
|
};
|
|
252
252
|
|
|
253
|
-
// src/
|
|
254
|
-
var
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
253
|
+
// src/db_ts.ts
|
|
254
|
+
var DBError = class extends Error {
|
|
255
|
+
constructor(message, status, response, originalError) {
|
|
256
|
+
super(message);
|
|
257
|
+
this.status = status;
|
|
258
|
+
this.response = response;
|
|
259
|
+
this.originalError = originalError;
|
|
260
|
+
this.name = "DBError";
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
var QueryBuilder = class {
|
|
264
|
+
constructor(tableClient, tableSchema) {
|
|
265
|
+
this.tableClient = tableClient;
|
|
266
|
+
this.tableSchema = tableSchema;
|
|
267
|
+
}
|
|
268
|
+
params = {};
|
|
269
|
+
// Reserved fields that are always allowed
|
|
270
|
+
reservedFields = ["created_at", "updated_at", "id"];
|
|
271
|
+
// Validate field existence in schema
|
|
272
|
+
validateField(field) {
|
|
273
|
+
if (this.tableSchema && !this.reservedFields.includes(field)) {
|
|
274
|
+
if (!this.tableSchema.fields || !(field in this.tableSchema.fields)) {
|
|
275
|
+
throw new Error(`Invalid field: "${field}". Field does not exist in table schema.`);
|
|
276
|
+
}
|
|
260
277
|
}
|
|
261
|
-
}
|
|
262
|
-
|
|
278
|
+
}
|
|
279
|
+
// Validate operator based on field type
|
|
280
|
+
validateOperator(field, operator, value) {
|
|
281
|
+
if (!this.tableSchema || this.reservedFields.includes(field)) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const fieldInfo = this.tableSchema.fields[field];
|
|
285
|
+
if (!fieldInfo)
|
|
286
|
+
return;
|
|
287
|
+
switch (operator) {
|
|
288
|
+
case "gt":
|
|
289
|
+
case "gte":
|
|
290
|
+
case "lt":
|
|
291
|
+
case "lte":
|
|
292
|
+
if (fieldInfo.type !== "number" && fieldInfo.type !== "string") {
|
|
293
|
+
throw new Error(`Operator "${operator}" can only be used with number or string fields. Field "${field}" is type "${fieldInfo.type}".`);
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
case "like":
|
|
297
|
+
case "ilike":
|
|
298
|
+
if (fieldInfo.type !== "string") {
|
|
299
|
+
throw new Error(`Operator "${operator}" can only be used with string fields. Field "${field}" is type "${fieldInfo.type}".`);
|
|
300
|
+
}
|
|
301
|
+
if (typeof value !== "string") {
|
|
302
|
+
throw new Error(`Operator "${operator}" requires a string value. Received: ${typeof value}`);
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
case "in":
|
|
306
|
+
if (!Array.isArray(value)) {
|
|
307
|
+
throw new Error(`Operator "in" requires an array value. Received: ${typeof value}`);
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
case "is":
|
|
311
|
+
if (value !== null && typeof value !== "boolean") {
|
|
312
|
+
throw new Error(`Operator "is" requires null or boolean. Received: ${typeof value}`);
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Add ordering to query with schema validation
|
|
318
|
+
order(field, direction = "asc") {
|
|
319
|
+
this.validateField(field);
|
|
320
|
+
this.params.order = `${field}.${direction}`;
|
|
321
|
+
return this;
|
|
322
|
+
}
|
|
323
|
+
// Add filtering to query
|
|
324
|
+
filter(conditions) {
|
|
325
|
+
if (!this.params.filters) {
|
|
326
|
+
this.params.filters = {};
|
|
327
|
+
}
|
|
328
|
+
for (const [field, condition] of Object.entries(conditions)) {
|
|
329
|
+
this.validateField(field);
|
|
330
|
+
if (condition === null || typeof condition !== "object") {
|
|
331
|
+
this.params.filters[field] = condition;
|
|
332
|
+
} else {
|
|
333
|
+
this.params.filters[field] = condition;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
338
|
+
// Add limit to query
|
|
339
|
+
limit(count) {
|
|
340
|
+
this.params.limit = count;
|
|
341
|
+
return this;
|
|
342
|
+
}
|
|
343
|
+
// Add offset to query for pagination
|
|
344
|
+
offset(count) {
|
|
345
|
+
this.params.offset = count;
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
// Auto-execute when awaited
|
|
349
|
+
then(onfulfilled, onrejected) {
|
|
350
|
+
return this.tableClient.executeQuery(this.params).then(onfulfilled, onrejected);
|
|
351
|
+
}
|
|
352
|
+
// Auto-execute when awaited with catch
|
|
353
|
+
catch(onrejected) {
|
|
354
|
+
return this.tableClient.executeQuery(this.params).catch(onrejected);
|
|
355
|
+
}
|
|
356
|
+
// Auto-execute when awaited with finally
|
|
357
|
+
finally(onfinally) {
|
|
358
|
+
return this.tableClient.executeQuery(this.params).finally(onfinally);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
var TableClient = class {
|
|
362
|
+
constructor(baseUrl, projectId, token, table, getToken, schema) {
|
|
363
|
+
this.baseUrl = baseUrl;
|
|
364
|
+
this.projectId = projectId;
|
|
365
|
+
this.token = token;
|
|
366
|
+
this.table = table;
|
|
367
|
+
this.getToken = getToken;
|
|
368
|
+
this.schema = schema;
|
|
369
|
+
if (schema && schema.tables && schema.tables[table]) {
|
|
370
|
+
this.tableSchema = schema.tables[table];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
tableSchema;
|
|
374
|
+
async headers() {
|
|
375
|
+
const token = await this.getToken();
|
|
376
|
+
return {
|
|
377
|
+
Authorization: `Bearer ${token}`,
|
|
378
|
+
"Content-Type": "application/json"
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async handleRequest(request) {
|
|
382
|
+
try {
|
|
383
|
+
const res = await request;
|
|
384
|
+
if (!res.ok) {
|
|
385
|
+
let errorMessage = `Request failed with status ${res.status}`;
|
|
386
|
+
let errorData;
|
|
387
|
+
try {
|
|
388
|
+
const json2 = await res.json();
|
|
389
|
+
errorData = json2;
|
|
390
|
+
if (json2.error || json2.message) {
|
|
391
|
+
const errorDetails = typeof json2.error === "object" ? JSON.stringify(json2.error) : json2.error;
|
|
392
|
+
const messageDetails = typeof json2.message === "object" ? JSON.stringify(json2.message) : json2.message;
|
|
393
|
+
errorMessage = `${res.status} ${res.statusText}: ${messageDetails || errorDetails || "Unknown error"}`;
|
|
394
|
+
}
|
|
395
|
+
} catch (e) {
|
|
396
|
+
console.log("Failed to parse error response:", e);
|
|
397
|
+
errorMessage = `${res.status} ${res.statusText}`;
|
|
398
|
+
}
|
|
399
|
+
throw new DBError(
|
|
400
|
+
errorMessage,
|
|
401
|
+
res.status,
|
|
402
|
+
errorData
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const json = await res.json();
|
|
406
|
+
return json.data;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.log("Caught error:", error);
|
|
409
|
+
if (error instanceof Error) {
|
|
410
|
+
console.log("Error type:", error.constructor.name);
|
|
411
|
+
console.log("Error stack:", error.stack);
|
|
412
|
+
}
|
|
413
|
+
if (error instanceof DBError) {
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
if (error instanceof TypeError && error.message === "Network request failed") {
|
|
417
|
+
throw new DBError(
|
|
418
|
+
"Network request failed. Please check your internet connection and try again.",
|
|
419
|
+
void 0,
|
|
420
|
+
void 0,
|
|
421
|
+
error
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
throw new DBError(
|
|
425
|
+
`Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
426
|
+
void 0,
|
|
427
|
+
void 0,
|
|
428
|
+
error instanceof Error ? error : void 0
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Build query string from query options
|
|
433
|
+
buildQueryParams(query) {
|
|
434
|
+
if (!query)
|
|
435
|
+
return "";
|
|
436
|
+
const params = [];
|
|
437
|
+
if (query.id) {
|
|
438
|
+
params.push(`id=${query.id}`);
|
|
439
|
+
}
|
|
440
|
+
if (query.filters) {
|
|
441
|
+
for (const [field, condition] of Object.entries(query.filters)) {
|
|
442
|
+
this.addFilterParam(params, field, condition);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (query.order) {
|
|
446
|
+
params.push(`order=${query.order}`);
|
|
447
|
+
}
|
|
448
|
+
if (query.limit !== void 0 && query.limit >= 0) {
|
|
449
|
+
params.push(`limit=${query.limit}`);
|
|
450
|
+
}
|
|
451
|
+
if (query.offset !== void 0 && query.offset >= 0) {
|
|
452
|
+
params.push(`offset=${query.offset}`);
|
|
453
|
+
}
|
|
454
|
+
return params.length > 0 ? `?${params.join("&")}` : "";
|
|
455
|
+
}
|
|
456
|
+
// Helper method to build filter parameters
|
|
457
|
+
addFilterParam(params, field, condition, negate = false) {
|
|
458
|
+
if (condition === null || typeof condition !== "object") {
|
|
459
|
+
if (condition === null) {
|
|
460
|
+
params.push(`${field}=${negate ? "not." : ""}is.null`);
|
|
461
|
+
} else if (typeof condition === "boolean") {
|
|
462
|
+
params.push(`${field}=${negate ? "not." : ""}is.${condition}`);
|
|
463
|
+
} else if (typeof condition === "number") {
|
|
464
|
+
params.push(`${field}=${negate ? "not." : ""}eq.${condition}`);
|
|
465
|
+
} else {
|
|
466
|
+
params.push(`${field}=${negate ? "not." : ""}eq.${encodeURIComponent(String(condition))}`);
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const operatorObj = condition;
|
|
471
|
+
if (operatorObj.not) {
|
|
472
|
+
this.addFilterParam(params, field, operatorObj.not, true);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
for (const [op, value] of Object.entries(operatorObj)) {
|
|
476
|
+
if (op === "not")
|
|
477
|
+
continue;
|
|
478
|
+
const operator = op;
|
|
479
|
+
if (value === null) {
|
|
480
|
+
params.push(`${field}=${negate ? "not." : ""}is.null`);
|
|
481
|
+
} else if (operator === "in" && Array.isArray(value)) {
|
|
482
|
+
params.push(`${field}=${negate ? "not." : ""}in.${value.join(",")}`);
|
|
483
|
+
} else if (operator === "is") {
|
|
484
|
+
if (typeof value === "boolean") {
|
|
485
|
+
params.push(`${field}=${negate ? "not." : ""}is.${value}`);
|
|
486
|
+
} else {
|
|
487
|
+
params.push(`${field}=${negate ? "not." : ""}is.null`);
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
const paramValue = typeof value === "string" ? encodeURIComponent(value) : String(value);
|
|
491
|
+
params.push(`${field}=${negate ? "not." : ""}${operator}.${paramValue}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Internal method to execute a query with options
|
|
496
|
+
async executeQuery(options) {
|
|
497
|
+
const params = this.buildQueryParams(options);
|
|
498
|
+
const headers = await this.headers();
|
|
499
|
+
return this.handleRequest(
|
|
500
|
+
fetch(`${this.baseUrl}/account/${this.projectId}/db/${this.table}${params}`, {
|
|
501
|
+
headers
|
|
502
|
+
})
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
// Public method to start building a query
|
|
506
|
+
getAll() {
|
|
507
|
+
return new QueryBuilder(this, this.tableSchema);
|
|
508
|
+
}
|
|
509
|
+
// Get a specific item by ID
|
|
510
|
+
async get(id) {
|
|
511
|
+
const headers = await this.headers();
|
|
512
|
+
return this.handleRequest(
|
|
513
|
+
fetch(`${this.baseUrl}/account/${this.projectId}/db/${this.table}/${id}`, {
|
|
514
|
+
headers
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
async create(value) {
|
|
519
|
+
const headers = await this.headers();
|
|
520
|
+
return this.handleRequest(
|
|
521
|
+
fetch(`${this.baseUrl}/account/${this.projectId}/db/${this.table}`, {
|
|
522
|
+
method: "POST",
|
|
523
|
+
headers,
|
|
524
|
+
body: JSON.stringify({ value })
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
async update(id, value) {
|
|
529
|
+
const headers = await this.headers();
|
|
530
|
+
return this.handleRequest(
|
|
531
|
+
fetch(`${this.baseUrl}/account/${this.projectId}/db/${this.table}/${id}`, {
|
|
532
|
+
method: "PATCH",
|
|
533
|
+
headers,
|
|
534
|
+
body: JSON.stringify({ value })
|
|
535
|
+
})
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
async replace(id, value) {
|
|
539
|
+
const headers = await this.headers();
|
|
540
|
+
return this.handleRequest(
|
|
541
|
+
fetch(`${this.baseUrl}/account/${this.projectId}/db/${this.table}/${id}`, {
|
|
542
|
+
method: "PUT",
|
|
543
|
+
headers,
|
|
544
|
+
body: JSON.stringify({ value })
|
|
545
|
+
})
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
async delete(id) {
|
|
549
|
+
const token = await this.getToken();
|
|
550
|
+
const headers = {
|
|
551
|
+
Authorization: `Bearer ${token}`
|
|
552
|
+
};
|
|
553
|
+
return this.handleRequest(
|
|
554
|
+
fetch(`${this.baseUrl}/account/${this.projectId}/db/${this.table}/${id}`, {
|
|
555
|
+
method: "DELETE",
|
|
556
|
+
headers
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
var BasicDBSDK = class {
|
|
562
|
+
projectId;
|
|
563
|
+
getToken;
|
|
564
|
+
baseUrl;
|
|
565
|
+
schema;
|
|
566
|
+
tableNames;
|
|
567
|
+
constructor(config) {
|
|
568
|
+
this.projectId = config.project_id;
|
|
569
|
+
if (config.getToken) {
|
|
570
|
+
this.getToken = config.getToken;
|
|
571
|
+
} else if (config.token) {
|
|
572
|
+
this.getToken = async () => config.token;
|
|
573
|
+
} else {
|
|
574
|
+
throw new Error("Either token or getToken must be provided");
|
|
575
|
+
}
|
|
576
|
+
this.baseUrl = config.baseUrl || "https://api.basic.tech";
|
|
577
|
+
this.schema = config.schema;
|
|
578
|
+
this.tableNames = Object.keys(this.schema.tables);
|
|
579
|
+
}
|
|
580
|
+
// Primary method - table access
|
|
581
|
+
table(name) {
|
|
582
|
+
if (!this.tableNames.includes(name)) {
|
|
583
|
+
throw new Error(`Table '${name}' not found in schema. Available tables: ${this.tableNames.join(", ")}`);
|
|
584
|
+
}
|
|
585
|
+
return new TableClient(
|
|
586
|
+
this.baseUrl,
|
|
587
|
+
this.projectId,
|
|
588
|
+
"",
|
|
589
|
+
// Empty placeholder, will be replaced in headers() method
|
|
590
|
+
name,
|
|
591
|
+
this.getToken,
|
|
592
|
+
this.schema
|
|
593
|
+
// Pass the entire schema to the TableClient
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
get tables() {
|
|
597
|
+
return {};
|
|
598
|
+
}
|
|
599
|
+
fields(table) {
|
|
600
|
+
const tableSchema = this.schema.tables[table];
|
|
601
|
+
if (!tableSchema) {
|
|
602
|
+
throw new Error(`Table '${table}' not found in schema`);
|
|
603
|
+
}
|
|
604
|
+
return Object.keys(tableSchema.fields);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// package.json
|
|
609
|
+
var version = "0.7.0-beta.1";
|
|
610
|
+
|
|
611
|
+
// src/updater/versionUpdater.ts
|
|
612
|
+
var VersionUpdater = class {
|
|
613
|
+
storage;
|
|
614
|
+
currentVersion;
|
|
615
|
+
migrations;
|
|
616
|
+
versionKey = "basic_app_version";
|
|
617
|
+
constructor(storage, currentVersion, migrations = []) {
|
|
618
|
+
this.storage = storage;
|
|
619
|
+
this.currentVersion = currentVersion;
|
|
620
|
+
this.migrations = migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Check current stored version and run migrations if needed
|
|
624
|
+
* Only compares major.minor versions, ignoring beta/prerelease parts
|
|
625
|
+
* Example: "0.7.0-beta.1" and "0.7.0" are treated as the same version
|
|
626
|
+
*/
|
|
627
|
+
async checkAndUpdate() {
|
|
628
|
+
const storedVersion = await this.getStoredVersion();
|
|
629
|
+
if (!storedVersion) {
|
|
630
|
+
await this.setStoredVersion(this.currentVersion);
|
|
631
|
+
return { updated: false, toVersion: this.currentVersion };
|
|
632
|
+
}
|
|
633
|
+
if (storedVersion === this.currentVersion) {
|
|
634
|
+
return { updated: false, toVersion: this.currentVersion };
|
|
635
|
+
}
|
|
636
|
+
const migrationsToRun = this.getMigrationsToRun(storedVersion, this.currentVersion);
|
|
637
|
+
if (migrationsToRun.length === 0) {
|
|
638
|
+
await this.setStoredVersion(this.currentVersion);
|
|
639
|
+
return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
|
|
640
|
+
}
|
|
641
|
+
for (const migration of migrationsToRun) {
|
|
642
|
+
try {
|
|
643
|
+
console.log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`);
|
|
644
|
+
await migration.migrate(this.storage);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
console.error(`Migration failed from ${migration.fromVersion} to ${migration.toVersion}:`, error);
|
|
647
|
+
throw new Error(`Migration failed: ${error}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
await this.setStoredVersion(this.currentVersion);
|
|
651
|
+
return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
|
|
652
|
+
}
|
|
653
|
+
async getStoredVersion() {
|
|
654
|
+
try {
|
|
655
|
+
const versionData = await this.storage.get(this.versionKey);
|
|
656
|
+
if (!versionData)
|
|
657
|
+
return null;
|
|
658
|
+
const versionInfo = JSON.parse(versionData);
|
|
659
|
+
return versionInfo.version;
|
|
660
|
+
} catch (error) {
|
|
661
|
+
console.warn("Failed to get stored version:", error);
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async setStoredVersion(version2) {
|
|
666
|
+
const versionInfo = {
|
|
667
|
+
version: version2,
|
|
668
|
+
lastUpdated: Date.now()
|
|
669
|
+
};
|
|
670
|
+
await this.storage.set(this.versionKey, JSON.stringify(versionInfo));
|
|
671
|
+
}
|
|
672
|
+
getMigrationsToRun(fromVersion, toVersion) {
|
|
673
|
+
return this.migrations.filter((migration) => {
|
|
674
|
+
const storedLessThanMigrationTo = this.compareVersions(fromVersion, migration.toVersion) < 0;
|
|
675
|
+
const currentGreaterThanOrEqualMigrationTo = this.compareVersions(toVersion, migration.toVersion) >= 0;
|
|
676
|
+
console.log(`Checking migration ${migration.fromVersion} \u2192 ${migration.toVersion}:`);
|
|
677
|
+
console.log(` stored ${fromVersion} < migration.to ${migration.toVersion}: ${storedLessThanMigrationTo}`);
|
|
678
|
+
console.log(` current ${toVersion} >= migration.to ${migration.toVersion}: ${currentGreaterThanOrEqualMigrationTo}`);
|
|
679
|
+
const shouldRun = storedLessThanMigrationTo && currentGreaterThanOrEqualMigrationTo;
|
|
680
|
+
console.log(` Should run: ${shouldRun}`);
|
|
681
|
+
return shouldRun;
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Simple semantic version comparison (major.minor only, ignoring beta/prerelease)
|
|
686
|
+
* Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
687
|
+
*/
|
|
688
|
+
compareVersions(a, b) {
|
|
689
|
+
const aMajorMinor = this.extractMajorMinor(a);
|
|
690
|
+
const bMajorMinor = this.extractMajorMinor(b);
|
|
691
|
+
if (aMajorMinor.major !== bMajorMinor.major) {
|
|
692
|
+
return aMajorMinor.major - bMajorMinor.major;
|
|
693
|
+
}
|
|
694
|
+
return aMajorMinor.minor - bMajorMinor.minor;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Extract major.minor from version string, ignoring beta/prerelease
|
|
698
|
+
* Examples: "0.7.0-beta.1" -> {major: 0, minor: 7}
|
|
699
|
+
* "1.2.3" -> {major: 1, minor: 2}
|
|
700
|
+
*/
|
|
701
|
+
extractMajorMinor(version2) {
|
|
702
|
+
const cleanVersion = version2.split("-")[0]?.split("+")[0] || version2;
|
|
703
|
+
const parts = cleanVersion.split(".").map(Number);
|
|
704
|
+
return {
|
|
705
|
+
major: parts[0] || 0,
|
|
706
|
+
minor: parts[1] || 0
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Add a migration to the updater
|
|
711
|
+
*/
|
|
712
|
+
addMigration(migration) {
|
|
713
|
+
this.migrations.push(migration);
|
|
714
|
+
this.migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
function createVersionUpdater(storage, currentVersion, migrations = []) {
|
|
718
|
+
return new VersionUpdater(storage, currentVersion, migrations);
|
|
263
719
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
720
|
+
|
|
721
|
+
// src/updater/updateMigrations.ts
|
|
722
|
+
var addMigrationTimestamp = {
|
|
723
|
+
fromVersion: "0.6.0",
|
|
724
|
+
toVersion: "0.7.0",
|
|
725
|
+
async migrate(storage) {
|
|
726
|
+
console.log("Running test migration");
|
|
727
|
+
storage.set("test_migration", "true");
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
function getMigrations() {
|
|
731
|
+
return [
|
|
732
|
+
addMigrationTimestamp
|
|
733
|
+
];
|
|
275
734
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
735
|
+
|
|
736
|
+
// src/utils/storage.ts
|
|
737
|
+
var LocalStorageAdapter = class {
|
|
738
|
+
async get(key) {
|
|
739
|
+
return localStorage.getItem(key);
|
|
740
|
+
}
|
|
741
|
+
async set(key, value) {
|
|
742
|
+
localStorage.setItem(key, value);
|
|
743
|
+
}
|
|
744
|
+
async remove(key) {
|
|
745
|
+
localStorage.removeItem(key);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
var STORAGE_KEYS = {
|
|
749
|
+
REFRESH_TOKEN: "basic_refresh_token",
|
|
750
|
+
USER_INFO: "basic_user_info",
|
|
751
|
+
AUTH_STATE: "basic_auth_state",
|
|
752
|
+
DEBUG: "basic_debug"
|
|
753
|
+
};
|
|
754
|
+
function getCookie(name) {
|
|
755
|
+
let cookieValue = "";
|
|
756
|
+
if (document.cookie && document.cookie !== "") {
|
|
757
|
+
const cookies = document.cookie.split(";");
|
|
758
|
+
for (let i = 0; i < cookies.length; i++) {
|
|
759
|
+
const cookie = cookies[i]?.trim();
|
|
760
|
+
if (cookie && cookie.substring(0, name.length + 1) === name + "=") {
|
|
761
|
+
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return cookieValue;
|
|
767
|
+
}
|
|
768
|
+
function setCookie(name, value, options) {
|
|
769
|
+
const opts = {
|
|
770
|
+
secure: true,
|
|
771
|
+
sameSite: "Strict",
|
|
772
|
+
httpOnly: false,
|
|
773
|
+
...options
|
|
774
|
+
};
|
|
775
|
+
let cookieString = `${name}=${value}`;
|
|
776
|
+
if (opts.secure)
|
|
777
|
+
cookieString += "; Secure";
|
|
778
|
+
if (opts.sameSite)
|
|
779
|
+
cookieString += `; SameSite=${opts.sameSite}`;
|
|
780
|
+
if (opts.httpOnly)
|
|
781
|
+
cookieString += "; HttpOnly";
|
|
782
|
+
document.cookie = cookieString;
|
|
783
|
+
}
|
|
784
|
+
function clearCookie(name) {
|
|
785
|
+
document.cookie = `${name}=; Secure; SameSite=Strict`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/utils/network.ts
|
|
789
|
+
function isDevelopment(debug) {
|
|
790
|
+
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;
|
|
287
791
|
}
|
|
288
|
-
async function
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"
|
|
792
|
+
async function checkForNewVersion() {
|
|
793
|
+
try {
|
|
794
|
+
const isBeta = version.includes("beta");
|
|
795
|
+
const response = await fetch(`https://registry.npmjs.org/@basictech/react/${isBeta ? "beta" : "latest"}`);
|
|
796
|
+
if (!response.ok) {
|
|
797
|
+
throw new Error("Failed to fetch version from npm");
|
|
294
798
|
}
|
|
295
|
-
|
|
296
|
-
|
|
799
|
+
const data = await response.json();
|
|
800
|
+
const latestVersion = data.version;
|
|
801
|
+
if (latestVersion !== version) {
|
|
802
|
+
console.warn("[basic] New version available:", latestVersion, `
|
|
803
|
+
run "npm install @basictech/react@${latestVersion}" to update`);
|
|
804
|
+
}
|
|
805
|
+
if (isBeta) {
|
|
806
|
+
log("thank you for being on basictech/react beta :)");
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
hasNewVersion: version !== latestVersion,
|
|
810
|
+
latestVersion,
|
|
811
|
+
currentVersion: version
|
|
812
|
+
};
|
|
813
|
+
} catch (error) {
|
|
814
|
+
log("Error checking for new version:", error);
|
|
815
|
+
return {
|
|
816
|
+
hasNewVersion: false,
|
|
817
|
+
latestVersion: null,
|
|
818
|
+
currentVersion: null
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function cleanOAuthParamsFromUrl() {
|
|
823
|
+
if (window.location.search.includes("code") || window.location.search.includes("state")) {
|
|
824
|
+
const url = new URL(window.location.href);
|
|
825
|
+
url.searchParams.delete("code");
|
|
826
|
+
url.searchParams.delete("state");
|
|
827
|
+
window.history.pushState({}, document.title, url.pathname + url.search);
|
|
828
|
+
log("Cleaned OAuth parameters from URL");
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function getSyncStatus(statusCode) {
|
|
832
|
+
switch (statusCode) {
|
|
833
|
+
case -1:
|
|
834
|
+
return "ERROR";
|
|
835
|
+
case 0:
|
|
836
|
+
return "OFFLINE";
|
|
837
|
+
case 1:
|
|
838
|
+
return "CONNECTING";
|
|
839
|
+
case 2:
|
|
840
|
+
return "ONLINE";
|
|
841
|
+
case 3:
|
|
842
|
+
return "SYNCING";
|
|
843
|
+
case 4:
|
|
844
|
+
return "ERROR_WILL_RETRY";
|
|
845
|
+
default:
|
|
846
|
+
return "UNKNOWN";
|
|
847
|
+
}
|
|
297
848
|
}
|
|
298
849
|
|
|
299
|
-
// src/
|
|
850
|
+
// src/utils/schema.ts
|
|
300
851
|
import { validateSchema as validateSchema2, compareSchemas } from "@basictech/schema";
|
|
301
|
-
|
|
302
|
-
// package.json
|
|
303
|
-
var version = "0.6.0";
|
|
304
|
-
|
|
305
|
-
// src/AuthContext.tsx
|
|
306
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
307
|
-
var BasicContext = createContext({
|
|
308
|
-
unicorn: "\u{1F984}",
|
|
309
|
-
isAuthReady: false,
|
|
310
|
-
isSignedIn: false,
|
|
311
|
-
user: null,
|
|
312
|
-
signout: () => {
|
|
313
|
-
},
|
|
314
|
-
signin: () => {
|
|
315
|
-
},
|
|
316
|
-
getToken: () => new Promise(() => {
|
|
317
|
-
}),
|
|
318
|
-
getSignInLink: () => "",
|
|
319
|
-
db: {},
|
|
320
|
-
dbStatus: "LOADING" /* LOADING */
|
|
321
|
-
});
|
|
322
852
|
async function getSchemaStatus(schema) {
|
|
323
853
|
const projectId = schema.project_id;
|
|
324
|
-
let status = "";
|
|
325
854
|
const valid = validateSchema2(schema);
|
|
326
855
|
if (!valid.valid) {
|
|
327
856
|
console.warn("BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled");
|
|
@@ -384,55 +913,62 @@ async function getSchemaStatus(schema) {
|
|
|
384
913
|
};
|
|
385
914
|
}
|
|
386
915
|
}
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
case 4:
|
|
400
|
-
return "ERROR_WILL_RETRY";
|
|
401
|
-
default:
|
|
402
|
-
return "UNKNOWN";
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
async function checkForNewVersion() {
|
|
406
|
-
try {
|
|
407
|
-
const isBeta = version.includes("beta");
|
|
408
|
-
const response = await fetch(`https://registry.npmjs.org/@basictech/react/${isBeta ? "beta" : "latest"}`);
|
|
409
|
-
if (!response.ok) {
|
|
410
|
-
throw new Error("Failed to fetch version from npm");
|
|
411
|
-
}
|
|
412
|
-
const data = await response.json();
|
|
413
|
-
const latestVersion = data.version;
|
|
414
|
-
if (latestVersion !== version) {
|
|
415
|
-
console.warn("[basic] New version available:", latestVersion, `
|
|
416
|
-
run "npm install @basictech/react@${latestVersion}" to update`);
|
|
417
|
-
}
|
|
418
|
-
if (isBeta) {
|
|
419
|
-
log("thank you for being on basictech/react beta :)");
|
|
420
|
-
}
|
|
916
|
+
async function validateAndCheckSchema(schema) {
|
|
917
|
+
const valid = validateSchema2(schema);
|
|
918
|
+
if (!valid.valid) {
|
|
919
|
+
log("Basic Schema is invalid!", valid.errors);
|
|
920
|
+
console.group("Schema Errors");
|
|
921
|
+
let errorMessage = "";
|
|
922
|
+
valid.errors.forEach((error, index) => {
|
|
923
|
+
log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`);
|
|
924
|
+
errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}
|
|
925
|
+
`;
|
|
926
|
+
});
|
|
927
|
+
console.groupEnd();
|
|
421
928
|
return {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
};
|
|
426
|
-
} catch (error) {
|
|
427
|
-
log("Error checking for new version:", error);
|
|
428
|
-
return {
|
|
429
|
-
hasNewVersion: false,
|
|
430
|
-
latestVersion: null,
|
|
431
|
-
currentVersion: null
|
|
929
|
+
isValid: false,
|
|
930
|
+
schemaStatus: { valid: false },
|
|
931
|
+
errors: valid.errors
|
|
432
932
|
};
|
|
433
933
|
}
|
|
934
|
+
let schemaStatus = { valid: false };
|
|
935
|
+
if (schema.version !== 0) {
|
|
936
|
+
schemaStatus = await getSchemaStatus(schema);
|
|
937
|
+
log("schemaStatus", schemaStatus);
|
|
938
|
+
} else {
|
|
939
|
+
log("schema not published - at version 0");
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
isValid: true,
|
|
943
|
+
schemaStatus
|
|
944
|
+
};
|
|
434
945
|
}
|
|
435
|
-
|
|
946
|
+
|
|
947
|
+
// src/AuthContext.tsx
|
|
948
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
949
|
+
var BasicContext = createContext({
|
|
950
|
+
unicorn: "\u{1F984}",
|
|
951
|
+
isAuthReady: false,
|
|
952
|
+
isSignedIn: false,
|
|
953
|
+
user: null,
|
|
954
|
+
signout: () => Promise.resolve(),
|
|
955
|
+
signin: () => Promise.resolve(),
|
|
956
|
+
signinWithCode: () => new Promise(() => {
|
|
957
|
+
}),
|
|
958
|
+
getToken: () => new Promise(() => {
|
|
959
|
+
}),
|
|
960
|
+
getSignInLink: () => Promise.resolve(""),
|
|
961
|
+
db: {},
|
|
962
|
+
remoteDb: {},
|
|
963
|
+
dbStatus: "LOADING" /* LOADING */
|
|
964
|
+
});
|
|
965
|
+
function BasicProvider({
|
|
966
|
+
children,
|
|
967
|
+
project_id,
|
|
968
|
+
schema,
|
|
969
|
+
debug = false,
|
|
970
|
+
storage
|
|
971
|
+
}) {
|
|
436
972
|
const [isAuthReady, setIsAuthReady] = useState(false);
|
|
437
973
|
const [isSignedIn, setIsSignedIn] = useState(false);
|
|
438
974
|
const [token, setToken] = useState(null);
|
|
@@ -441,7 +977,41 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
|
|
|
441
977
|
const [isReady, setIsReady] = useState(false);
|
|
442
978
|
const [dbStatus, setDbStatus] = useState("OFFLINE" /* OFFLINE */);
|
|
443
979
|
const [error, setError] = useState(null);
|
|
980
|
+
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
981
|
+
const [pendingRefresh, setPendingRefresh] = useState(false);
|
|
444
982
|
const syncRef = useRef(null);
|
|
983
|
+
const remoteDbRef = useRef(null);
|
|
984
|
+
const storageAdapter = storage || new LocalStorageAdapter();
|
|
985
|
+
const isDevMode = () => isDevelopment(debug);
|
|
986
|
+
const cleanOAuthParams = () => cleanOAuthParamsFromUrl();
|
|
987
|
+
useEffect(() => {
|
|
988
|
+
const handleOnline = () => {
|
|
989
|
+
log("Network came back online");
|
|
990
|
+
setIsOnline(true);
|
|
991
|
+
if (pendingRefresh) {
|
|
992
|
+
log("Retrying pending token refresh");
|
|
993
|
+
setPendingRefresh(false);
|
|
994
|
+
if (token) {
|
|
995
|
+
const refreshToken = token.refresh_token || localStorage.getItem("basic_refresh_token");
|
|
996
|
+
if (refreshToken) {
|
|
997
|
+
fetchToken(refreshToken).catch((error2) => {
|
|
998
|
+
log("Retry refresh failed:", error2);
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
const handleOffline = () => {
|
|
1005
|
+
log("Network went offline");
|
|
1006
|
+
setIsOnline(false);
|
|
1007
|
+
};
|
|
1008
|
+
window.addEventListener("online", handleOnline);
|
|
1009
|
+
window.addEventListener("offline", handleOffline);
|
|
1010
|
+
return () => {
|
|
1011
|
+
window.removeEventListener("online", handleOnline);
|
|
1012
|
+
window.removeEventListener("offline", handleOffline);
|
|
1013
|
+
};
|
|
1014
|
+
}, [pendingRefresh, token]);
|
|
445
1015
|
useEffect(() => {
|
|
446
1016
|
function initDb(options) {
|
|
447
1017
|
if (!syncRef.current) {
|
|
@@ -450,9 +1020,6 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
|
|
|
450
1020
|
syncRef.current.syncable.on("statusChanged", (status, url) => {
|
|
451
1021
|
setDbStatus(getSyncStatus(status));
|
|
452
1022
|
});
|
|
453
|
-
syncRef.current.syncable.getStatus().then((status) => {
|
|
454
|
-
setDbStatus(getSyncStatus(status));
|
|
455
|
-
});
|
|
456
1023
|
if (options.shouldConnect) {
|
|
457
1024
|
setShouldConnect(true);
|
|
458
1025
|
} else {
|
|
@@ -462,17 +1029,15 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
|
|
|
462
1029
|
}
|
|
463
1030
|
}
|
|
464
1031
|
async function checkSchema() {
|
|
465
|
-
const
|
|
466
|
-
if (!
|
|
467
|
-
log("Basic Schema is invalid!", valid.errors);
|
|
468
|
-
console.group("Schema Errors");
|
|
1032
|
+
const result = await validateAndCheckSchema(schema);
|
|
1033
|
+
if (!result.isValid) {
|
|
469
1034
|
let errorMessage = "";
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
1035
|
+
if (result.errors) {
|
|
1036
|
+
result.errors.forEach((error2, index) => {
|
|
1037
|
+
errorMessage += `${index + 1}: ${error2.message} - at ${error2.instancePath}
|
|
473
1038
|
`;
|
|
474
|
-
|
|
475
|
-
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
476
1041
|
setError({
|
|
477
1042
|
code: "schema_invalid",
|
|
478
1043
|
title: "Basic Schema is invalid!",
|
|
@@ -481,17 +1046,10 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
|
|
|
481
1046
|
setIsReady(true);
|
|
482
1047
|
return null;
|
|
483
1048
|
}
|
|
484
|
-
|
|
485
|
-
if (schema.version !== 0) {
|
|
486
|
-
schemaStatus = await getSchemaStatus(schema);
|
|
487
|
-
log("schemaStatus", schemaStatus);
|
|
488
|
-
} else {
|
|
489
|
-
log("schema not published - at version 0");
|
|
490
|
-
}
|
|
491
|
-
if (schemaStatus.valid) {
|
|
1049
|
+
if (result.schemaStatus.valid) {
|
|
492
1050
|
initDb({ shouldConnect: true });
|
|
493
1051
|
} else {
|
|
494
|
-
log("Schema is invalid!", schemaStatus);
|
|
1052
|
+
log("Schema is invalid!", result.schemaStatus);
|
|
495
1053
|
initDb({ shouldConnect: false });
|
|
496
1054
|
}
|
|
497
1055
|
checkForNewVersion();
|
|
@@ -503,36 +1061,101 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
|
|
|
503
1061
|
}
|
|
504
1062
|
}, []);
|
|
505
1063
|
useEffect(() => {
|
|
506
|
-
|
|
507
|
-
|
|
1064
|
+
async function connectToDb() {
|
|
1065
|
+
if (token && syncRef.current && isSignedIn && shouldConnect) {
|
|
1066
|
+
const tok = await getToken();
|
|
1067
|
+
if (!tok) {
|
|
1068
|
+
log("no token found");
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
log("connecting to db...");
|
|
1072
|
+
syncRef.current?.connect({ access_token: tok }).catch((e) => {
|
|
1073
|
+
log("error connecting to db", e);
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
508
1076
|
}
|
|
1077
|
+
connectToDb();
|
|
509
1078
|
}, [isSignedIn, shouldConnect]);
|
|
510
1079
|
useEffect(() => {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
1080
|
+
if (project_id && schema && token?.access_token && !remoteDbRef.current) {
|
|
1081
|
+
log("Initializing Remote DB SDK");
|
|
1082
|
+
remoteDbRef.current = new BasicDBSDK({
|
|
1083
|
+
project_id,
|
|
1084
|
+
schema,
|
|
1085
|
+
getToken: () => getToken(),
|
|
1086
|
+
baseUrl: "https://api.basic.tech"
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}, [token, project_id, schema]);
|
|
1090
|
+
useEffect(() => {
|
|
1091
|
+
const initializeAuth = async () => {
|
|
1092
|
+
await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? "true" : "false");
|
|
1093
|
+
try {
|
|
1094
|
+
const versionUpdater = createVersionUpdater(storageAdapter, version, getMigrations());
|
|
1095
|
+
const updateResult = await versionUpdater.checkAndUpdate();
|
|
1096
|
+
if (updateResult.updated) {
|
|
1097
|
+
log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`);
|
|
1098
|
+
} else {
|
|
1099
|
+
log(`App version ${updateResult.toVersion} is current`);
|
|
522
1100
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (
|
|
528
|
-
|
|
1101
|
+
} catch (error2) {
|
|
1102
|
+
log("Version update failed:", error2);
|
|
1103
|
+
}
|
|
1104
|
+
try {
|
|
1105
|
+
if (window.location.search.includes("code")) {
|
|
1106
|
+
let code = window.location?.search?.split("code=")[1]?.split("&")[0];
|
|
1107
|
+
if (!code)
|
|
1108
|
+
return;
|
|
1109
|
+
const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE);
|
|
1110
|
+
const urlState = window.location.search.split("state=")[1]?.split("&")[0];
|
|
1111
|
+
if (!state || state !== urlState) {
|
|
1112
|
+
log("error: auth state does not match");
|
|
1113
|
+
setIsAuthReady(true);
|
|
1114
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1115
|
+
cleanOAuthParams();
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1119
|
+
cleanOAuthParams();
|
|
1120
|
+
fetchToken(code).catch((error2) => {
|
|
1121
|
+
log("Error fetching token:", error2);
|
|
1122
|
+
});
|
|
529
1123
|
} else {
|
|
530
|
-
|
|
1124
|
+
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1125
|
+
if (refreshToken) {
|
|
1126
|
+
log("Found refresh token in storage, attempting to refresh access token");
|
|
1127
|
+
fetchToken(refreshToken).catch((error2) => {
|
|
1128
|
+
log("Error fetching refresh token:", error2);
|
|
1129
|
+
});
|
|
1130
|
+
} else {
|
|
1131
|
+
let cookie_token = getCookie("basic_token");
|
|
1132
|
+
if (cookie_token !== "") {
|
|
1133
|
+
const tokenData = JSON.parse(cookie_token);
|
|
1134
|
+
setToken(tokenData);
|
|
1135
|
+
if (tokenData.refresh_token) {
|
|
1136
|
+
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token);
|
|
1137
|
+
}
|
|
1138
|
+
} else {
|
|
1139
|
+
const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO);
|
|
1140
|
+
if (cachedUserInfo) {
|
|
1141
|
+
try {
|
|
1142
|
+
const userData = JSON.parse(cachedUserInfo);
|
|
1143
|
+
setUser(userData);
|
|
1144
|
+
setIsSignedIn(true);
|
|
1145
|
+
log("Loaded cached user info for offline mode");
|
|
1146
|
+
} catch (error2) {
|
|
1147
|
+
log("Error parsing cached user info:", error2);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
setIsAuthReady(true);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
531
1153
|
}
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
log("error getting token", e);
|
|
532
1156
|
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
}
|
|
1157
|
+
};
|
|
1158
|
+
initializeAuth();
|
|
536
1159
|
}, []);
|
|
537
1160
|
useEffect(() => {
|
|
538
1161
|
async function fetchUser(acc_token) {
|
|
@@ -547,10 +1170,13 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
|
|
|
547
1170
|
log("error fetching user", user2.error);
|
|
548
1171
|
return;
|
|
549
1172
|
} else {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
window.history.pushState({}, document.title, "/");
|
|
1173
|
+
if (token?.refresh_token) {
|
|
1174
|
+
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
|
|
553
1175
|
}
|
|
1176
|
+
await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user2));
|
|
1177
|
+
log("Cached user info in storage");
|
|
1178
|
+
setCookie("basic_access_token", token?.access_token || "", { httpOnly: false });
|
|
1179
|
+
setCookie("basic_token", JSON.stringify(token));
|
|
554
1180
|
setUser(user2);
|
|
555
1181
|
setIsSignedIn(true);
|
|
556
1182
|
setIsAuthReady(true);
|
|
@@ -566,56 +1192,122 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
|
|
|
566
1192
|
const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3;
|
|
567
1193
|
if (isExpired) {
|
|
568
1194
|
log("token is expired - refreshing ...");
|
|
569
|
-
|
|
570
|
-
|
|
1195
|
+
try {
|
|
1196
|
+
const newToken = await fetchToken(token?.refresh_token || "");
|
|
1197
|
+
fetchUser(newToken?.access_token || "");
|
|
1198
|
+
} catch (error2) {
|
|
1199
|
+
log("Failed to refresh token in checkToken:", error2);
|
|
1200
|
+
if (error2.message.includes("offline") || error2.message.includes("Network")) {
|
|
1201
|
+
log("Network issue - continuing with expired token until online");
|
|
1202
|
+
fetchUser(token?.access_token || "");
|
|
1203
|
+
} else {
|
|
1204
|
+
setIsAuthReady(true);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
571
1207
|
} else {
|
|
572
|
-
fetchUser(token
|
|
1208
|
+
fetchUser(token?.access_token || "");
|
|
573
1209
|
}
|
|
574
1210
|
}
|
|
575
1211
|
if (token) {
|
|
576
1212
|
checkToken();
|
|
577
1213
|
}
|
|
578
1214
|
}, [token]);
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1215
|
+
const getSignInLink = async (redirectUri) => {
|
|
1216
|
+
try {
|
|
1217
|
+
log("getting sign in link...");
|
|
1218
|
+
if (!project_id) {
|
|
1219
|
+
throw new Error("Project ID is required to generate sign-in link");
|
|
1220
|
+
}
|
|
1221
|
+
const randomState = Math.random().toString(36).substring(6);
|
|
1222
|
+
await storageAdapter.set(STORAGE_KEYS.AUTH_STATE, randomState);
|
|
1223
|
+
const redirectUrl = redirectUri || window.location.href;
|
|
1224
|
+
if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
|
|
1225
|
+
throw new Error("Invalid redirect URI provided");
|
|
1226
|
+
}
|
|
1227
|
+
let baseUrl = "https://api.basic.tech/auth/authorize";
|
|
1228
|
+
baseUrl += `?client_id=${project_id}`;
|
|
1229
|
+
baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`;
|
|
1230
|
+
baseUrl += `&response_type=code`;
|
|
1231
|
+
baseUrl += `&scope=profile`;
|
|
1232
|
+
baseUrl += `&state=${randomState}`;
|
|
1233
|
+
log("Generated sign-in link successfully");
|
|
1234
|
+
return baseUrl;
|
|
1235
|
+
} catch (error2) {
|
|
1236
|
+
log("Error generating sign-in link:", error2);
|
|
1237
|
+
throw error2;
|
|
584
1238
|
}
|
|
585
|
-
log("connecting to db...");
|
|
586
|
-
syncRef.current.connect({ access_token: tok }).catch((e) => {
|
|
587
|
-
log("error connecting to db", e);
|
|
588
|
-
});
|
|
589
1239
|
};
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1240
|
+
const signin = async () => {
|
|
1241
|
+
try {
|
|
1242
|
+
log("signing in...");
|
|
1243
|
+
if (!project_id) {
|
|
1244
|
+
log("Error: project_id is required for sign-in");
|
|
1245
|
+
throw new Error("Project ID is required for authentication");
|
|
1246
|
+
}
|
|
1247
|
+
const signInLink = await getSignInLink();
|
|
1248
|
+
log("Generated sign-in link:", signInLink);
|
|
1249
|
+
if (!signInLink || !signInLink.startsWith("https://")) {
|
|
1250
|
+
log("Error: Invalid sign-in link generated");
|
|
1251
|
+
throw new Error("Failed to generate valid sign-in URL");
|
|
1252
|
+
}
|
|
1253
|
+
window.location.href = signInLink;
|
|
1254
|
+
} catch (error2) {
|
|
1255
|
+
log("Error during sign-in:", error2);
|
|
1256
|
+
if (isDevMode()) {
|
|
1257
|
+
setError({
|
|
1258
|
+
code: "signin_error",
|
|
1259
|
+
title: "Sign-in Failed",
|
|
1260
|
+
message: error2.message || "An error occurred during sign-in. Please try again."
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
throw error2;
|
|
1264
|
+
}
|
|
601
1265
|
};
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1266
|
+
const signinWithCode = async (code, state) => {
|
|
1267
|
+
try {
|
|
1268
|
+
log("signinWithCode called with code:", code);
|
|
1269
|
+
if (!code || typeof code !== "string") {
|
|
1270
|
+
return { success: false, error: "Invalid authorization code" };
|
|
1271
|
+
}
|
|
1272
|
+
if (state) {
|
|
1273
|
+
const storedState = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE);
|
|
1274
|
+
if (storedState && storedState !== state) {
|
|
1275
|
+
log("State parameter mismatch:", { provided: state, stored: storedState });
|
|
1276
|
+
return { success: false, error: "State parameter mismatch" };
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1280
|
+
cleanOAuthParams();
|
|
1281
|
+
const token2 = await fetchToken(code);
|
|
1282
|
+
if (token2) {
|
|
1283
|
+
log("signinWithCode successful");
|
|
1284
|
+
return { success: true };
|
|
1285
|
+
} else {
|
|
1286
|
+
return { success: false, error: "Failed to exchange code for token" };
|
|
1287
|
+
}
|
|
1288
|
+
} catch (error2) {
|
|
1289
|
+
log("signinWithCode error:", error2);
|
|
1290
|
+
return {
|
|
1291
|
+
success: false,
|
|
1292
|
+
error: error2.message || "Authentication failed"
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
606
1295
|
};
|
|
607
|
-
const signout = () => {
|
|
1296
|
+
const signout = async () => {
|
|
608
1297
|
log("signing out!");
|
|
609
1298
|
setUser({});
|
|
610
1299
|
setIsSignedIn(false);
|
|
611
1300
|
setToken(null);
|
|
612
|
-
|
|
613
|
-
|
|
1301
|
+
clearCookie("basic_token");
|
|
1302
|
+
clearCookie("basic_access_token");
|
|
1303
|
+
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1304
|
+
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1305
|
+
await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
|
|
614
1306
|
if (syncRef.current) {
|
|
615
1307
|
(async () => {
|
|
616
1308
|
try {
|
|
617
|
-
await syncRef.current
|
|
618
|
-
await syncRef.current
|
|
1309
|
+
await syncRef.current?.close();
|
|
1310
|
+
await syncRef.current?.delete({ disableAutoOpen: false });
|
|
619
1311
|
syncRef.current = null;
|
|
620
1312
|
window?.location?.reload();
|
|
621
1313
|
} catch (error2) {
|
|
@@ -627,6 +1319,27 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
|
|
|
627
1319
|
const getToken = async () => {
|
|
628
1320
|
log("getting token...");
|
|
629
1321
|
if (!token) {
|
|
1322
|
+
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1323
|
+
if (refreshToken) {
|
|
1324
|
+
log("No token in memory, attempting to refresh from storage");
|
|
1325
|
+
try {
|
|
1326
|
+
const newToken = await fetchToken(refreshToken);
|
|
1327
|
+
if (newToken?.access_token) {
|
|
1328
|
+
return newToken.access_token;
|
|
1329
|
+
}
|
|
1330
|
+
} catch (error2) {
|
|
1331
|
+
log("Failed to refresh token from storage:", error2);
|
|
1332
|
+
if (error2.message.includes("offline") || error2.message.includes("Network")) {
|
|
1333
|
+
log("Network issue - continuing with potentially expired token");
|
|
1334
|
+
const lastToken = localStorage.getItem("basic_access_token");
|
|
1335
|
+
if (lastToken) {
|
|
1336
|
+
return lastToken;
|
|
1337
|
+
}
|
|
1338
|
+
throw new Error("Network offline - authentication will be retried when online");
|
|
1339
|
+
}
|
|
1340
|
+
throw new Error("Authentication expired. Please sign in again.");
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
630
1343
|
log("no token found");
|
|
631
1344
|
throw new Error("no token found");
|
|
632
1345
|
}
|
|
@@ -634,69 +1347,86 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
|
|
|
634
1347
|
const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3;
|
|
635
1348
|
if (isExpired) {
|
|
636
1349
|
log("token is expired - refreshing ...");
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
650
|
-
break;
|
|
1350
|
+
const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1351
|
+
if (refreshToken) {
|
|
1352
|
+
try {
|
|
1353
|
+
const newToken = await fetchToken(refreshToken);
|
|
1354
|
+
return newToken?.access_token || "";
|
|
1355
|
+
} catch (error2) {
|
|
1356
|
+
log("Failed to refresh expired token:", error2);
|
|
1357
|
+
if (error2.message.includes("offline") || error2.message.includes("Network")) {
|
|
1358
|
+
log("Network issue - using expired token until network is restored");
|
|
1359
|
+
return token.access_token;
|
|
1360
|
+
}
|
|
1361
|
+
throw new Error("Authentication expired. Please sign in again.");
|
|
651
1362
|
}
|
|
1363
|
+
} else {
|
|
1364
|
+
throw new Error("no refresh token available");
|
|
652
1365
|
}
|
|
653
1366
|
}
|
|
654
|
-
return
|
|
655
|
-
}
|
|
656
|
-
const fetchToken = async (code) => {
|
|
657
|
-
const token2 = await fetch("https://api.basic.tech/auth/token", {
|
|
658
|
-
method: "POST",
|
|
659
|
-
headers: {
|
|
660
|
-
"Content-Type": "application/json"
|
|
661
|
-
},
|
|
662
|
-
body: JSON.stringify({ code })
|
|
663
|
-
}).then((response) => response.json()).catch((error2) => log("Error:", error2));
|
|
664
|
-
if (token2.error) {
|
|
665
|
-
log("error fetching token", token2.error);
|
|
666
|
-
return;
|
|
667
|
-
} else {
|
|
668
|
-
setToken(token2);
|
|
669
|
-
}
|
|
670
|
-
return token2;
|
|
1367
|
+
return token?.access_token || "";
|
|
671
1368
|
};
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
if (!
|
|
675
|
-
|
|
1369
|
+
const fetchToken = async (code) => {
|
|
1370
|
+
try {
|
|
1371
|
+
if (!isOnline) {
|
|
1372
|
+
log("Network is offline, marking refresh as pending");
|
|
1373
|
+
setPendingRefresh(true);
|
|
1374
|
+
throw new Error("Network offline - refresh will be retried when online");
|
|
676
1375
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1376
|
+
const token2 = await fetch("https://api.basic.tech/auth/token", {
|
|
1377
|
+
method: "POST",
|
|
1378
|
+
headers: {
|
|
1379
|
+
"Content-Type": "application/json"
|
|
1380
|
+
},
|
|
1381
|
+
body: JSON.stringify({ code })
|
|
1382
|
+
}).then((response) => response.json()).catch((error2) => {
|
|
1383
|
+
log("Network error fetching token:", error2);
|
|
1384
|
+
if (!isOnline) {
|
|
1385
|
+
setPendingRefresh(true);
|
|
1386
|
+
throw new Error("Network offline - refresh will be retried when online");
|
|
1387
|
+
}
|
|
1388
|
+
throw new Error("Network error during token refresh");
|
|
1389
|
+
});
|
|
1390
|
+
if (token2.error) {
|
|
1391
|
+
log("error fetching token", token2.error);
|
|
1392
|
+
if (token2.error.includes("network") || token2.error.includes("timeout")) {
|
|
1393
|
+
setPendingRefresh(true);
|
|
1394
|
+
throw new Error("Network issue - refresh will be retried when online");
|
|
1395
|
+
}
|
|
1396
|
+
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1397
|
+
await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
|
|
1398
|
+
clearCookie("basic_token");
|
|
1399
|
+
clearCookie("basic_access_token");
|
|
1400
|
+
setUser({});
|
|
1401
|
+
setIsSignedIn(false);
|
|
1402
|
+
setToken(null);
|
|
1403
|
+
setIsAuthReady(true);
|
|
1404
|
+
throw new Error(`Token refresh failed: ${token2.error}`);
|
|
1405
|
+
} else {
|
|
1406
|
+
setToken(token2);
|
|
1407
|
+
setPendingRefresh(false);
|
|
1408
|
+
if (token2.refresh_token) {
|
|
1409
|
+
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token2.refresh_token);
|
|
1410
|
+
log("Updated refresh token in storage");
|
|
1411
|
+
}
|
|
1412
|
+
setCookie("basic_access_token", token2.access_token, { httpOnly: false });
|
|
1413
|
+
log("Updated access token in cookie");
|
|
698
1414
|
}
|
|
699
|
-
|
|
1415
|
+
return token2;
|
|
1416
|
+
} catch (error2) {
|
|
1417
|
+
log("Token refresh error:", error2);
|
|
1418
|
+
if (!error2.message.includes("offline") && !error2.message.includes("Network")) {
|
|
1419
|
+
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1420
|
+
await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
|
|
1421
|
+
clearCookie("basic_token");
|
|
1422
|
+
clearCookie("basic_access_token");
|
|
1423
|
+
setUser({});
|
|
1424
|
+
setIsSignedIn(false);
|
|
1425
|
+
setToken(null);
|
|
1426
|
+
setIsAuthReady(true);
|
|
1427
|
+
}
|
|
1428
|
+
throw error2;
|
|
1429
|
+
}
|
|
700
1430
|
};
|
|
701
1431
|
const noDb = {
|
|
702
1432
|
collection: () => {
|
|
@@ -710,12 +1440,14 @@ function BasicProvider({ children, project_id, schema, debug = false }) {
|
|
|
710
1440
|
user,
|
|
711
1441
|
signout,
|
|
712
1442
|
signin,
|
|
1443
|
+
signinWithCode,
|
|
713
1444
|
getToken,
|
|
714
1445
|
getSignInLink,
|
|
715
1446
|
db: syncRef.current ? syncRef.current : noDb,
|
|
1447
|
+
remoteDb: remoteDbRef.current ? remoteDbRef.current : noDb,
|
|
716
1448
|
dbStatus
|
|
717
1449
|
}, children: [
|
|
718
|
-
error && /* @__PURE__ */ jsx(ErrorDisplay, { error }),
|
|
1450
|
+
error && isDevMode() && /* @__PURE__ */ jsx(ErrorDisplay, { error }),
|
|
719
1451
|
isReady && children
|
|
720
1452
|
] });
|
|
721
1453
|
}
|
|
@@ -749,6 +1481,7 @@ function useBasic() {
|
|
|
749
1481
|
// src/index.ts
|
|
750
1482
|
import { useLiveQuery as useQuery } from "dexie-react-hooks";
|
|
751
1483
|
export {
|
|
1484
|
+
BasicDBSDK,
|
|
752
1485
|
BasicProvider,
|
|
753
1486
|
useBasic,
|
|
754
1487
|
useQuery
|