@basictech/react 0.8.0-beta.3 → 0.9.0-beta.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.
@@ -1,493 +0,0 @@
1
- import React, { useCallback, useEffect, useRef, useState, Suspense, lazy } from 'react'
2
-
3
- import { BasicSync, initDexieExtensions } from './sync'
4
- import { RemoteDB, DBMode, BasicDB } from './core/db'
5
- import { AuthManager } from './core/auth/AuthManager'
6
- import type { User, AuthResult, GetTokenOptions, PdsEndpoints } from './core/auth/AuthManager'
7
-
8
- import { log } from './config'
9
- import { version as currentVersion } from '../package.json'
10
- import { createVersionUpdater } from './updater/versionUpdater'
11
- import { getMigrations } from './updater/updateMigrations'
12
- import { BasicStorage, LocalStorageAdapter } from './utils/storage'
13
- import { isDevelopment, checkForNewVersion, getSyncStatus } from './utils/network'
14
- import { validateAndCheckSchema } from './utils/schema'
15
- import { BasicContext, DBStatus, noDb, type BasicSchemaDevInfo } from './context'
16
-
17
- const BasicDevToolbar = lazy(() =>
18
- import('./dev/BasicDevToolbar').then((m) => ({ default: m.BasicDevToolbar }))
19
- )
20
-
21
- export type { BasicStorage, LocalStorageAdapter } from './utils/storage'
22
- export type { DBMode, BasicDB, Collection } from './core/db'
23
- export type { Token, User, AuthResult, GetTokenOptions, PdsEndpoints } from './core/auth/AuthManager'
24
- export type { BasicContextType, BasicSchemaDevInfo } from './context'
25
- export { DBStatus, useBasic, BasicContext } from './context'
26
-
27
- export type AuthConfig = {
28
- scopes?: string | string[]
29
- /** @deprecated Use pds_url instead */
30
- server_url?: string
31
- /** PDS URL for auth and data (default: https://pds.basic.id) */
32
- pds_url?: string
33
- /** Admin server URL for connect reporting (default: https://api.basic.tech) */
34
- admin_url?: string
35
- ws_url?: string
36
- }
37
-
38
- export type BasicProviderProps = {
39
- children: React.ReactNode
40
- /**
41
- * @deprecated Project ID is now extracted from schema.project_id.
42
- * This prop is kept for backward compatibility but can be omitted.
43
- */
44
- project_id?: string
45
- /** The Basic schema object containing project_id and table definitions */
46
- schema?: any
47
- debug?: boolean
48
- storage?: BasicStorage
49
- auth?: AuthConfig
50
- /**
51
- * Database mode - determines which implementation is used
52
- * - 'sync': Uses Dexie + WebSocket for local-first sync (default)
53
- * - 'remote': Uses REST API calls directly to server
54
- */
55
- dbMode?: DBMode
56
- /** Show floating dev toolbar (localhost, NODE_ENV=development, or debug=true). */
57
- devToolbar?: boolean
58
- }
59
-
60
- const DEFAULT_AUTH_CONFIG = {
61
- scopes: 'profile,email,app:admin',
62
- pds_url: 'https://pds.basic.id',
63
- admin_url: 'https://api.basic.tech',
64
- ws_url: 'wss://pds.basic.id/ws',
65
- } as const
66
-
67
- type ErrorObject = {
68
- code: string
69
- title: string
70
- message: string
71
- }
72
-
73
- type AuthSnapshot = {
74
- isSignedIn: boolean
75
- hasToken: boolean
76
- isAuthReady: boolean
77
- user: User | null
78
- did: string | null
79
- tokenScope: string | null
80
- }
81
-
82
- function snapshotAuth(mgr: AuthManager): AuthSnapshot {
83
- return {
84
- isSignedIn: mgr.isSignedIn,
85
- hasToken: !!mgr.token,
86
- isAuthReady: mgr.isAuthReady,
87
- user: mgr.user,
88
- did: mgr.did,
89
- tokenScope: mgr.tokenScope,
90
- }
91
- }
92
-
93
- export function BasicProvider({
94
- children,
95
- project_id: project_id_prop,
96
- schema,
97
- debug = false,
98
- storage,
99
- auth,
100
- dbMode = 'sync',
101
- devToolbar = false,
102
- }: BasicProviderProps) {
103
- const project_id = schema?.project_id || project_id_prop
104
-
105
- if (auth?.server_url && !auth?.pds_url) {
106
- log('Warning: auth.server_url is deprecated, use auth.pds_url instead')
107
- }
108
- const authConfig = {
109
- scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
110
- pds_url: auth?.pds_url || auth?.server_url || DEFAULT_AUTH_CONFIG.pds_url,
111
- admin_url: auth?.admin_url || DEFAULT_AUTH_CONFIG.admin_url,
112
- ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url,
113
- }
114
-
115
- const scopesString = Array.isArray(authConfig.scopes)
116
- ? authConfig.scopes.join(' ')
117
- : authConfig.scopes
118
-
119
- const storageRef = useRef<BasicStorage>(storage || new LocalStorageAdapter())
120
- const storageAdapter = storageRef.current
121
-
122
- const schemaRef = useRef(schema)
123
- schemaRef.current = schema
124
-
125
- const [authState, setAuthState] = useState<AuthSnapshot>({
126
- isSignedIn: false,
127
- hasToken: false,
128
- isAuthReady: false,
129
- user: null,
130
- did: null,
131
- tokenScope: null,
132
- })
133
-
134
- const authRef = useRef<AuthManager>(null!)
135
- if (!authRef.current) {
136
- authRef.current = new AuthManager(
137
- {
138
- projectId: project_id,
139
- scopes: scopesString,
140
- pdsUrl: authConfig.pds_url,
141
- adminUrl: authConfig.admin_url,
142
- debug,
143
- },
144
- storageAdapter,
145
- () => setAuthState(snapshotAuth(authRef.current)),
146
- )
147
- }
148
-
149
- const syncRef = useRef<BasicSync | null>(null)
150
- const remoteDbRef = useRef<RemoteDB | null>(null)
151
- const [shouldConnect, setShouldConnect] = useState(false)
152
- const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
153
- const [isReady, setIsReady] = useState(false)
154
- const [error, setError] = useState<ErrorObject | null>(null)
155
- const [schemaDevInfo, setSchemaDevInfo] = useState<BasicSchemaDevInfo | null>(null)
156
-
157
- const isDevMode = () => isDevelopment(debug)
158
-
159
- const refreshSchemaStatus = useCallback(async () => {
160
- const s = schemaRef.current
161
- if (!s) {
162
- setSchemaDevInfo(
163
- project_id
164
- ? {
165
- projectId: project_id,
166
- localVersion: undefined,
167
- status: 'no_schema',
168
- valid: false,
169
- lastCheckedAt: Date.now(),
170
- }
171
- : null,
172
- )
173
- return
174
- }
175
- const result = await validateAndCheckSchema(s)
176
- if (!result.isValid) {
177
- const errText =
178
- result.errors?.map((e: { message?: string }) => e.message || '').join('; ') || 'invalid'
179
- setSchemaDevInfo({
180
- projectId: s.project_id ?? null,
181
- localVersion: s.version,
182
- status: 'invalid',
183
- valid: false,
184
- lastCheckedAt: Date.now(),
185
- error: errText,
186
- })
187
- return
188
- }
189
- setSchemaDevInfo({
190
- projectId: s.project_id ?? null,
191
- localVersion: s.version,
192
- status: result.schemaStatus.status ?? 'unknown',
193
- valid: result.schemaStatus.valid,
194
- lastCheckedAt: Date.now(),
195
- })
196
- }, [project_id])
197
-
198
- useEffect(() => {
199
- const runVersionUpdater = async () => {
200
- try {
201
- const versionUpdater = createVersionUpdater(storageAdapter, currentVersion, getMigrations())
202
- const updateResult = await versionUpdater.checkAndUpdate()
203
-
204
- if (updateResult.updated) {
205
- log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`)
206
- } else {
207
- log(`App version ${updateResult.toVersion} is current`)
208
- }
209
- } catch (error) {
210
- log('Version update failed:', error)
211
- }
212
- }
213
-
214
- runVersionUpdater()
215
- authRef.current.initialize()
216
-
217
- return authRef.current.setupNetworkListeners()
218
- }, [])
219
-
220
- useEffect(() => {
221
- async function initSyncDb(options: { shouldConnect: boolean }) {
222
- if (!syncRef.current) {
223
- log('Initializing Basic Sync DB')
224
-
225
- await initDexieExtensions()
226
-
227
- syncRef.current = new BasicSync('basicdb', { schema: schema })
228
-
229
- syncRef.current.syncable.on('statusChanged', (status: number) => {
230
- const newStatus = getSyncStatus(status) as DBStatus
231
- setDbStatus(newStatus)
232
-
233
- if (newStatus === DBStatus.ERROR_WILL_RETRY) {
234
- log('Sync entered ERROR_WILL_RETRY - proactively refreshing token')
235
- authRef.current.getToken({ forceRefresh: true }).catch(() => {})
236
- }
237
- })
238
-
239
- if (options.shouldConnect) {
240
- setShouldConnect(true)
241
- } else {
242
- log('Sync is disabled')
243
- }
244
-
245
- setIsReady(true)
246
- }
247
- }
248
-
249
- function initRemoteDb() {
250
- if (!remoteDbRef.current) {
251
- if (!project_id) {
252
- setError({
253
- code: 'missing_project_id',
254
- title: 'Project ID Required',
255
- message:
256
- 'Remote mode requires a project_id. Provide it via schema.project_id or the project_id prop.',
257
- })
258
- setIsReady(true)
259
- return
260
- }
261
-
262
- log('Initializing Basic Remote DB')
263
- remoteDbRef.current = new RemoteDB({
264
- serverUrl: authConfig.pds_url,
265
- projectId: project_id,
266
- getToken: (opts) => authRef.current.getToken(opts),
267
- schema: schema,
268
- debug: debug,
269
- onAuthError: (error) => {
270
- log('RemoteDB auth error:', error)
271
- if (error.errorType === 'forbidden') {
272
- log('403 Forbidden - user lacks required scope, not signing out')
273
- return
274
- }
275
- handleSignOut()
276
- },
277
- })
278
- setDbStatus(DBStatus.ONLINE)
279
- setIsReady(true)
280
- }
281
- }
282
-
283
- async function checkSchema() {
284
- const result = await validateAndCheckSchema(schema)
285
-
286
- if (!result.isValid) {
287
- let errorMessage = ''
288
- if (result.errors) {
289
- result.errors.forEach((err: any, index: number) => {
290
- errorMessage += `${index + 1}: ${err.message} - at ${err.instancePath}\n`
291
- })
292
- }
293
- setSchemaDevInfo({
294
- projectId: schema?.project_id ?? null,
295
- localVersion: schema?.version,
296
- status: 'invalid',
297
- valid: false,
298
- lastCheckedAt: Date.now(),
299
- error: errorMessage.trim() || undefined,
300
- })
301
- setError({
302
- code: 'schema_invalid',
303
- title: 'Basic Schema is invalid!',
304
- message: errorMessage,
305
- })
306
- setIsReady(true)
307
- return null
308
- }
309
-
310
- setSchemaDevInfo({
311
- projectId: schema?.project_id ?? null,
312
- localVersion: schema?.version,
313
- status: result.schemaStatus.status ?? 'unknown',
314
- valid: result.schemaStatus.valid,
315
- lastCheckedAt: Date.now(),
316
- })
317
-
318
- if (dbMode === 'remote') {
319
- initRemoteDb()
320
- } else {
321
- if (result.schemaStatus.valid) {
322
- await initSyncDb({ shouldConnect: true })
323
- } else {
324
- if (result.schemaStatus.status === 'unpublished') {
325
- log('Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.')
326
- } else {
327
- log('Schema is invalid!', result.schemaStatus)
328
- }
329
- await initSyncDb({ shouldConnect: false })
330
- }
331
- }
332
-
333
- checkForNewVersion()
334
- }
335
-
336
- if (schema) {
337
- checkSchema()
338
- } else {
339
- setSchemaDevInfo(
340
- project_id
341
- ? {
342
- projectId: project_id,
343
- localVersion: undefined,
344
- status: 'no_schema',
345
- valid: false,
346
- lastCheckedAt: Date.now(),
347
- }
348
- : null,
349
- )
350
- if (dbMode === 'remote' && project_id) {
351
- initRemoteDb()
352
- } else {
353
- setIsReady(true)
354
- }
355
- }
356
- }, [])
357
-
358
- useEffect(() => {
359
- if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
360
- log('connecting to db...')
361
-
362
- syncRef.current
363
- ?.connect({
364
- getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
365
- ws_url: authConfig.ws_url,
366
- })
367
- .catch((e: any) => {
368
- log('error connecting to db', e)
369
- })
370
- }
371
- }, [authState.isSignedIn, authState.hasToken, shouldConnect])
372
-
373
- const handleSignOut = async () => {
374
- await authRef.current.signOut()
375
- if (syncRef.current) {
376
- try {
377
- await syncRef.current.close()
378
- await syncRef.current.delete({ disableAutoOpen: false })
379
- syncRef.current = null
380
- window?.location?.reload()
381
- } catch (error) {
382
- console.error('Error during database cleanup:', error)
383
- }
384
- }
385
- }
386
-
387
- const handleSignIn = async () => {
388
- try {
389
- await authRef.current.signIn()
390
- } catch (error) {
391
- if (isDevMode()) {
392
- setError({
393
- code: 'signin_error',
394
- title: 'Sign-in Failed',
395
- message:
396
- (error as Error).message || 'An error occurred during sign-in. Please try again.',
397
- })
398
- }
399
- throw error
400
- }
401
- }
402
-
403
- const handleSignInWithHandle = async (handle: string) => {
404
- try {
405
- await authRef.current.signInWithHandle(handle)
406
- } catch (error) {
407
- if (isDevMode()) {
408
- setError({
409
- code: 'signin_error',
410
- title: 'Sign-in Failed',
411
- message:
412
- (error as Error).message || 'An error occurred during sign-in. Please try again.',
413
- })
414
- }
415
- throw error
416
- }
417
- }
418
-
419
- const getCurrentDb = (): BasicDB => {
420
- if (dbMode === 'remote') {
421
- return remoteDbRef.current || noDb
422
- }
423
- return syncRef.current || noDb
424
- }
425
-
426
- const contextValue = {
427
- isReady: authState.isAuthReady,
428
- isSignedIn: authState.isSignedIn,
429
- user: authState.user,
430
- did: authState.did,
431
- scope: authState.tokenScope,
432
- hasScope: (s: string) => authRef.current.hasScope(s),
433
- missingScopes: () => authRef.current.missingScopes(),
434
-
435
- signIn: handleSignIn,
436
- signInWithHandle: handleSignInWithHandle,
437
- signOut: handleSignOut,
438
- signInWithCode: (code: string, state?: string) => authRef.current.signInWithCode(code, state),
439
-
440
- getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
441
- getSignInUrl: (redirectUri?: string) => authRef.current.getSignInUrl(redirectUri),
442
-
443
- db: getCurrentDb(),
444
- dbStatus,
445
- dbMode,
446
-
447
- devInfo: schemaDevInfo,
448
- refreshSchemaStatus,
449
-
450
- isAuthReady: authState.isAuthReady,
451
- signin: handleSignIn,
452
- signout: handleSignOut,
453
- signinWithCode: (code: string, state?: string) => authRef.current.signInWithCode(code, state),
454
- getSignInLink: (redirectUri?: string) => authRef.current.getSignInUrl(redirectUri),
455
- }
456
-
457
- return (
458
- <BasicContext.Provider value={contextValue}>
459
- {error && isDevMode() && <ErrorDisplay error={error} />}
460
- {devToolbar && isDevMode() && (
461
- <Suspense fallback={null}>
462
- <BasicDevToolbar debug={debug} />
463
- </Suspense>
464
- )}
465
- {isReady && children}
466
- </BasicContext.Provider>
467
- )
468
- }
469
-
470
- function ErrorDisplay({ error }: { error: ErrorObject }) {
471
- return (
472
- <div
473
- style={{
474
- position: 'absolute',
475
- top: 20,
476
- left: 20,
477
- color: 'black',
478
- backgroundColor: '#f8d7da',
479
- border: '1px solid #f5c6cb',
480
- borderRadius: '4px',
481
- padding: '20px',
482
- maxWidth: '400px',
483
- margin: '20px auto',
484
- boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
485
- fontFamily: 'monospace',
486
- }}
487
- >
488
- <h3 style={{ fontSize: '0.8rem', opacity: 0.8 }}>code: {error.code}</h3>
489
- <h1 style={{ fontSize: '1.2rem', lineHeight: 1.5 }}>{error.title}</h1>
490
- <p>{error.message}</p>
491
- </div>
492
- )
493
- }
package/src/config.ts DELETED
@@ -1,9 +0,0 @@
1
- export const log = (...args: any[]) => {
2
- try {
3
- if (localStorage.getItem('basic_debug') === 'true') {
4
- console.log('[basic]', ...args)
5
- }
6
- } catch (e) {
7
- // silently fail if localStorage is unavailable (SSR)
8
- }
9
- }
package/src/context.tsx DELETED
@@ -1,104 +0,0 @@
1
- import { createContext, useContext } from 'react'
2
- import type { BasicDB } from './core/db'
3
- import type { User, AuthResult, GetTokenOptions } from './core/auth/AuthManager'
4
- import type { DBMode } from './core/db'
5
-
6
- export enum DBStatus {
7
- LOADING = 'LOADING',
8
- OFFLINE = 'OFFLINE',
9
- CONNECTING = 'CONNECTING',
10
- ONLINE = 'ONLINE',
11
- SYNCING = 'SYNCING',
12
- ERROR = 'ERROR',
13
- ERROR_WILL_RETRY = 'ERROR_WILL_RETRY',
14
- ERROR_TOKEN_EXPIRED = 'ERROR_TOKEN_EXPIRED',
15
- }
16
-
17
- /** Snapshot of local schema vs server (for dev toolbar and debugging). */
18
- export type BasicSchemaDevInfo = {
19
- projectId: string | null
20
- localVersion: number | undefined
21
- status: string
22
- valid: boolean
23
- lastCheckedAt: number
24
- error?: string
25
- }
26
-
27
- /**
28
- * Context type for useBasic hook
29
- */
30
- export type BasicContextType = {
31
- isReady: boolean
32
- isSignedIn: boolean
33
- user: User | null
34
- did: string | null
35
- scope: string | null
36
- hasScope: (scope: string) => boolean
37
- missingScopes: () => string[]
38
-
39
- signIn: () => Promise<void>
40
- signInWithHandle: (handle: string) => Promise<void>
41
- signOut: () => Promise<void>
42
- signInWithCode: (code: string, state?: string) => Promise<AuthResult>
43
-
44
- getToken: (options?: GetTokenOptions) => Promise<string>
45
- getSignInUrl: (redirectUri?: string) => Promise<string>
46
-
47
- db: BasicDB
48
- dbStatus: DBStatus
49
- dbMode: DBMode
50
-
51
- /** Local schema vs server status; null if no schema on the provider. */
52
- devInfo: BasicSchemaDevInfo | null
53
- /** Re-run remote schema check (dev toolbar). */
54
- refreshSchemaStatus: () => Promise<void>
55
-
56
- isAuthReady: boolean
57
- signin: () => Promise<void>
58
- signout: () => Promise<void>
59
- signinWithCode: (code: string, state?: string) => Promise<AuthResult>
60
- getSignInLink: (redirectUri?: string) => Promise<string>
61
- }
62
-
63
- const noDb: BasicDB = {
64
- collection: () => {
65
- throw new Error('no basicdb found - initialization failed. double check your schema.')
66
- },
67
- }
68
-
69
- export const BasicContext = createContext<BasicContextType>({
70
- isReady: false,
71
- isSignedIn: false,
72
- user: null,
73
- did: null,
74
- scope: null,
75
- hasScope: () => false,
76
- missingScopes: () => [],
77
-
78
- signIn: () => Promise.resolve(),
79
- signInWithHandle: () => Promise.resolve(),
80
- signOut: () => Promise.resolve(),
81
- signInWithCode: () => Promise.resolve({ success: false }),
82
-
83
- getToken: (_options?: GetTokenOptions) => Promise.reject(new Error('no token')),
84
- getSignInUrl: () => Promise.resolve(''),
85
-
86
- db: noDb,
87
- dbStatus: DBStatus.LOADING,
88
- dbMode: 'sync',
89
-
90
- devInfo: null,
91
- refreshSchemaStatus: async () => {},
92
-
93
- isAuthReady: false,
94
- signin: () => Promise.resolve(),
95
- signout: () => Promise.resolve(),
96
- signinWithCode: () => Promise.resolve({ success: false }),
97
- getSignInLink: () => Promise.resolve(''),
98
- })
99
-
100
- export function useBasic() {
101
- return useContext(BasicContext)
102
- }
103
-
104
- export { noDb }