@basictech/react 0.7.0-beta.1 → 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 +6 -0
- package/dist/index.d.mts +107 -1
- package/dist/index.d.ts +107 -1
- package/dist/index.js +719 -356
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +716 -344
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/AuthContext.tsx +178 -402
- package/src/index.ts +6 -31
- package/src/sync/index.ts +1 -1
- 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/src/schema.ts +0 -159
package/dist/index.mjs
CHANGED
|
@@ -127,116 +127,8 @@ var syncProtocol = function() {
|
|
|
127
127
|
});
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
-
// src/schema.ts
|
|
131
|
-
import Ajv from "ajv";
|
|
132
|
-
var basicJsonSchema = {
|
|
133
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
134
|
-
"type": "object",
|
|
135
|
-
"properties": {
|
|
136
|
-
"project_id": {
|
|
137
|
-
"type": "string"
|
|
138
|
-
},
|
|
139
|
-
"namespace": {
|
|
140
|
-
"type": "string"
|
|
141
|
-
},
|
|
142
|
-
"version": {
|
|
143
|
-
"type": "integer",
|
|
144
|
-
"minimum": 0
|
|
145
|
-
},
|
|
146
|
-
"tables": {
|
|
147
|
-
"type": "object",
|
|
148
|
-
"patternProperties": {
|
|
149
|
-
"^[a-zA-Z0-9_]+$": {
|
|
150
|
-
"type": "object",
|
|
151
|
-
"properties": {
|
|
152
|
-
"name": {
|
|
153
|
-
"type": "string"
|
|
154
|
-
},
|
|
155
|
-
"type": {
|
|
156
|
-
"type": "string",
|
|
157
|
-
"enum": ["collection"]
|
|
158
|
-
},
|
|
159
|
-
"fields": {
|
|
160
|
-
"type": "object",
|
|
161
|
-
"patternProperties": {
|
|
162
|
-
"^[a-zA-Z0-9_]+$": {
|
|
163
|
-
"type": "object",
|
|
164
|
-
"properties": {
|
|
165
|
-
"type": {
|
|
166
|
-
"type": "string",
|
|
167
|
-
"enum": ["string", "boolean", "number", "json"]
|
|
168
|
-
},
|
|
169
|
-
"indexed": {
|
|
170
|
-
"type": "boolean"
|
|
171
|
-
},
|
|
172
|
-
"required": {
|
|
173
|
-
"type": "boolean"
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
"required": ["type"]
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
"additionalProperties": true
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
"required": ["fields"]
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
"additionalProperties": true
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
"required": ["project_id", "version", "tables"]
|
|
189
|
-
};
|
|
190
|
-
var ajv = new Ajv();
|
|
191
|
-
var validator = ajv.compile(basicJsonSchema);
|
|
192
|
-
function validateSchema(schema) {
|
|
193
|
-
const v = validator(schema);
|
|
194
|
-
return {
|
|
195
|
-
valid: v,
|
|
196
|
-
errors: validator.errors || []
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
function validateData(schema, table, data, checkRequired = true) {
|
|
200
|
-
const valid = validateSchema(schema);
|
|
201
|
-
if (!valid.valid) {
|
|
202
|
-
return { valid: false, errors: valid.errors, message: "Schema is invalid" };
|
|
203
|
-
}
|
|
204
|
-
const tableSchema = schema.tables[table];
|
|
205
|
-
if (!tableSchema) {
|
|
206
|
-
return { valid: false, errors: [{ message: `Table ${table} not found in schema` }], message: "Table not found" };
|
|
207
|
-
}
|
|
208
|
-
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
209
|
-
const fieldSchema = tableSchema.fields[fieldName];
|
|
210
|
-
if (!fieldSchema) {
|
|
211
|
-
return {
|
|
212
|
-
valid: false,
|
|
213
|
-
errors: [{ message: `Field ${fieldName} not found in schema` }],
|
|
214
|
-
message: "Invalid field"
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
const schemaType = fieldSchema.type;
|
|
218
|
-
const valueType = typeof fieldValue;
|
|
219
|
-
if (schemaType === "string" && valueType !== "string" || schemaType === "number" && valueType !== "number" || schemaType === "boolean" && valueType !== "boolean" || schemaType === "json" && valueType !== "object") {
|
|
220
|
-
return {
|
|
221
|
-
valid: false,
|
|
222
|
-
errors: [{
|
|
223
|
-
message: `Field ${fieldName} should be type ${schemaType}, got ${valueType}`
|
|
224
|
-
}],
|
|
225
|
-
message: "invalid type"
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (checkRequired) {
|
|
230
|
-
for (const [fieldName, fieldSchema] of Object.entries(tableSchema.fields)) {
|
|
231
|
-
if (fieldSchema.required && !data[fieldName]) {
|
|
232
|
-
return { valid: false, errors: [{ message: `Field ${fieldName} is required` }], message: "Required field missing" };
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return { valid: true, errors: [] };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
130
|
// src/sync/index.ts
|
|
131
|
+
import { validateData } from "@basictech/schema";
|
|
240
132
|
syncProtocol();
|
|
241
133
|
var BasicSync = class extends Dexie2 {
|
|
242
134
|
basic_schema;
|
|
@@ -358,60 +250,490 @@ var BasicSync = class extends Dexie2 {
|
|
|
358
250
|
}
|
|
359
251
|
};
|
|
360
252
|
|
|
361
|
-
// src/
|
|
362
|
-
var
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
}
|
|
368
277
|
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const response = await fetch(url, {
|
|
375
|
-
method: "POST",
|
|
376
|
-
headers: {
|
|
377
|
-
"Content-Type": "application/json",
|
|
378
|
-
"Authorization": `Bearer ${token}`
|
|
379
|
-
},
|
|
380
|
-
body: JSON.stringify({ "value": value })
|
|
381
|
-
});
|
|
382
|
-
return response.json();
|
|
383
|
-
}
|
|
384
|
-
async function update({ projectId, accountId, tableName, id, value, token }) {
|
|
385
|
-
const url = `${baseUrl}/project/${projectId}/db/${accountId}/${tableName}/${id}`;
|
|
386
|
-
const response = await fetch(url, {
|
|
387
|
-
method: "PATCH",
|
|
388
|
-
headers: {
|
|
389
|
-
"Content-Type": "application/json",
|
|
390
|
-
"Authorization": `Bearer ${token}`
|
|
391
|
-
},
|
|
392
|
-
body: JSON.stringify({ id, value })
|
|
393
|
-
});
|
|
394
|
-
return response.json();
|
|
395
|
-
}
|
|
396
|
-
async function deleteRecord({ projectId, accountId, tableName, id, token }) {
|
|
397
|
-
const url = `${baseUrl}/project/${projectId}/db/${accountId}/${tableName}/${id}`;
|
|
398
|
-
const response = await fetch(url, {
|
|
399
|
-
method: "DELETE",
|
|
400
|
-
headers: {
|
|
401
|
-
"Authorization": `Bearer ${token}`
|
|
278
|
+
}
|
|
279
|
+
// Validate operator based on field type
|
|
280
|
+
validateOperator(field, operator, value) {
|
|
281
|
+
if (!this.tableSchema || this.reservedFields.includes(field)) {
|
|
282
|
+
return;
|
|
402
283
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
};
|
|
409
607
|
|
|
410
608
|
// package.json
|
|
411
|
-
var version = "0.
|
|
609
|
+
var version = "0.7.0-beta.1";
|
|
412
610
|
|
|
413
|
-
// src/
|
|
414
|
-
|
|
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);
|
|
719
|
+
}
|
|
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
|
+
];
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/utils/storage.ts
|
|
415
737
|
var LocalStorageAdapter = class {
|
|
416
738
|
async get(key) {
|
|
417
739
|
return localStorage.getItem(key);
|
|
@@ -423,25 +745,113 @@ var LocalStorageAdapter = class {
|
|
|
423
745
|
localStorage.removeItem(key);
|
|
424
746
|
}
|
|
425
747
|
};
|
|
426
|
-
var
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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;
|
|
791
|
+
}
|
|
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");
|
|
798
|
+
}
|
|
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
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/utils/schema.ts
|
|
851
|
+
import { validateSchema as validateSchema2, compareSchemas } from "@basictech/schema";
|
|
441
852
|
async function getSchemaStatus(schema) {
|
|
442
853
|
const projectId = schema.project_id;
|
|
443
|
-
|
|
444
|
-
const valid = validateSchema3(schema);
|
|
854
|
+
const valid = validateSchema2(schema);
|
|
445
855
|
if (!valid.valid) {
|
|
446
856
|
console.warn("BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled");
|
|
447
857
|
return {
|
|
@@ -503,54 +913,55 @@ async function getSchemaStatus(schema) {
|
|
|
503
913
|
};
|
|
504
914
|
}
|
|
505
915
|
}
|
|
506
|
-
function
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
case 4:
|
|
519
|
-
return "ERROR_WILL_RETRY";
|
|
520
|
-
default:
|
|
521
|
-
return "UNKNOWN";
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
async function checkForNewVersion() {
|
|
525
|
-
try {
|
|
526
|
-
const isBeta = version.includes("beta");
|
|
527
|
-
const response = await fetch(`https://registry.npmjs.org/@basictech/react/${isBeta ? "beta" : "latest"}`);
|
|
528
|
-
if (!response.ok) {
|
|
529
|
-
throw new Error("Failed to fetch version from npm");
|
|
530
|
-
}
|
|
531
|
-
const data = await response.json();
|
|
532
|
-
const latestVersion = data.version;
|
|
533
|
-
if (latestVersion !== version) {
|
|
534
|
-
console.warn("[basic] New version available:", latestVersion, `
|
|
535
|
-
run "npm install @basictech/react@${latestVersion}" to update`);
|
|
536
|
-
}
|
|
537
|
-
if (isBeta) {
|
|
538
|
-
log("thank you for being on basictech/react beta :)");
|
|
539
|
-
}
|
|
540
|
-
return {
|
|
541
|
-
hasNewVersion: version !== latestVersion,
|
|
542
|
-
latestVersion,
|
|
543
|
-
currentVersion: version
|
|
544
|
-
};
|
|
545
|
-
} catch (error) {
|
|
546
|
-
log("Error checking for new version:", error);
|
|
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();
|
|
547
928
|
return {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
929
|
+
isValid: false,
|
|
930
|
+
schemaStatus: { valid: false },
|
|
931
|
+
errors: valid.errors
|
|
551
932
|
};
|
|
552
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
|
+
};
|
|
553
945
|
}
|
|
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
|
+
});
|
|
554
965
|
function BasicProvider({
|
|
555
966
|
children,
|
|
556
967
|
project_id,
|
|
@@ -569,25 +980,10 @@ function BasicProvider({
|
|
|
569
980
|
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
570
981
|
const [pendingRefresh, setPendingRefresh] = useState(false);
|
|
571
982
|
const syncRef = useRef(null);
|
|
983
|
+
const remoteDbRef = useRef(null);
|
|
572
984
|
const storageAdapter = storage || new LocalStorageAdapter();
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
USER_INFO: "basic_user_info",
|
|
576
|
-
AUTH_STATE: "basic_auth_state",
|
|
577
|
-
DEBUG: "basic_debug"
|
|
578
|
-
};
|
|
579
|
-
const isDevelopment = () => {
|
|
580
|
-
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;
|
|
581
|
-
};
|
|
582
|
-
const cleanOAuthParamsFromUrl = () => {
|
|
583
|
-
if (window.location.search.includes("code") || window.location.search.includes("state")) {
|
|
584
|
-
const url = new URL(window.location.href);
|
|
585
|
-
url.searchParams.delete("code");
|
|
586
|
-
url.searchParams.delete("state");
|
|
587
|
-
window.history.pushState({}, document.title, url.pathname + url.search);
|
|
588
|
-
log("Cleaned OAuth parameters from URL");
|
|
589
|
-
}
|
|
590
|
-
};
|
|
985
|
+
const isDevMode = () => isDevelopment(debug);
|
|
986
|
+
const cleanOAuthParams = () => cleanOAuthParamsFromUrl();
|
|
591
987
|
useEffect(() => {
|
|
592
988
|
const handleOnline = () => {
|
|
593
989
|
log("Network came back online");
|
|
@@ -624,9 +1020,6 @@ function BasicProvider({
|
|
|
624
1020
|
syncRef.current.syncable.on("statusChanged", (status, url) => {
|
|
625
1021
|
setDbStatus(getSyncStatus(status));
|
|
626
1022
|
});
|
|
627
|
-
syncRef.current.syncable.getStatus().then((status) => {
|
|
628
|
-
setDbStatus(getSyncStatus(status));
|
|
629
|
-
});
|
|
630
1023
|
if (options.shouldConnect) {
|
|
631
1024
|
setShouldConnect(true);
|
|
632
1025
|
} else {
|
|
@@ -636,17 +1029,15 @@ function BasicProvider({
|
|
|
636
1029
|
}
|
|
637
1030
|
}
|
|
638
1031
|
async function checkSchema() {
|
|
639
|
-
const
|
|
640
|
-
if (!
|
|
641
|
-
log("Basic Schema is invalid!", valid.errors);
|
|
642
|
-
console.group("Schema Errors");
|
|
1032
|
+
const result = await validateAndCheckSchema(schema);
|
|
1033
|
+
if (!result.isValid) {
|
|
643
1034
|
let errorMessage = "";
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1035
|
+
if (result.errors) {
|
|
1036
|
+
result.errors.forEach((error2, index) => {
|
|
1037
|
+
errorMessage += `${index + 1}: ${error2.message} - at ${error2.instancePath}
|
|
647
1038
|
`;
|
|
648
|
-
|
|
649
|
-
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
650
1041
|
setError({
|
|
651
1042
|
code: "schema_invalid",
|
|
652
1043
|
title: "Basic Schema is invalid!",
|
|
@@ -655,17 +1046,10 @@ function BasicProvider({
|
|
|
655
1046
|
setIsReady(true);
|
|
656
1047
|
return null;
|
|
657
1048
|
}
|
|
658
|
-
|
|
659
|
-
if (schema.version !== 0) {
|
|
660
|
-
schemaStatus = await getSchemaStatus(schema);
|
|
661
|
-
log("schemaStatus", schemaStatus);
|
|
662
|
-
} else {
|
|
663
|
-
log("schema not published - at version 0");
|
|
664
|
-
}
|
|
665
|
-
if (schemaStatus.valid) {
|
|
1049
|
+
if (result.schemaStatus.valid) {
|
|
666
1050
|
initDb({ shouldConnect: true });
|
|
667
1051
|
} else {
|
|
668
|
-
log("Schema is invalid!", schemaStatus);
|
|
1052
|
+
log("Schema is invalid!", result.schemaStatus);
|
|
669
1053
|
initDb({ shouldConnect: false });
|
|
670
1054
|
}
|
|
671
1055
|
checkForNewVersion();
|
|
@@ -677,43 +1061,72 @@ function BasicProvider({
|
|
|
677
1061
|
}
|
|
678
1062
|
}, []);
|
|
679
1063
|
useEffect(() => {
|
|
680
|
-
|
|
681
|
-
|
|
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
|
+
}
|
|
682
1076
|
}
|
|
1077
|
+
connectToDb();
|
|
683
1078
|
}, [isSignedIn, shouldConnect]);
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1079
|
+
useEffect(() => {
|
|
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
|
+
});
|
|
689
1088
|
}
|
|
690
|
-
|
|
691
|
-
syncRef.current.connect({ access_token: tok }).catch((e) => {
|
|
692
|
-
log("error connecting to db", e);
|
|
693
|
-
});
|
|
694
|
-
};
|
|
1089
|
+
}, [token, project_id, schema]);
|
|
695
1090
|
useEffect(() => {
|
|
696
1091
|
const initializeAuth = async () => {
|
|
697
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`);
|
|
1100
|
+
}
|
|
1101
|
+
} catch (error2) {
|
|
1102
|
+
log("Version update failed:", error2);
|
|
1103
|
+
}
|
|
698
1104
|
try {
|
|
699
1105
|
if (window.location.search.includes("code")) {
|
|
700
|
-
let code = window.location?.search?.split("code=")[1]
|
|
1106
|
+
let code = window.location?.search?.split("code=")[1]?.split("&")[0];
|
|
1107
|
+
if (!code)
|
|
1108
|
+
return;
|
|
701
1109
|
const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE);
|
|
702
|
-
|
|
1110
|
+
const urlState = window.location.search.split("state=")[1]?.split("&")[0];
|
|
1111
|
+
if (!state || state !== urlState) {
|
|
703
1112
|
log("error: auth state does not match");
|
|
704
1113
|
setIsAuthReady(true);
|
|
705
1114
|
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
706
|
-
|
|
1115
|
+
cleanOAuthParams();
|
|
707
1116
|
return;
|
|
708
1117
|
}
|
|
709
1118
|
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
710
|
-
|
|
711
|
-
fetchToken(code)
|
|
1119
|
+
cleanOAuthParams();
|
|
1120
|
+
fetchToken(code).catch((error2) => {
|
|
1121
|
+
log("Error fetching token:", error2);
|
|
1122
|
+
});
|
|
712
1123
|
} else {
|
|
713
1124
|
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
714
1125
|
if (refreshToken) {
|
|
715
1126
|
log("Found refresh token in storage, attempting to refresh access token");
|
|
716
|
-
fetchToken(refreshToken)
|
|
1127
|
+
fetchToken(refreshToken).catch((error2) => {
|
|
1128
|
+
log("Error fetching refresh token:", error2);
|
|
1129
|
+
});
|
|
717
1130
|
} else {
|
|
718
1131
|
let cookie_token = getCookie("basic_token");
|
|
719
1132
|
if (cookie_token !== "") {
|
|
@@ -762,8 +1175,8 @@ function BasicProvider({
|
|
|
762
1175
|
}
|
|
763
1176
|
await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user2));
|
|
764
1177
|
log("Cached user info in storage");
|
|
765
|
-
|
|
766
|
-
|
|
1178
|
+
setCookie("basic_access_token", token?.access_token || "", { httpOnly: false });
|
|
1179
|
+
setCookie("basic_token", JSON.stringify(token));
|
|
767
1180
|
setUser(user2);
|
|
768
1181
|
setIsSignedIn(true);
|
|
769
1182
|
setIsAuthReady(true);
|
|
@@ -780,19 +1193,19 @@ function BasicProvider({
|
|
|
780
1193
|
if (isExpired) {
|
|
781
1194
|
log("token is expired - refreshing ...");
|
|
782
1195
|
try {
|
|
783
|
-
const newToken = await fetchToken(token?.refresh_token);
|
|
784
|
-
fetchUser(newToken
|
|
1196
|
+
const newToken = await fetchToken(token?.refresh_token || "");
|
|
1197
|
+
fetchUser(newToken?.access_token || "");
|
|
785
1198
|
} catch (error2) {
|
|
786
1199
|
log("Failed to refresh token in checkToken:", error2);
|
|
787
1200
|
if (error2.message.includes("offline") || error2.message.includes("Network")) {
|
|
788
1201
|
log("Network issue - continuing with expired token until online");
|
|
789
|
-
fetchUser(token
|
|
1202
|
+
fetchUser(token?.access_token || "");
|
|
790
1203
|
} else {
|
|
791
1204
|
setIsAuthReady(true);
|
|
792
1205
|
}
|
|
793
1206
|
}
|
|
794
1207
|
} else {
|
|
795
|
-
fetchUser(token
|
|
1208
|
+
fetchUser(token?.access_token || "");
|
|
796
1209
|
}
|
|
797
1210
|
}
|
|
798
1211
|
if (token) {
|
|
@@ -811,14 +1224,14 @@ function BasicProvider({
|
|
|
811
1224
|
if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
|
|
812
1225
|
throw new Error("Invalid redirect URI provided");
|
|
813
1226
|
}
|
|
814
|
-
let
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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}`;
|
|
820
1233
|
log("Generated sign-in link successfully");
|
|
821
|
-
return
|
|
1234
|
+
return baseUrl;
|
|
822
1235
|
} catch (error2) {
|
|
823
1236
|
log("Error generating sign-in link:", error2);
|
|
824
1237
|
throw error2;
|
|
@@ -840,7 +1253,7 @@ function BasicProvider({
|
|
|
840
1253
|
window.location.href = signInLink;
|
|
841
1254
|
} catch (error2) {
|
|
842
1255
|
log("Error during sign-in:", error2);
|
|
843
|
-
if (
|
|
1256
|
+
if (isDevMode()) {
|
|
844
1257
|
setError({
|
|
845
1258
|
code: "signin_error",
|
|
846
1259
|
title: "Sign-in Failed",
|
|
@@ -864,7 +1277,7 @@ function BasicProvider({
|
|
|
864
1277
|
}
|
|
865
1278
|
}
|
|
866
1279
|
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
867
|
-
|
|
1280
|
+
cleanOAuthParams();
|
|
868
1281
|
const token2 = await fetchToken(code);
|
|
869
1282
|
if (token2) {
|
|
870
1283
|
log("signinWithCode successful");
|
|
@@ -885,16 +1298,16 @@ function BasicProvider({
|
|
|
885
1298
|
setUser({});
|
|
886
1299
|
setIsSignedIn(false);
|
|
887
1300
|
setToken(null);
|
|
888
|
-
|
|
889
|
-
|
|
1301
|
+
clearCookie("basic_token");
|
|
1302
|
+
clearCookie("basic_access_token");
|
|
890
1303
|
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
891
1304
|
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
892
1305
|
await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
|
|
893
1306
|
if (syncRef.current) {
|
|
894
1307
|
(async () => {
|
|
895
1308
|
try {
|
|
896
|
-
await syncRef.current
|
|
897
|
-
await syncRef.current
|
|
1309
|
+
await syncRef.current?.close();
|
|
1310
|
+
await syncRef.current?.delete({ disableAutoOpen: false });
|
|
898
1311
|
syncRef.current = null;
|
|
899
1312
|
window?.location?.reload();
|
|
900
1313
|
} catch (error2) {
|
|
@@ -953,20 +1366,6 @@ function BasicProvider({
|
|
|
953
1366
|
}
|
|
954
1367
|
return token?.access_token || "";
|
|
955
1368
|
};
|
|
956
|
-
function getCookie(name) {
|
|
957
|
-
let cookieValue = "";
|
|
958
|
-
if (document.cookie && document.cookie !== "") {
|
|
959
|
-
const cookies = document.cookie.split(";");
|
|
960
|
-
for (let i = 0; i < cookies.length; i++) {
|
|
961
|
-
const cookie = cookies[i].trim();
|
|
962
|
-
if (cookie.substring(0, name.length + 1) === name + "=") {
|
|
963
|
-
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
964
|
-
break;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return cookieValue;
|
|
969
|
-
}
|
|
970
1369
|
const fetchToken = async (code) => {
|
|
971
1370
|
try {
|
|
972
1371
|
if (!isOnline) {
|
|
@@ -996,8 +1395,8 @@ function BasicProvider({
|
|
|
996
1395
|
}
|
|
997
1396
|
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
998
1397
|
await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
|
|
999
|
-
|
|
1000
|
-
|
|
1398
|
+
clearCookie("basic_token");
|
|
1399
|
+
clearCookie("basic_access_token");
|
|
1001
1400
|
setUser({});
|
|
1002
1401
|
setIsSignedIn(false);
|
|
1003
1402
|
setToken(null);
|
|
@@ -1010,7 +1409,7 @@ function BasicProvider({
|
|
|
1010
1409
|
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token2.refresh_token);
|
|
1011
1410
|
log("Updated refresh token in storage");
|
|
1012
1411
|
}
|
|
1013
|
-
|
|
1412
|
+
setCookie("basic_access_token", token2.access_token, { httpOnly: false });
|
|
1014
1413
|
log("Updated access token in cookie");
|
|
1015
1414
|
}
|
|
1016
1415
|
return token2;
|
|
@@ -1019,8 +1418,8 @@ function BasicProvider({
|
|
|
1019
1418
|
if (!error2.message.includes("offline") && !error2.message.includes("Network")) {
|
|
1020
1419
|
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1021
1420
|
await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
|
|
1022
|
-
|
|
1023
|
-
|
|
1421
|
+
clearCookie("basic_token");
|
|
1422
|
+
clearCookie("basic_access_token");
|
|
1024
1423
|
setUser({});
|
|
1025
1424
|
setIsSignedIn(false);
|
|
1026
1425
|
setToken(null);
|
|
@@ -1029,35 +1428,6 @@ function BasicProvider({
|
|
|
1029
1428
|
throw error2;
|
|
1030
1429
|
}
|
|
1031
1430
|
};
|
|
1032
|
-
const db_ = (tableName) => {
|
|
1033
|
-
const checkSignIn = () => {
|
|
1034
|
-
if (!isSignedIn) {
|
|
1035
|
-
throw new Error("cannot use db. user not logged in.");
|
|
1036
|
-
}
|
|
1037
|
-
};
|
|
1038
|
-
return {
|
|
1039
|
-
get: async () => {
|
|
1040
|
-
checkSignIn();
|
|
1041
|
-
const tok = await getToken();
|
|
1042
|
-
return get({ projectId: project_id, accountId: user.id, tableName, token: tok });
|
|
1043
|
-
},
|
|
1044
|
-
add: async (value) => {
|
|
1045
|
-
checkSignIn();
|
|
1046
|
-
const tok = await getToken();
|
|
1047
|
-
return add({ projectId: project_id, accountId: user.id, tableName, value, token: tok });
|
|
1048
|
-
},
|
|
1049
|
-
update: async (id, value) => {
|
|
1050
|
-
checkSignIn();
|
|
1051
|
-
const tok = await getToken();
|
|
1052
|
-
return update({ projectId: project_id, accountId: user.id, tableName, id, value, token: tok });
|
|
1053
|
-
},
|
|
1054
|
-
delete: async (id) => {
|
|
1055
|
-
checkSignIn();
|
|
1056
|
-
const tok = await getToken();
|
|
1057
|
-
return deleteRecord({ projectId: project_id, accountId: user.id, tableName, id, token: tok });
|
|
1058
|
-
}
|
|
1059
|
-
};
|
|
1060
|
-
};
|
|
1061
1431
|
const noDb = {
|
|
1062
1432
|
collection: () => {
|
|
1063
1433
|
throw new Error("no basicdb found - initialization failed. double check your schema.");
|
|
@@ -1074,9 +1444,10 @@ function BasicProvider({
|
|
|
1074
1444
|
getToken,
|
|
1075
1445
|
getSignInLink,
|
|
1076
1446
|
db: syncRef.current ? syncRef.current : noDb,
|
|
1447
|
+
remoteDb: remoteDbRef.current ? remoteDbRef.current : noDb,
|
|
1077
1448
|
dbStatus
|
|
1078
1449
|
}, children: [
|
|
1079
|
-
error &&
|
|
1450
|
+
error && isDevMode() && /* @__PURE__ */ jsx(ErrorDisplay, { error }),
|
|
1080
1451
|
isReady && children
|
|
1081
1452
|
] });
|
|
1082
1453
|
}
|
|
@@ -1110,6 +1481,7 @@ function useBasic() {
|
|
|
1110
1481
|
// src/index.ts
|
|
1111
1482
|
import { useLiveQuery as useQuery } from "dexie-react-hooks";
|
|
1112
1483
|
export {
|
|
1484
|
+
BasicDBSDK,
|
|
1113
1485
|
BasicProvider,
|
|
1114
1486
|
useBasic,
|
|
1115
1487
|
useQuery
|