@basictech/react 0.7.0-beta.6 → 0.7.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/changelog.md +6 -0
- package/dist/index.d.mts +266 -13
- package/dist/index.d.ts +266 -13
- package/dist/index.js +645 -184
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +625 -172
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/readme.md +203 -209
- package/src/AuthContext.tsx +183 -53
- package/src/core/db/RemoteCollection.ts +294 -0
- package/src/core/db/RemoteDB.ts +40 -0
- package/src/core/db/index.ts +7 -0
- package/src/core/db/types.ts +128 -0
- package/src/index.ts +25 -9
- package/src/sync/index.ts +133 -54
- package/src/db.ts +0 -55
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core DB types for Basic SDK
|
|
3
|
+
* These interfaces are implemented by both SyncDB (Dexie-based) and RemoteDB (REST-based)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Collection interface for CRUD operations on a table
|
|
8
|
+
* All write operations return the full object (not just the id)
|
|
9
|
+
*/
|
|
10
|
+
export interface Collection<T extends { id: string } = Record<string, any> & { id: string }> {
|
|
11
|
+
/**
|
|
12
|
+
* Add a new record to the collection
|
|
13
|
+
* @param data - The data to add (without id, which will be generated)
|
|
14
|
+
* @returns The created object with its generated id
|
|
15
|
+
*/
|
|
16
|
+
add(data: Omit<T, 'id'>): Promise<T>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Put (upsert) a record - requires id
|
|
20
|
+
* @param data - The full object including id
|
|
21
|
+
* @returns The upserted object
|
|
22
|
+
*/
|
|
23
|
+
put(data: T): Promise<T>
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Update an existing record by id
|
|
27
|
+
* @param id - The record id to update
|
|
28
|
+
* @param data - Partial data to merge
|
|
29
|
+
* @returns The updated object, or null if not found
|
|
30
|
+
*/
|
|
31
|
+
update(id: string, data: Partial<Omit<T, 'id'>>): Promise<T | null>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Delete a record by id
|
|
35
|
+
* @param id - The record id to delete
|
|
36
|
+
* @returns true if deleted, false if not found
|
|
37
|
+
*/
|
|
38
|
+
delete(id: string): Promise<boolean>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a single record by id
|
|
42
|
+
* @param id - The record id to fetch
|
|
43
|
+
* @returns The object or null if not found
|
|
44
|
+
*/
|
|
45
|
+
get(id: string): Promise<T | null>
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get all records in the collection
|
|
49
|
+
* @returns Array of all objects
|
|
50
|
+
*/
|
|
51
|
+
getAll(): Promise<T[]>
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Filter records using a predicate function
|
|
55
|
+
* @param fn - Filter function that returns true for matches
|
|
56
|
+
* @returns Array of matching objects
|
|
57
|
+
*/
|
|
58
|
+
filter(fn: (item: T) => boolean): Promise<T[]>
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Direct access to underlying storage (optional)
|
|
62
|
+
* For sync mode: Dexie table reference
|
|
63
|
+
* For remote mode: undefined
|
|
64
|
+
*/
|
|
65
|
+
ref?: any
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* BasicDB interface - factory for creating collections
|
|
70
|
+
*/
|
|
71
|
+
export interface BasicDB {
|
|
72
|
+
/**
|
|
73
|
+
* Get a collection by name
|
|
74
|
+
* @param name - The table/collection name (must match schema)
|
|
75
|
+
* @returns A Collection instance for CRUD operations
|
|
76
|
+
*/
|
|
77
|
+
collection<T extends { id: string } = Record<string, any> & { id: string }>(name: string): Collection<T>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Database mode - determines which implementation is used
|
|
82
|
+
* - 'sync': Uses Dexie + WebSocket for local-first sync (default)
|
|
83
|
+
* - 'remote': Uses REST API calls directly to server
|
|
84
|
+
*/
|
|
85
|
+
export type DBMode = 'sync' | 'remote'
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Auth error information passed to onAuthError callback
|
|
89
|
+
*/
|
|
90
|
+
export interface AuthError {
|
|
91
|
+
status: number
|
|
92
|
+
message: string
|
|
93
|
+
response?: any
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Custom error class for Remote DB API errors
|
|
98
|
+
* Includes HTTP status code for reliable error handling
|
|
99
|
+
*/
|
|
100
|
+
export class RemoteDBError extends Error {
|
|
101
|
+
status: number
|
|
102
|
+
response?: any
|
|
103
|
+
|
|
104
|
+
constructor(message: string, status: number, response?: any) {
|
|
105
|
+
super(message)
|
|
106
|
+
this.name = 'RemoteDBError'
|
|
107
|
+
this.status = status
|
|
108
|
+
this.response = response
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Configuration for RemoteDB
|
|
114
|
+
*/
|
|
115
|
+
export interface RemoteDBConfig {
|
|
116
|
+
serverUrl: string
|
|
117
|
+
projectId: string
|
|
118
|
+
getToken: () => Promise<string>
|
|
119
|
+
schema?: any
|
|
120
|
+
/** Enable debug logging (default: false) */
|
|
121
|
+
debug?: boolean
|
|
122
|
+
/**
|
|
123
|
+
* Optional callback when authentication fails (401 error after retry)
|
|
124
|
+
* Use this to show login UI or redirect to sign-in
|
|
125
|
+
*/
|
|
126
|
+
onAuthError?: (error: AuthError) => void
|
|
127
|
+
}
|
|
128
|
+
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useBasic, BasicProvider, BasicStorage, LocalStorageAdapter, AuthConfig, BasicProviderProps } from "./AuthContext";
|
|
1
|
+
import { useBasic, BasicProvider } from "./AuthContext";
|
|
3
2
|
import { useLiveQuery as useQuery } from "dexie-react-hooks";
|
|
4
|
-
// import { createVersionUpdater, VersionUpdater, Migration } from "./versionUpdater";
|
|
5
3
|
|
|
4
|
+
// Re-export from AuthContext
|
|
5
|
+
export { useBasic, BasicProvider, useQuery }
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
// Type exports
|
|
8
|
+
export type {
|
|
9
|
+
AuthConfig,
|
|
10
|
+
BasicStorage,
|
|
11
|
+
LocalStorageAdapter,
|
|
12
|
+
BasicProviderProps,
|
|
13
|
+
BasicContextType,
|
|
14
|
+
AuthResult
|
|
15
|
+
} from "./AuthContext"
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
// Core DB exports
|
|
18
|
+
export type {
|
|
19
|
+
DBMode,
|
|
20
|
+
BasicDB,
|
|
21
|
+
Collection,
|
|
22
|
+
RemoteDBConfig,
|
|
23
|
+
AuthError
|
|
24
|
+
} from "./core/db"
|
|
25
|
+
|
|
26
|
+
export { RemoteDB, RemoteCollection, RemoteDBError, NotAuthenticatedError } from "./core/db"
|
|
27
|
+
|
|
28
|
+
// Storage utilities
|
|
29
|
+
export { STORAGE_KEYS } from "./utils/storage"
|
package/src/sync/index.ts
CHANGED
|
@@ -1,28 +1,55 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { v7 as uuidv7 } from 'uuid';
|
|
4
|
-
import { Dexie
|
|
5
|
-
import 'dexie-syncable';
|
|
6
|
-
import 'dexie-observable';
|
|
7
|
-
|
|
8
|
-
import { syncProtocol } from './syncProtocol'
|
|
9
|
-
import { log } from '../config'
|
|
4
|
+
import { Dexie } from 'dexie';
|
|
10
5
|
|
|
6
|
+
import { log } from '../config'
|
|
11
7
|
import { validateSchema, validateData } from '@basictech/schema'
|
|
12
|
-
syncProtocol()
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// const DexieSyncStatus = {
|
|
16
|
-
// "-1": "ERROR",
|
|
17
|
-
// "0": "OFFLINE",
|
|
18
|
-
// "1": "CONNECTING",
|
|
19
|
-
// "2": "ONLINE",
|
|
20
|
-
// "3": "SYNCING",
|
|
21
|
-
// "4": "ERROR_WILL_RETRY"
|
|
22
|
-
// }
|
|
23
8
|
|
|
9
|
+
// Track initialization state
|
|
10
|
+
let dexieExtensionsLoaded = false;
|
|
11
|
+
let initPromise: Promise<void> | null = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Initialize Dexie extensions (syncable and observable)
|
|
15
|
+
* This must be called before creating a BasicSync instance
|
|
16
|
+
* Safe to call multiple times - will only load once
|
|
17
|
+
*/
|
|
18
|
+
export async function initDexieExtensions(): Promise<void> {
|
|
19
|
+
// Return early if already loaded or not in browser
|
|
20
|
+
if (dexieExtensionsLoaded) return;
|
|
21
|
+
if (typeof window === 'undefined') return;
|
|
22
|
+
|
|
23
|
+
// If already initializing, wait for that promise
|
|
24
|
+
if (initPromise) return initPromise;
|
|
25
|
+
|
|
26
|
+
initPromise = (async () => {
|
|
27
|
+
try {
|
|
28
|
+
// Dynamic imports - only loaded in browser
|
|
29
|
+
await import('dexie-syncable');
|
|
30
|
+
await import('dexie-observable');
|
|
31
|
+
|
|
32
|
+
// Import and register sync protocol
|
|
33
|
+
const { syncProtocol } = await import('./syncProtocol');
|
|
34
|
+
syncProtocol();
|
|
35
|
+
|
|
36
|
+
dexieExtensionsLoaded = true;
|
|
37
|
+
log('Dexie extensions loaded successfully');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Failed to load Dexie extensions:', error);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
|
|
44
|
+
return initPromise;
|
|
45
|
+
}
|
|
24
46
|
|
|
25
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Check if Dexie extensions are loaded
|
|
49
|
+
*/
|
|
50
|
+
export function isDexieReady(): boolean {
|
|
51
|
+
return dexieExtensionsLoaded;
|
|
52
|
+
}
|
|
26
53
|
|
|
27
54
|
|
|
28
55
|
export class BasicSync extends Dexie {
|
|
@@ -141,83 +168,135 @@ export class BasicSync extends Dexie {
|
|
|
141
168
|
return this.syncable
|
|
142
169
|
}
|
|
143
170
|
|
|
144
|
-
collection(name: string) {
|
|
145
|
-
//
|
|
171
|
+
collection<T extends { id: string } = Record<string, any> & { id: string }>(name: string) {
|
|
172
|
+
// Validate table exists in schema
|
|
173
|
+
if (this.basic_schema?.tables && !this.basic_schema.tables[name]) {
|
|
174
|
+
throw new Error(`Table "${name}" not found in schema`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const table = this.table(name)
|
|
146
178
|
|
|
147
179
|
return {
|
|
148
|
-
|
|
149
180
|
/**
|
|
150
181
|
* Returns the underlying Dexie table
|
|
151
182
|
* @type {Dexie.Table}
|
|
152
183
|
*/
|
|
153
|
-
ref:
|
|
184
|
+
ref: table,
|
|
154
185
|
|
|
155
186
|
// --- WRITE ---- //
|
|
156
|
-
add: (data: any) => {
|
|
157
|
-
// log("Adding data to", name, data)
|
|
158
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Add a new record - returns the full object with generated id
|
|
190
|
+
*/
|
|
191
|
+
add: async (data: Omit<T, 'id'>): Promise<T> => {
|
|
159
192
|
const valid = validateData(this.basic_schema, name, data)
|
|
160
193
|
if (!valid.valid) {
|
|
161
194
|
log('Invalid data', valid)
|
|
162
|
-
|
|
195
|
+
throw new Error(valid.message || 'Data validation failed')
|
|
163
196
|
}
|
|
164
197
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
198
|
+
const id = uuidv7()
|
|
199
|
+
const fullData = { id, ...data } as T
|
|
200
|
+
|
|
201
|
+
await table.add(fullData)
|
|
202
|
+
return fullData
|
|
170
203
|
},
|
|
171
204
|
|
|
172
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Put (upsert) a record - returns the full object
|
|
207
|
+
*/
|
|
208
|
+
put: async (data: T): Promise<T> => {
|
|
209
|
+
if (!data.id) {
|
|
210
|
+
throw new Error('put() requires an id field')
|
|
211
|
+
}
|
|
212
|
+
|
|
173
213
|
const valid = validateData(this.basic_schema, name, data)
|
|
174
214
|
if (!valid.valid) {
|
|
175
215
|
log('Invalid data', valid)
|
|
176
|
-
|
|
216
|
+
throw new Error(valid.message || 'Data validation failed')
|
|
177
217
|
}
|
|
178
218
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
...data
|
|
182
|
-
})
|
|
219
|
+
await table.put(data)
|
|
220
|
+
return data
|
|
183
221
|
},
|
|
184
222
|
|
|
185
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Update an existing record - returns updated object or null
|
|
225
|
+
*/
|
|
226
|
+
update: async (id: string, data: Partial<Omit<T, 'id'>>): Promise<T | null> => {
|
|
227
|
+
if (!id) {
|
|
228
|
+
throw new Error('update() requires an id')
|
|
229
|
+
}
|
|
230
|
+
|
|
186
231
|
const valid = validateData(this.basic_schema, name, data, false)
|
|
187
232
|
if (!valid.valid) {
|
|
188
233
|
log('Invalid data', valid)
|
|
189
|
-
|
|
234
|
+
throw new Error(valid.message || 'Data validation failed')
|
|
190
235
|
}
|
|
191
236
|
|
|
192
|
-
|
|
193
|
-
|
|
237
|
+
const updated = await table.update(id, data)
|
|
238
|
+
if (updated === 0) {
|
|
239
|
+
return null
|
|
240
|
+
}
|
|
194
241
|
|
|
195
|
-
|
|
196
|
-
|
|
242
|
+
// Fetch and return the updated record
|
|
243
|
+
const record = await table.get(id)
|
|
244
|
+
return (record as T) || null
|
|
197
245
|
},
|
|
198
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Delete a record - returns true if deleted, false if not found
|
|
249
|
+
*/
|
|
250
|
+
delete: async (id: string): Promise<boolean> => {
|
|
251
|
+
if (!id) {
|
|
252
|
+
throw new Error('delete() requires an id')
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check if record exists first
|
|
256
|
+
const exists = await table.get(id)
|
|
257
|
+
if (!exists) {
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await table.delete(id)
|
|
262
|
+
return true
|
|
263
|
+
},
|
|
199
264
|
|
|
200
265
|
// --- READ ---- //
|
|
201
266
|
|
|
202
|
-
|
|
203
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Get a single record by id - returns null if not found
|
|
269
|
+
*/
|
|
270
|
+
get: async (id: string): Promise<T | null> => {
|
|
271
|
+
if (!id) {
|
|
272
|
+
throw new Error('get() requires an id')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const record = await table.get(id)
|
|
276
|
+
return (record as T) || null
|
|
204
277
|
},
|
|
205
278
|
|
|
206
|
-
|
|
207
|
-
|
|
279
|
+
/**
|
|
280
|
+
* Get all records in the collection
|
|
281
|
+
*/
|
|
282
|
+
getAll: async (): Promise<T[]> => {
|
|
283
|
+
return table.toArray() as Promise<T[]>
|
|
208
284
|
},
|
|
209
285
|
|
|
210
286
|
// --- QUERY ---- //
|
|
211
|
-
// TODO: lots to do here. simplifing creating querie, filtering/ordering/limit, and execute
|
|
212
|
-
|
|
213
|
-
query: () => this.table(name),
|
|
214
287
|
|
|
215
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Filter records using a predicate function
|
|
290
|
+
*/
|
|
291
|
+
filter: async (fn: (item: T) => boolean): Promise<T[]> => {
|
|
292
|
+
return table.filter(fn).toArray() as Promise<T[]>
|
|
293
|
+
},
|
|
216
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Get the raw Dexie table for advanced queries
|
|
297
|
+
* @deprecated Use ref instead
|
|
298
|
+
*/
|
|
299
|
+
query: () => table,
|
|
217
300
|
}
|
|
218
301
|
}
|
|
219
302
|
}
|
|
220
|
-
|
|
221
|
-
class QueryMethod {
|
|
222
|
-
|
|
223
|
-
}
|
package/src/db.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
//@ts-nocheck
|
|
2
|
-
|
|
3
|
-
const baseUrl = 'https://api.basic.tech';
|
|
4
|
-
// const baseUrl = 'http://localhost:3000';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
async function get({ projectId, accountId, tableName, token }) {
|
|
8
|
-
const url = `${baseUrl}/project/${projectId}/db/${accountId}/${tableName}`;
|
|
9
|
-
const response = await fetch(url, {
|
|
10
|
-
headers: {
|
|
11
|
-
'Authorization': `Bearer ${token}`
|
|
12
|
-
}
|
|
13
|
-
});
|
|
14
|
-
return response.json();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function add({ projectId, accountId, tableName, value, token }) {
|
|
18
|
-
const url = `${baseUrl}/project/${projectId}/db/${accountId}/${tableName}`;
|
|
19
|
-
const response = await fetch(url, {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
headers: {
|
|
22
|
-
'Content-Type': 'application/json',
|
|
23
|
-
'Authorization': `Bearer ${token}`
|
|
24
|
-
},
|
|
25
|
-
body: JSON.stringify({"value": value})
|
|
26
|
-
});
|
|
27
|
-
return response.json();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function update({ projectId, accountId, tableName, id, value, token }) {
|
|
31
|
-
const url = `${baseUrl}/project/${projectId}/db/${accountId}/${tableName}/${id}`;
|
|
32
|
-
const response = await fetch(url, {
|
|
33
|
-
method: 'PATCH',
|
|
34
|
-
headers: {
|
|
35
|
-
'Content-Type': 'application/json',
|
|
36
|
-
'Authorization': `Bearer ${token}`
|
|
37
|
-
},
|
|
38
|
-
body: JSON.stringify({id: id, value: value})
|
|
39
|
-
});
|
|
40
|
-
return response.json();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function deleteRecord({ projectId, accountId, tableName, id, token }) {
|
|
44
|
-
const url = `${baseUrl}/project/${projectId}/db/${accountId}/${tableName}/${id}`;
|
|
45
|
-
const response = await fetch(url, {
|
|
46
|
-
method: 'DELETE',
|
|
47
|
-
headers: {
|
|
48
|
-
'Authorization': `Bearer ${token}`
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
return response.json();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export { get, add, update, deleteRecord };
|
|
55
|
-
|