@basictech/react 0.7.0-beta.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 { useState } from "react";
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
- export {
8
- useBasic, BasicProvider, useQuery
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
- export type {
12
- AuthConfig, BasicStorage, LocalStorageAdapter, BasicProviderProps
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, PromiseExtended } from '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
- // const SERVER_URL = "https://pds.basic.id"
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
- // TODO: check against schema
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: this.table(name),
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
- return Promise.reject({ ... valid })
195
+ throw new Error(valid.message || 'Data validation failed')
163
196
  }
164
197
 
165
- return this.table(name).add({
166
- id: uuidv7(),
167
- ...data
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
- put: (data: any) => {
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
- return Promise.reject({ ... valid })
216
+ throw new Error(valid.message || 'Data validation failed')
177
217
  }
178
218
 
179
- return this.table(name).put({
180
- id: uuidv7(),
181
- ...data
182
- })
219
+ await table.put(data)
220
+ return data
183
221
  },
184
222
 
185
- update: (id: string, data: any) => {
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
- return Promise.reject({ ... valid })
234
+ throw new Error(valid.message || 'Data validation failed')
190
235
  }
191
236
 
192
- return this.table(name).update(id, data)
193
- },
237
+ const updated = await table.update(id, data)
238
+ if (updated === 0) {
239
+ return null
240
+ }
194
241
 
195
- delete: (id: string) => {
196
- return this.table(name).delete(id)
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
- get: async (id: string) => {
203
- return this.table(name).get(id)
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
- getAll: async () => {
207
- return this.table(name).toArray();
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
- filter: (fn: any) => this.table(name).filter(fn).toArray(),
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
-