@basictech/react 0.2.0-beta.1 → 0.2.0-beta.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basictech/react",
3
- "version": "0.2.0-beta.1",
3
+ "version": "0.2.0-beta.11",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -17,15 +17,21 @@
17
17
  "author": "",
18
18
  "license": "ISC",
19
19
  "dependencies": {
20
+ "ajv": "^8.17.1",
21
+ "dexie": "^4.0.8",
22
+ "dexie-observable": "^4.0.1-beta.13",
23
+ "dexie-react-hooks": "^1.1.7",
24
+ "dexie-syncable": "^4.0.1-beta.13",
20
25
  "jwt-decode": "^4.0.0",
21
- "@repo/sync": "0.1.0-beta.0"
26
+ "uuid": "^10.0.0",
27
+ "@basictech/schema": "0.1.0-beta.1"
22
28
  },
23
29
  "devDependencies": {
30
+ "@repo/typescript-config": "*",
24
31
  "tsup": "^7.2.0",
25
- "typescript": "^5.0.0",
26
- "@repo/typescript-config": "*"
32
+ "typescript": "^5.0.0"
27
33
  },
28
34
  "peerDependencies": {
29
35
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
30
36
  }
31
- }
37
+ }
package/readme.md CHANGED
@@ -17,9 +17,31 @@ In your root component or App.tsx, wrap your application with the `BasicProvider
17
17
  ```typescript
18
18
  import { BasicProvider } from '@basictech/react';
19
19
 
20
+ const schema = {
21
+ tables: {
22
+ todos: {
23
+ fields: {
24
+ id: {
25
+ type: "string",
26
+ primary: true
27
+ },
28
+ title: {
29
+ type: "string",
30
+ indexed: true
31
+ },
32
+ completed: {
33
+ type: "boolean",
34
+ indexed: true
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+
20
42
  function App() {
21
43
  return (
22
- <BasicProvider project_id="YOUR_PROJECT_ID">
44
+ <BasicProvider project_id="YOUR_PROJECT_ID" schema={schema} debug>
23
45
  {/* Your app components */}
24
46
  </BasicProvider>
25
47
  );
@@ -53,33 +75,50 @@ function MyComponent() {
53
75
  }
54
76
  ```
55
77
 
56
- ### 3. Database Operations
78
+ ## API Reference
79
+
80
+
81
+ ### <BasicProvider>
82
+
83
+ The `BasicProvider` component accepts the following props:
84
+
85
+ - `project_id` (required): String - Your Basic project ID.
86
+ - `schema` (required): Object - The schema definition for your database.
87
+ - `debug` (optional): Boolean - Enable debug mode for additional logging. Default is `false`.
88
+ - `children` (required): React.ReactNode - The child components to be wrapped by the provider.
89
+
90
+
91
+
57
92
 
58
- You can perform database operations using the `db` object:
93
+ ### useQuery
94
+
95
+ returns a react hook that will automatically update data based on your query
96
+
97
+ usage:
59
98
 
60
99
  ```typescript
61
- const { db } = useBasic();
62
-
63
- // Get data
64
- const getData = async () => {
65
- const data = await db.table('myTable').get();
66
- console.log(data);
67
- };
68
-
69
- // Add data
70
- const addData = async () => {
71
- const result = await db.table('myTable').add({ key: 'value' });
72
- console.log(result);
73
- };
74
-
75
- // Update data
76
- const updateData = async () => {
77
- const result = await db.table('myTable').update('itemId', { key: 'newValue' });
78
- console.log(result);
79
- };
100
+ import { useQuery } from '@basictech/react'
101
+
102
+ function MyComponent() {
103
+ const data = useQuery(db.collection('data').getAll())
104
+
105
+ return (
106
+ <div>
107
+ {
108
+ data.map((item: any) => {
109
+ <>
110
+ // render your data here
111
+ </>
112
+ })
113
+ }
114
+ </div>
115
+ );
116
+ }
80
117
  ```
118
+ Notes:
119
+ - must pass in a db function, ie `db.collection('todos').getAll()`
120
+ - default will be empty array (this might change in the future)
81
121
 
82
- ## API Reference
83
122
 
84
123
  ### useBasic()
85
124
 
@@ -91,14 +130,47 @@ Returns an object with the following properties and methods:
91
130
  - `signout()`: Function to sign out the user
92
131
  - `db`: Object for database operations
93
132
 
94
- ### db
95
133
 
96
- The `db` object provides the following methods:
97
134
 
98
- - `table(tableName)`: Selects a table for operations
99
- - `get()`: Retrieves all items from the table
100
- - `add(value)`: Adds a new item to the table
101
- - `update(id, value)`: Updates an item in the table
135
+ db methods:
136
+
137
+ - `collection(name: string)`: returns a collection object
138
+
139
+
140
+ db.collection(name) methods:
141
+
142
+ - `getAll()`: returns all items in the collection
143
+ - `get(id: string)`: returns a single item from the collection
144
+ - `add(data: any)`: adds a new item to the collection
145
+ - `put(data: any)`: updates an item in the collection
146
+ - `update(id: string, data: any)`: updates an item in the collection
147
+ - `delete(id: string)`: deletes an item from the collection
148
+
149
+ all db.collection() methods return a promise
150
+
151
+ example usage:
152
+
153
+ ```typescript
154
+ import { useBasic } from '@basictech/react';
155
+
156
+ function MyComponent() {
157
+ const { db } = useBasic();
158
+
159
+ async function addTodo() {
160
+ await db.collection('todos').add({
161
+ title: 'test',
162
+ completed: false
163
+ })
164
+ }
165
+
166
+ return (
167
+ <div>
168
+ <button onClick={addTodo}>Add Todo</button>
169
+ </div>
170
+ );
171
+ }
172
+
173
+ ```
102
174
 
103
175
  ## License
104
176
 
@@ -3,23 +3,54 @@
3
3
  import React, { createContext, useContext, useEffect, useState, useRef } from 'react'
4
4
  import { jwtDecode } from 'jwt-decode'
5
5
 
6
-
7
-
8
- import { BasicSync } from '@repo/sync'
6
+ import { BasicSync } from './sync'
9
7
  import { get, add, update, deleteRecord } from './db'
8
+ import { validateSchema } from '@basictech/schema'
9
+
10
+ import { log } from './config'
11
+
12
+ /*
13
+ schema todo:
14
+ field types
15
+ array types
16
+ relations
17
+ */
18
+
19
+
20
+ // const example = {
21
+ // project_id: '123',
22
+ // version: 0,
23
+ // tables: {
24
+ // example: {
25
+ // name: 'example',
26
+ // type: 'collection',
27
+ // fields: {
28
+ // id: {
29
+ // type: 'uuid',
30
+ // primary: true,
31
+ // },
32
+ // value: {
33
+ // type: 'string',
34
+ // indexed: true,
35
+ // },
36
+ // }
37
+ // }
38
+ // }
39
+ // }
40
+
10
41
 
11
42
  type BasicSyncType = {
12
43
  basic_schema: any;
13
44
  connect: (options: { access_token: string }) => void;
14
45
  debugeroo: () => void;
15
46
  collection: (name: string) => {
16
- ref: {
17
- toArray: () => Promise<any[]>;
18
- count: () => Promise<number>;
19
- };
47
+ ref: {
48
+ toArray: () => Promise<any[]>;
49
+ count: () => Promise<number>;
50
+ };
20
51
  };
21
52
  [key: string]: any; // For other potential methods and properties
22
- };
53
+ };
23
54
 
24
55
 
25
56
  enum DBStatus {
@@ -49,18 +80,18 @@ type Token = {
49
80
 
50
81
  export const BasicContext = createContext<{
51
82
  unicorn: string,
52
- isLoaded: boolean,
83
+ isAuthReady: boolean,
53
84
  isSignedIn: boolean,
54
85
  user: User | null,
55
86
  signout: () => void,
56
87
  signin: () => void,
57
88
  getToken: () => Promise<string>,
58
89
  getSignInLink: () => string,
59
- db: any,
90
+ db: any,
60
91
  dbStatus: DBStatus
61
92
  }>({
62
93
  unicorn: "🦄",
63
- isLoaded: false,
94
+ isAuthReady: false,
64
95
  isSignedIn: false,
65
96
  user: null,
66
97
  signout: () => { },
@@ -68,7 +99,7 @@ export const BasicContext = createContext<{
68
99
  getToken: () => new Promise(() => { }),
69
100
  getSignInLink: () => "",
70
101
  db: {},
71
- dbStatus: DBStatus.OFFLINE
102
+ dbStatus: DBStatus.LOADING
72
103
  });
73
104
 
74
105
  const EmptyDB: BasicSyncType = {
@@ -103,98 +134,143 @@ function getSyncStatus(statusCode: number): string {
103
134
  }
104
135
  }
105
136
 
106
- export function BasicProvider({ children, project_id, schema }: { children: React.ReactNode, project_id: string, schema: any }) {
107
- const [isLoaded, setIsLoaded] = useState(false)
108
- const [isSignedIn, setIsSignedIn] = useState(false)
137
+ type ErrorObject = {
138
+ code: string;
139
+ title: string;
140
+ message: string;
141
+ }
142
+
143
+ export function BasicProvider({ children, project_id, schema, debug = false }: { children: React.ReactNode, project_id: string, schema: any, debug?: boolean }) {
144
+ const [isAuthReady, setIsAuthReady] = useState(false)
145
+ const [isSignedIn, setIsSignedIn] = useState<boolean>(false)
109
146
  const [token, setToken] = useState<Token | null>(null)
110
- const [authCode, setAuthCode] = useState<string | null>(null)
111
147
  const [user, setUser] = useState<User>({})
112
148
 
113
- const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
114
-
149
+ const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.LOADING)
150
+ const [error, setError] = useState<ErrorObject | null>(null)
115
151
 
116
152
  const syncRef = useRef<BasicSync | null>(null);
117
153
 
154
+
118
155
  useEffect(() => {
119
- if (!syncRef.current) {
120
- syncRef.current = new BasicSync('basicdb', { schema: schema });
156
+ function initDb() {
157
+ const valid = validateSchema(schema)
158
+ if (!valid.valid) {
159
+ log('Basic Schema is invalid!', valid.errors)
160
+ console.group('Schema Errors')
161
+ let errorMessage = ''
162
+ valid.errors.forEach((error, index) => {
163
+ log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`)
164
+ errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
165
+ })
166
+ console.groupEnd('Schema Errors')
167
+ setError({
168
+ code: 'schema_invalid',
169
+ title: 'Basic Schema is invalid!',
170
+ message: errorMessage
171
+ })
172
+ return null
173
+ }
121
174
 
122
- // console.log('db is open', syncRef.current.isOpen())
123
- // syncRef.current.open()
124
- // .then(() => {
125
- // console.log("is open now:", syncRef.current.isOpen())
126
- // })
175
+ if (!syncRef.current) {
176
+ log('Initializing BasicDB')
177
+ syncRef.current = new BasicSync('basicdb', { schema: schema });
127
178
 
128
- syncRef.current.handleStatusChange((status: number, url: string) => {
129
- setDbStatus(getSyncStatus(status))
130
- })
179
+ // log('db is open', syncRef.current.isOpen())
180
+ // syncRef.current.open()
181
+ // .then(() => {
182
+ // log("is open now:", syncRef.current.isOpen())
183
+ // })
184
+ }
185
+ }
131
186
 
132
- syncRef.current.syncable.getStatus().then((status) => {
133
- console.log('sync status', getSyncStatus(status))
134
- })
187
+ initDb()
188
+ }, []);
135
189
 
190
+ useEffect(() => {
191
+ if (!syncRef.current) {
192
+ return
136
193
  }
137
- }, []);
138
194
 
195
+ // syncRef.current.handleStatusChange((status: number, url: string) => {
196
+ // setDbStatus(getSyncStatus(status))
197
+ // })
139
198
 
140
- //todo:
141
- //add random state to signin link & verify random state
199
+ syncRef.current.syncable.on('statusChanged', (status: number, url: string) => {
200
+ setDbStatus(getSyncStatus(status))
201
+ })
142
202
 
143
- const connectToDb = async () => {
203
+ syncRef.current.syncable.getStatus().then((status) => {
204
+ setDbStatus(getSyncStatus(status))
205
+ })
206
+ }, [syncRef.current])
144
207
 
208
+
209
+ const connectToDb = async () => {
145
210
  const tok = await getToken()
211
+ if (!tok) {
212
+ log('no token found')
213
+ return
214
+ }
215
+
216
+ log('connecting to db...')
146
217
 
147
- console.log('connecting to db...', tok.substring(0, 10))
218
+ // TODO: handle if signed out after connect() is already called
148
219
 
149
220
  syncRef.current.connect({ access_token: tok })
150
221
  .catch((e) => {
151
- console.log('error connecting to db', e)
222
+ log('error connecting to db', e)
152
223
  })
153
224
  }
154
225
 
155
226
  useEffect(() => {
156
- if (token) {
227
+ if (token && syncRef.current && isSignedIn && isSignedIn) {
157
228
  connectToDb()
158
229
  }
159
- }, [token])
230
+ }, [isSignedIn])
160
231
 
161
232
  const getSignInLink = () => {
162
- console.log('getting sign in link...')
233
+ log('getting sign in link...')
163
234
 
164
- const randomState = Math.random().toString(36).substring(7);
235
+ const randomState = Math.random().toString(36).substring(6);
236
+ localStorage.setItem('basic_auth_state', randomState)
165
237
 
166
- // let baseUrl = "https://api.basic.tech/auth/authorize"
167
- let baseUrl = "http://localhost:3003/auth/authorize"
238
+ let baseUrl = "https://api.basic.tech/auth/authorize"
168
239
  baseUrl += `?client_id=${project_id}`
169
240
  baseUrl += `&redirect_uri=${encodeURIComponent(window.location.href)}`
170
241
  baseUrl += `&response_type=code`
171
242
  baseUrl += `&scope=openid`
172
- baseUrl += `&state=1234zyx`
243
+ baseUrl += `&state=${randomState}`
173
244
 
174
245
  return baseUrl;
175
246
  }
176
247
 
177
248
  const signin = () => {
178
- console.log('signing in: ', getSignInLink())
249
+ log('signing in: ', getSignInLink())
179
250
  const signInLink = getSignInLink()
180
251
  //todo: change to the other thing?
181
252
  window.location.href = signInLink;
182
253
  }
183
254
 
184
255
  const signout = () => {
185
- console.log('signing out!')
256
+ log('signing out!')
186
257
  setUser({})
187
258
  setIsSignedIn(false)
188
259
  setToken(null)
189
- setAuthCode(null)
190
260
  document.cookie = `basic_token=; Secure; SameSite=Strict`;
261
+ localStorage.removeItem('basic_auth_state')
262
+
263
+ if (syncRef.current) {
264
+ // WIP - BUG - sometimes connects even after signout
265
+ syncRef.current.disconnect()
266
+ }
191
267
  }
192
268
 
193
269
  const getToken = async (): Promise<string> => {
194
- console.log('getting token...')
270
+ log('getting token...')
195
271
 
196
272
  if (!token) {
197
- console.log('no token found')
273
+ log('no token found')
198
274
  throw new Error('no token found')
199
275
  }
200
276
 
@@ -202,7 +278,7 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
202
278
  const isExpired = decoded.exp && decoded.exp < Date.now() / 1000
203
279
 
204
280
  if (isExpired) {
205
- console.log('token is expired - refreshing ...')
281
+ log('token is expired - refreshing ...')
206
282
  const newToken = await fetchToken(token?.refresh)
207
283
  return newToken?.access_token || ''
208
284
  }
@@ -234,45 +310,56 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
234
310
  body: JSON.stringify({ code: code })
235
311
  })
236
312
  .then(response => response.json())
237
- .catch(error => console.error('Error:', error))
313
+ .catch(error => log('Error:', error))
238
314
 
239
315
  if (token.error) {
240
- console.log('error fetching token', token.error)
316
+ log('error fetching token', token.error)
241
317
  return
242
318
  } else {
243
- // console.log('token', token)
319
+ // log('token', token)
244
320
  setToken(token)
245
321
  }
246
322
  return token
247
323
  }
248
324
 
249
325
  useEffect(() => {
250
- try {
251
- let cookie_token = getCookie('basic_token')
252
- if (cookie_token !== '') {
253
- setToken(JSON.parse(cookie_token))
254
- }
326
+ localStorage.setItem('basic_debug', debug ? 'true' : 'false')
255
327
 
328
+ try {
256
329
  if (window.location.search.includes('code')) {
257
330
  let code = window.location?.search?.split('code=')[1].split('&')[0]
258
- // console.log('code found', code)
259
331
 
260
- // todo: check state is valid
261
- setAuthCode(code) // remove this? dont need to store code?
262
- fetchToken(code)
332
+ const state = localStorage.getItem('basic_auth_state')
333
+ if (!state || state !== window.location.search.split('state=')[1].split('&')[0]) {
334
+ log('error: auth state does not match')
335
+ setIsAuthReady(true)
263
336
 
264
- window.history.pushState({}, document.title, "/");
337
+ localStorage.removeItem('basic_auth_state')
338
+ window.history.pushState({}, document.title, "/");
339
+ return
340
+ }
265
341
 
266
- } else {
267
- setIsLoaded(true)
342
+ localStorage.removeItem('basic_auth_state')
343
+
344
+ fetchToken(code)
345
+ } else {
346
+ let cookie_token = getCookie('basic_token')
347
+ if (cookie_token !== '') {
348
+ setToken(JSON.parse(cookie_token))
349
+ } else {
350
+ setIsAuthReady(true)
351
+ }
268
352
  }
353
+
354
+
269
355
  } catch (e) {
270
- console.log('error getting cookie', e)
356
+ log('error getting cookie', e)
271
357
  }
272
358
  }, [])
273
359
 
274
360
  useEffect(() => {
275
361
  async function fetchUser(acc_token: string) {
362
+ console.info('fetching user')
276
363
  const user = await fetch('https://api.basic.tech/auth/userInfo', {
277
364
  method: 'GET',
278
365
  headers: {
@@ -280,24 +367,32 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
280
367
  }
281
368
  })
282
369
  .then(response => response.json())
283
- .catch(error => console.error('Error:', error))
370
+ .catch(error => log('Error:', error))
284
371
 
285
372
  if (user.error) {
286
- console.log('error fetching user', user.error)
373
+ log('error fetching user', user.error)
287
374
  // refreshToken()
288
375
  return
289
376
  } else {
290
- // console.log('user', user)
377
+ // log('user', user)
291
378
  document.cookie = `basic_token=${JSON.stringify(token)}; Secure; SameSite=Strict`;
379
+
380
+ if (window.location.search.includes('code')) {
381
+ window.history.pushState({}, document.title, "/");
382
+ }
383
+
292
384
  setUser(user)
293
385
  setIsSignedIn(true)
294
- setIsLoaded(true)
386
+
387
+ setIsAuthReady(true)
295
388
  }
296
389
  }
297
390
 
298
391
  async function checkToken() {
299
392
  if (!token) {
300
- console.log('error: no user token found')
393
+ log('error: no user token found')
394
+
395
+ setIsAuthReady(true)
301
396
  return
302
397
  }
303
398
 
@@ -305,7 +400,7 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
305
400
  const isExpired = decoded.exp && decoded.exp < Date.now() / 1000
306
401
 
307
402
  if (isExpired) {
308
- console.log('token is expired - refreshing ...')
403
+ log('token is expired - refreshing ...')
309
404
  const newToken = await fetchToken(token?.refresh)
310
405
  fetchUser(newToken.access_token)
311
406
  } else {
@@ -315,8 +410,7 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
315
410
 
316
411
  if (token) {
317
412
  checkToken()
318
- setIsLoaded(true)
319
- }
413
+ }
320
414
  }, [token])
321
415
 
322
416
 
@@ -356,7 +450,7 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
356
450
  return (
357
451
  <BasicContext.Provider value={{
358
452
  unicorn: "🦄",
359
- isLoaded,
453
+ isAuthReady,
360
454
  isSignedIn,
361
455
  user,
362
456
  signout,
@@ -366,11 +460,39 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
366
460
  db: syncRef.current,
367
461
  dbStatus
368
462
  }}>
463
+ {error && <ErrorDisplay error={error} />}
369
464
  {syncRef.current ? children : null}
370
465
  </BasicContext.Provider>
371
466
  )
372
467
  }
373
468
 
469
+ function ErrorDisplay({ error }: { error: ErrorObject }) {
470
+ return <div style={{
471
+ position: 'absolute',
472
+ top: 20,
473
+ left: 20,
474
+ color: 'black',
475
+ backgroundColor: '#f8d7da',
476
+ border: '1px solid #f5c6cb',
477
+ borderRadius: '4px',
478
+ padding: '20px',
479
+ maxWidth: '400px',
480
+ margin: '20px auto',
481
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
482
+ fontFamily: 'monospace',
483
+ }}>
484
+ <h3 style={{fontSize: '0.8rem', opacity: 0.8}}>code: {error.code}</h3>
485
+ <h1 style={{fontSize: '1.2rem', lineHeight: '1.5'}}>{error.title}</h1>
486
+ <p>{error.message}</p>
487
+ </div>
488
+ }
489
+
490
+ /*
491
+ possible errors:
492
+ - projectid missing / invalid
493
+ - schema missing / invalid
494
+ */
495
+
374
496
  export function useBasic() {
375
497
  return useContext(BasicContext);
376
- }
498
+ }
package/src/config.ts ADDED
@@ -0,0 +1,27 @@
1
+
2
+ export const SERVER_URL = "https://api.basic.tech"
3
+ // export const SERVER_URL = "http://localhost:3000"
4
+
5
+
6
+ export const log = (...args: any[]) => {
7
+ try {
8
+ if (localStorage.getItem('basic_debug') === 'true') {
9
+ console.log('[basic]', ...args)
10
+ }
11
+ } catch (e) {
12
+ // console.log('error logging', e)
13
+ }
14
+ }
15
+
16
+ // export const log = (message: string, ...args: any[]) => {
17
+ // try {
18
+ // if (process.env.NODE_ENV === 'development') {
19
+ // const stack = new Error().stack;
20
+ // const caller = stack?.split('\n')[2]?.trim();
21
+ // console.log(`[basic] ${message}`, ...args);
22
+ // // console.log(`[stack] ${caller}`);
23
+ // }
24
+ // } catch (e) {
25
+ // console.error('Error in logWithStack:', e);
26
+ // }
27
+ // }