asasvirtuais 1.1.1 → 2.0.1
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/README.md +137 -46
- package/package.json +54 -54
- package/packages/hooks.tsx +2 -2
- package/packages/interface.ts +5 -3
- package/packages/react-interface.tsx +243 -322
package/README.md
CHANGED
|
@@ -191,7 +191,7 @@ function DeleteButton({ userId }: { userId: string }) {
|
|
|
191
191
|
|
|
192
192
|
## React Interface: Data-Driven Applications
|
|
193
193
|
|
|
194
|
-
The `react-interface` package provides
|
|
194
|
+
The `react-interface` package provides components and hooks for building data-driven React apps. Define your schema once, and use the components directly—no initialization needed.
|
|
195
195
|
|
|
196
196
|
### Complete Todo App Example
|
|
197
197
|
|
|
@@ -201,53 +201,68 @@ The `react-interface` package provides a complete abstraction for building data-
|
|
|
201
201
|
// app/database.ts
|
|
202
202
|
import { z } from 'zod';
|
|
203
203
|
|
|
204
|
-
export const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
204
|
+
export const todoSchema = {
|
|
205
|
+
readable: z.object({
|
|
206
|
+
id: z.string(),
|
|
207
|
+
text: z.string(),
|
|
208
|
+
completed: z.boolean(),
|
|
209
|
+
createdAt: z.date(),
|
|
210
|
+
}),
|
|
211
|
+
writable: z.object({
|
|
212
|
+
text: z.string(),
|
|
213
|
+
completed: z.boolean().optional(),
|
|
214
|
+
}),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// You can export multiple schemas
|
|
218
|
+
export const userSchema = {
|
|
219
|
+
readable: z.object({
|
|
220
|
+
id: z.string(),
|
|
221
|
+
name: z.string(),
|
|
222
|
+
email: z.string(),
|
|
223
|
+
}),
|
|
224
|
+
writable: z.object({
|
|
225
|
+
name: z.string(),
|
|
226
|
+
email: z.string(),
|
|
227
|
+
}),
|
|
228
|
+
}
|
|
218
229
|
```
|
|
219
230
|
|
|
220
|
-
#### 2.
|
|
231
|
+
#### 2. Create Your Table Interface
|
|
221
232
|
|
|
222
233
|
```typescript
|
|
223
234
|
// app/interface.ts
|
|
224
|
-
import {
|
|
225
|
-
import {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
SingleProvider,
|
|
234
|
-
useSingle,
|
|
235
|
-
} = reactInterface<typeof schema>(schema, yourDataInterface);
|
|
235
|
+
import { fetchInterface } from '@asasvirtuais/fetch-interface'
|
|
236
|
+
import { todoSchema } from './database'
|
|
237
|
+
|
|
238
|
+
// Create interface for your API
|
|
239
|
+
export const todosInterface = fetchInterface({
|
|
240
|
+
schema: todoSchema,
|
|
241
|
+
defaultTable: 'todos',
|
|
242
|
+
baseUrl: '/api/v1'
|
|
243
|
+
})
|
|
236
244
|
```
|
|
237
245
|
|
|
238
|
-
#### 3. Provide
|
|
246
|
+
#### 3. Provide Table Context
|
|
239
247
|
|
|
240
248
|
```tsx
|
|
241
|
-
// app/layout.tsx
|
|
242
|
-
import {
|
|
249
|
+
// app/todos/layout.tsx
|
|
250
|
+
import { TableProvider } from '@asasvirtuais/react-interface'
|
|
251
|
+
import { todoSchema, todosInterface } from '@/app/interface'
|
|
243
252
|
|
|
244
|
-
export default async function
|
|
245
|
-
const initialTodos = await fetchTodos()
|
|
253
|
+
export default async function TodosLayout({ children }) {
|
|
254
|
+
const initialTodos = await fetchTodos()
|
|
255
|
+
|
|
246
256
|
return (
|
|
247
|
-
<
|
|
257
|
+
<TableProvider
|
|
258
|
+
table="todos"
|
|
259
|
+
schema={todoSchema}
|
|
260
|
+
interface={todosInterface}
|
|
261
|
+
asAbove={initialTodos}
|
|
262
|
+
>
|
|
248
263
|
{children}
|
|
249
|
-
</
|
|
250
|
-
)
|
|
264
|
+
</TableProvider>
|
|
265
|
+
)
|
|
251
266
|
}
|
|
252
267
|
```
|
|
253
268
|
|
|
@@ -255,16 +270,19 @@ export default async function RootLayout({ children }) {
|
|
|
255
270
|
|
|
256
271
|
```tsx
|
|
257
272
|
// app/todos/page.tsx
|
|
258
|
-
'use client'
|
|
259
|
-
import {
|
|
273
|
+
'use client'
|
|
274
|
+
import { useDatabaseTable, CreateForm } from '@asasvirtuais/react-interface'
|
|
275
|
+
import { todoSchema } from '@/app/database'
|
|
260
276
|
|
|
261
277
|
function TodoList() {
|
|
262
|
-
const {
|
|
278
|
+
const { index, remove, update } = useDatabaseTable('todos')
|
|
279
|
+
const todos = Object.values(index.index)
|
|
263
280
|
|
|
264
281
|
return (
|
|
265
282
|
<>
|
|
266
|
-
<CreateForm
|
|
283
|
+
<CreateForm
|
|
267
284
|
table="todos"
|
|
285
|
+
schema={todoSchema}
|
|
268
286
|
defaults={{ text: '' }}
|
|
269
287
|
>
|
|
270
288
|
{({ fields, setField, submit, loading }) => (
|
|
@@ -300,7 +318,42 @@ function TodoList() {
|
|
|
300
318
|
))}
|
|
301
319
|
</ul>
|
|
302
320
|
</>
|
|
303
|
-
)
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### 5. Multiple Tables with DatabaseProvider
|
|
326
|
+
|
|
327
|
+
For apps with multiple tables, wrap them all in a DatabaseProvider:
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
// app/layout.tsx
|
|
331
|
+
import { DatabaseProvider, TableProvider } from '@asasvirtuais/react-interface'
|
|
332
|
+
import { todoSchema, userSchema } from './database'
|
|
333
|
+
import { todosInterface, usersInterface } from './interface'
|
|
334
|
+
|
|
335
|
+
export default async function RootLayout({ children }) {
|
|
336
|
+
const [initialTodos, initialUsers] = await Promise.all([
|
|
337
|
+
fetchTodos(),
|
|
338
|
+
fetchUsers()
|
|
339
|
+
])
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
<DatabaseProvider>
|
|
343
|
+
<TableProvider table="todos" schema={todoSchema} interface={todosInterface} asAbove={initialTodos}>
|
|
344
|
+
<TableProvider table="users" schema={userSchema} interface={usersInterface} asAbove={initialUsers}>
|
|
345
|
+
{children}
|
|
346
|
+
</TableProvider>
|
|
347
|
+
</TableProvider>
|
|
348
|
+
</DatabaseProvider>
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Now any component can access tables:
|
|
353
|
+
function MyComponent() {
|
|
354
|
+
const todos = useDatabaseTable('todos')
|
|
355
|
+
const users = useDatabaseTable('users')
|
|
356
|
+
// ...
|
|
304
357
|
}
|
|
305
358
|
```
|
|
306
359
|
|
|
@@ -374,8 +427,12 @@ In React, you control exactly when effects happen by writing code around form ac
|
|
|
374
427
|
Run code before submission:
|
|
375
428
|
|
|
376
429
|
```tsx
|
|
377
|
-
|
|
430
|
+
import { CreateForm } from '@asasvirtuais/react-interface'
|
|
431
|
+
import { messageSchema } from '@/app/database'
|
|
432
|
+
|
|
433
|
+
<CreateForm
|
|
378
434
|
table="messages"
|
|
435
|
+
schema={messageSchema}
|
|
379
436
|
defaults={{ content: '' }}
|
|
380
437
|
>
|
|
381
438
|
{({ fields, setField, submit, loading }) => (
|
|
@@ -405,8 +462,9 @@ Run code before submission:
|
|
|
405
462
|
Run code after successful submission:
|
|
406
463
|
|
|
407
464
|
```tsx
|
|
408
|
-
<CreateForm
|
|
465
|
+
<CreateForm
|
|
409
466
|
table="messages"
|
|
467
|
+
schema={messageSchema}
|
|
410
468
|
defaults={{ content: '' }}
|
|
411
469
|
onSuccess={(message) => {
|
|
412
470
|
// Post-flight effects - run after success
|
|
@@ -423,6 +481,39 @@ Run code after successful submission:
|
|
|
423
481
|
</CreateForm>
|
|
424
482
|
```
|
|
425
483
|
|
|
484
|
+
#### Using Field Values Without Submitting
|
|
485
|
+
|
|
486
|
+
Sometimes you want to use the form's field values without calling the server action:
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
<CreateForm
|
|
490
|
+
table="messages"
|
|
491
|
+
schema={messageSchema}
|
|
492
|
+
defaults={{ content: '' }}
|
|
493
|
+
>
|
|
494
|
+
{(form) => (
|
|
495
|
+
<div>
|
|
496
|
+
<textarea
|
|
497
|
+
value={form.fields.content}
|
|
498
|
+
onChange={(e) => form.setField('content', e.target.value)}
|
|
499
|
+
/>
|
|
500
|
+
|
|
501
|
+
{/* This button calls the server action */}
|
|
502
|
+
<button onClick={form.submit}>Send to Server</button>
|
|
503
|
+
|
|
504
|
+
{/* This button uses field values without calling the action */}
|
|
505
|
+
<button onClick={() => {
|
|
506
|
+
// Just use the field values directly for local operations
|
|
507
|
+
saveToLocalStorage(form.fields)
|
|
508
|
+
showPreview(form.fields)
|
|
509
|
+
}}>
|
|
510
|
+
Save Draft Locally
|
|
511
|
+
</button>
|
|
512
|
+
</div>
|
|
513
|
+
)}
|
|
514
|
+
</CreateForm>
|
|
515
|
+
```
|
|
516
|
+
|
|
426
517
|
### Backend Effects (API Routes)
|
|
427
518
|
|
|
428
519
|
On the backend, effects are just functions wrapping other functions. No framework magic.
|
|
@@ -433,10 +524,10 @@ On the backend, effects are just functions wrapping other functions. No framewor
|
|
|
433
524
|
// app/api/v1/[...params]/route.ts
|
|
434
525
|
import { tableInterface } from '@asasvirtuais/interface'
|
|
435
526
|
import { firestoreInterface } from '@/lib/firestore'
|
|
436
|
-
import {
|
|
527
|
+
import { messageSchema } from '@/app/database'
|
|
437
528
|
|
|
438
529
|
// Wrap your base interface with business logic
|
|
439
|
-
const messagesInterface = tableInterface(
|
|
530
|
+
const messagesInterface = tableInterface(messageSchema, 'messages', {
|
|
440
531
|
async create(props) {
|
|
441
532
|
// Pre-flight validation
|
|
442
533
|
await checkUserQuota(props.data.userId)
|
|
@@ -482,7 +573,7 @@ const messagesInterface = tableInterface(schema, 'messages', {
|
|
|
482
573
|
find: firestoreInterface.find,
|
|
483
574
|
list: firestoreInterface.list,
|
|
484
575
|
})
|
|
485
|
-
|
|
576
|
+
|
|
486
577
|
|
|
487
578
|
### Key Principles
|
|
488
579
|
|
package/package.json
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "asasvirtuais",
|
|
3
|
-
"type": "module",
|
|
4
|
-
"version": "
|
|
5
|
-
"description": "React form and action management utilities",
|
|
6
|
-
"directories": {
|
|
7
|
-
"packages": "./packages"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"packages",
|
|
11
|
-
"tsconfig.json"
|
|
12
|
-
],
|
|
13
|
-
"exports": {
|
|
14
|
-
"./package.json": "./package.json",
|
|
15
|
-
"./action": {
|
|
16
|
-
"types": "./packages/action.tsx",
|
|
17
|
-
"default": "./packages/action.tsx"
|
|
18
|
-
},
|
|
19
|
-
"./fields": {
|
|
20
|
-
"types": "./packages/fields.tsx",
|
|
21
|
-
"default": "./packages/fields.tsx"
|
|
22
|
-
},
|
|
23
|
-
"./form": {
|
|
24
|
-
"types": "./packages/form.tsx",
|
|
25
|
-
"default": "./packages/form.tsx"
|
|
26
|
-
},
|
|
27
|
-
"./hooks": {
|
|
28
|
-
"types": "./packages/hooks.tsx",
|
|
29
|
-
"default": "./packages/hooks.tsx"
|
|
30
|
-
},
|
|
31
|
-
"./interface": {
|
|
32
|
-
"types": "./packages/interface/interface.ts",
|
|
33
|
-
"default": "./packages/interface/interface.ts"
|
|
34
|
-
},
|
|
35
|
-
"./react-interface": {
|
|
36
|
-
"types": "./packages/react-interface.tsx",
|
|
37
|
-
"default": "./packages/react-interface.tsx"
|
|
38
|
-
},
|
|
39
|
-
"./next-interface": {
|
|
40
|
-
"types": "./packages/next-interface.tsx",
|
|
41
|
-
"default": "./packages/next-interface.tsx"
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"peerDependencies": {
|
|
45
|
-
"react": "^19.2.3"
|
|
46
|
-
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"next": "^16.1.1",
|
|
49
|
-
"zod": "^3.
|
|
50
|
-
},
|
|
51
|
-
"devDependencies": {
|
|
52
|
-
"@types/react": "^19.2.
|
|
53
|
-
}
|
|
54
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "asasvirtuais",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "2.0.1",
|
|
5
|
+
"description": "React form and action management utilities",
|
|
6
|
+
"directories": {
|
|
7
|
+
"packages": "./packages"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"packages",
|
|
11
|
+
"tsconfig.json"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
"./action": {
|
|
16
|
+
"types": "./packages/action.tsx",
|
|
17
|
+
"default": "./packages/action.tsx"
|
|
18
|
+
},
|
|
19
|
+
"./fields": {
|
|
20
|
+
"types": "./packages/fields.tsx",
|
|
21
|
+
"default": "./packages/fields.tsx"
|
|
22
|
+
},
|
|
23
|
+
"./form": {
|
|
24
|
+
"types": "./packages/form.tsx",
|
|
25
|
+
"default": "./packages/form.tsx"
|
|
26
|
+
},
|
|
27
|
+
"./hooks": {
|
|
28
|
+
"types": "./packages/hooks.tsx",
|
|
29
|
+
"default": "./packages/hooks.tsx"
|
|
30
|
+
},
|
|
31
|
+
"./interface": {
|
|
32
|
+
"types": "./packages/interface/interface.ts",
|
|
33
|
+
"default": "./packages/interface/interface.ts"
|
|
34
|
+
},
|
|
35
|
+
"./react-interface": {
|
|
36
|
+
"types": "./packages/react-interface.tsx",
|
|
37
|
+
"default": "./packages/react-interface.tsx"
|
|
38
|
+
},
|
|
39
|
+
"./next-interface": {
|
|
40
|
+
"types": "./packages/next-interface.tsx",
|
|
41
|
+
"default": "./packages/next-interface.tsx"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": "^19.2.3"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"next": "^16.1.1",
|
|
49
|
+
"zod": "^4.3.5"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/react": "^19.2.8"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/packages/hooks.tsx
CHANGED
|
@@ -80,7 +80,7 @@ export function useIndex<T>(value: Record<string, any>) {
|
|
|
80
80
|
}))
|
|
81
81
|
}, [])
|
|
82
82
|
|
|
83
|
-
const
|
|
83
|
+
const unset = useCallback((...params: readable[]) => {
|
|
84
84
|
setIndex(prev => {
|
|
85
85
|
const newState = { ...prev }
|
|
86
86
|
for (const data of params) {
|
|
@@ -97,7 +97,7 @@ export function useIndex<T>(value: Record<string, any>) {
|
|
|
97
97
|
array,
|
|
98
98
|
set,
|
|
99
99
|
setIndex,
|
|
100
|
-
|
|
100
|
+
unset,
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
package/packages/interface.ts
CHANGED
|
@@ -8,12 +8,14 @@ export interface TableInterface<Readable, Writable = Readable> {
|
|
|
8
8
|
list (props: ListProps<Readable>) : Promise<Readable[]>
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function tableInterface<Schema extends
|
|
11
|
+
export function tableInterface<Schema extends DatabaseSchema, Table extends keyof Schema & string>(schema: Schema, table?: Table | null, tableInterface?: TableInterface<z.infer<Schema[Table]['readable']>, z.infer<Schema[Table]['writable']>>) {
|
|
12
12
|
return tableInterface
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export interface
|
|
16
|
-
|
|
15
|
+
export interface TableSchema { readable: z.ZodObject<z.ZodRawShape>, writable: z.ZodObject<z.ZodRawShape> }
|
|
16
|
+
|
|
17
|
+
export interface DatabaseSchema {
|
|
18
|
+
[T: string]: TableSchema
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export type BasicOperators<T, K extends keyof T> = {
|
|
@@ -1,394 +1,315 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
useEffect,
|
|
6
|
-
useMemo,
|
|
7
|
-
createContext,
|
|
8
|
-
useContext,
|
|
9
|
-
} from 'react'
|
|
10
|
-
import { useAction as useAsyncAction, useIndex } from './hooks'
|
|
1
|
+
import z from 'zod'
|
|
2
|
+
import { DatabaseSchema, ListProps, TableInterface, TableSchema } from './interface'
|
|
3
|
+
import { createContextFromHook, useAction as useAsyncAction, useIndex } from './hooks'
|
|
4
|
+
import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useState } from 'react'
|
|
11
5
|
import { ActionProvider, useAction, useActionProvider } from './action'
|
|
12
|
-
import { TableInterface, ListProps, DatabaseInterface } from './interface'
|
|
13
6
|
import { FieldsProvider, useFields } from './fields'
|
|
14
7
|
|
|
15
|
-
export function
|
|
16
|
-
Database extends DatabaseInterface>(
|
|
17
|
-
database: Database,
|
|
18
|
-
{
|
|
19
|
-
find,
|
|
20
|
-
create,
|
|
21
|
-
update,
|
|
22
|
-
remove,
|
|
23
|
-
list,
|
|
24
|
-
}: TableInterface<
|
|
25
|
-
z.infer<Database[keyof Database]['readable']>,
|
|
26
|
-
z.infer<Database[keyof Database]['writable']>
|
|
27
|
-
>
|
|
28
|
-
) {
|
|
29
|
-
type TableKey = keyof Database & string
|
|
30
|
-
|
|
31
|
-
type TableProviderProps<Table extends TableKey> = {
|
|
32
|
-
table: Table
|
|
33
|
-
asAbove?: Record<string, z.infer<Database[Table]["readable"]>>
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function useTableProvider<Table extends TableKey>({
|
|
37
|
-
table,
|
|
38
|
-
asAbove,
|
|
39
|
-
}: TableProviderProps<Table>) {
|
|
40
|
-
type Readable = z.infer<Database[Table]['readable']>
|
|
41
|
-
type Writable = z.infer<Database[Table]['writable']>
|
|
42
|
-
|
|
43
|
-
const index = useIndex<Readable>({ ...(asAbove ?? {}) })
|
|
8
|
+
export function useDatabaseProvider() {
|
|
44
9
|
|
|
45
|
-
const
|
|
46
|
-
() => Object.values(index.index) as Readable[],
|
|
47
|
-
[index.index]
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
useEffect(function soBelow() {
|
|
51
|
-
index.setIndex((prev) => ({ ...prev, ...asAbove }))
|
|
52
|
-
}, [])
|
|
53
|
-
|
|
54
|
-
const methods = { find, create, update, remove, list } as TableInterface<
|
|
55
|
-
Readable,
|
|
56
|
-
Writable
|
|
57
|
-
>
|
|
10
|
+
const [database, setDatabase] = useState<Record<string, ReturnType<typeof useInterface<any>>>>({})
|
|
58
11
|
|
|
59
12
|
return {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
find: useAsyncAction(((props) => methods.find({ ...props, table }).then(res => {
|
|
63
|
-
index.set(res)
|
|
64
|
-
return res
|
|
65
|
-
})) as typeof find),
|
|
66
|
-
create: useAsyncAction(((props) => create({ ...props, table }).then(res => {
|
|
67
|
-
index.set(res)
|
|
68
|
-
return res
|
|
69
|
-
})) as typeof create),
|
|
70
|
-
update: useAsyncAction(((props) => update({ ...props, table }).then(res => {
|
|
71
|
-
index.set(res)
|
|
72
|
-
return res
|
|
73
|
-
})) as typeof update),
|
|
74
|
-
remove: useAsyncAction(((props) => remove({ ...props, table }).then(res => {
|
|
75
|
-
index.remove(res)
|
|
76
|
-
return res
|
|
77
|
-
})) as typeof remove),
|
|
78
|
-
list: useAsyncAction(((props) => list({ ...props, table }).then(arr => {
|
|
79
|
-
index.set(...arr)
|
|
80
|
-
return arr
|
|
81
|
-
})) as typeof list),
|
|
13
|
+
database,
|
|
14
|
+
setDatabase,
|
|
82
15
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
function useDatabaseProvider(tables: { [T in TableKey]: Record<string, z.infer<Database[T]['readable']>> }): {
|
|
86
|
-
[T in TableKey]: ReturnType<typeof useTableProvider<T>>
|
|
87
|
-
} {
|
|
88
|
-
return Object.fromEntries(
|
|
89
|
-
Object.entries(tables).map(([table, value]) => [
|
|
90
|
-
table, useTableProvider({ table: table as TableKey, asAbove: value })
|
|
91
|
-
])
|
|
92
|
-
) as {
|
|
93
|
-
[T in TableKey]: ReturnType<typeof useTableProvider<T>>
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Create a separate context for each table dynamically
|
|
97
|
-
const tableContexts = new Map<TableKey, React.Context<ReturnType<typeof useTableProvider<any>> | undefined>>();
|
|
98
|
-
|
|
99
|
-
function getTableContext<T extends TableKey>(table: T) {
|
|
100
|
-
if (!tableContexts.has(table)) {
|
|
101
|
-
tableContexts.set(table, createContext<ReturnType<typeof useTableProvider<T>> | undefined>(undefined));
|
|
102
|
-
}
|
|
103
|
-
return tableContexts.get(table)!;
|
|
104
|
-
}
|
|
16
|
+
}
|
|
105
17
|
|
|
106
|
-
|
|
107
|
-
children: React.ReactNode | ((props: ReturnType<typeof useTableProvider<Table>>) => React.ReactNode)
|
|
108
|
-
}) {
|
|
109
|
-
const context = useTableProvider(props);
|
|
110
|
-
const TableContext = getTableContext(props.table);
|
|
18
|
+
export const [DatabaseProvider, useDatabase] = createContextFromHook(useDatabaseProvider)
|
|
111
19
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
{typeof children === 'function' ? children(context) : children}
|
|
115
|
-
</TableContext.Provider>
|
|
116
|
-
)
|
|
117
|
-
}
|
|
20
|
+
export function useDatabaseTable<TSchema extends TableSchema>(table: string) {
|
|
21
|
+
const { database } = useDatabase()
|
|
118
22
|
|
|
119
|
-
|
|
120
|
-
children,
|
|
121
|
-
...tables
|
|
122
|
-
}: React.PropsWithChildren<{
|
|
123
|
-
[T in TableKey]?: Record<string, z.infer<Database[T]['readable']>>
|
|
124
|
-
}>) {
|
|
125
|
-
return Object.entries(tables).reduce(
|
|
126
|
-
(prev, [table, asAbove]) => {
|
|
127
|
-
return (
|
|
128
|
-
<TableProvider table={table as TableKey} asAbove={asAbove}>
|
|
129
|
-
{prev}
|
|
130
|
-
</TableProvider>
|
|
131
|
-
);
|
|
132
|
-
},
|
|
133
|
-
children as React.ReactNode
|
|
134
|
-
)
|
|
135
|
-
}
|
|
23
|
+
const tableMethods = database[table] as ReturnType<typeof useInterface<TSchema>> | undefined
|
|
136
24
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const context = useContext(TableContext);
|
|
25
|
+
if (!tableMethods)
|
|
26
|
+
throw new Error(`Table "${table}" is not defined in the database schema.`)
|
|
140
27
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
28
|
+
return tableMethods
|
|
29
|
+
}
|
|
144
30
|
|
|
145
|
-
|
|
146
|
-
|
|
31
|
+
export type TableProviderProps<TSchema extends TableSchema> = {
|
|
32
|
+
table: string
|
|
33
|
+
schema: TSchema
|
|
34
|
+
interface: TableInterface<z.infer<TSchema['readable']>, z.infer<TSchema['writable']>>
|
|
35
|
+
asAbove?: Record<string, z.infer<TSchema['readable']>>
|
|
36
|
+
}
|
|
147
37
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}: {
|
|
152
|
-
id: string
|
|
153
|
-
table: Table
|
|
154
|
-
}) {
|
|
155
|
-
const { index, find } = useTable(table)
|
|
156
|
-
const [single, setSingle] = useState<z.infer<Database[Table]['readable']>>(
|
|
157
|
-
() => index[id]
|
|
158
|
-
)
|
|
159
|
-
useEffect(() => {
|
|
160
|
-
if (!single) find.trigger({ id }).then(setSingle)
|
|
161
|
-
}, [])
|
|
162
|
-
useEffect(() => {
|
|
163
|
-
setSingle(index[id])
|
|
164
|
-
}, [index[id]])
|
|
38
|
+
export function useInterface<TSchema extends TableSchema>(table: string, {
|
|
39
|
+
find, create, update, remove, list
|
|
40
|
+
}: TableInterface<z.infer<TSchema['readable']>, z.infer<TSchema['writable']>>, index: ReturnType<typeof useIndex<z.infer<TSchema['readable']>>>) {
|
|
165
41
|
return {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
42
|
+
index,
|
|
43
|
+
find: useAsyncAction(((props) => find({ ...props, table }).then(res => {
|
|
44
|
+
index.set(res)
|
|
45
|
+
return res
|
|
46
|
+
})) as typeof find),
|
|
47
|
+
create: useAsyncAction(((props) => create({ ...props, table }).then(res => {
|
|
48
|
+
index.set(res)
|
|
49
|
+
return res
|
|
50
|
+
})) as typeof create),
|
|
51
|
+
update: useAsyncAction(((props) => update({ ...props, table }).then(res => {
|
|
52
|
+
index.set(res)
|
|
53
|
+
return res
|
|
54
|
+
})) as typeof update),
|
|
55
|
+
remove: useAsyncAction(((props) => remove({ ...props, table }).then(res => {
|
|
56
|
+
index.unset(res)
|
|
57
|
+
return res
|
|
58
|
+
})) as typeof remove),
|
|
59
|
+
list: useAsyncAction(((props) => list({ ...props, table }).then(arr => {
|
|
60
|
+
index.set(...arr)
|
|
61
|
+
return arr
|
|
62
|
+
})) as typeof list),
|
|
170
63
|
}
|
|
171
|
-
|
|
64
|
+
}
|
|
172
65
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
66
|
+
export function useTableProvider<TSchema extends TableSchema>({
|
|
67
|
+
table,
|
|
68
|
+
schema,
|
|
69
|
+
interface: tableInterface,
|
|
70
|
+
asAbove,
|
|
71
|
+
}: TableProviderProps<TSchema>) {
|
|
176
72
|
|
|
177
|
-
|
|
178
|
-
children,
|
|
179
|
-
...props
|
|
180
|
-
}: {
|
|
181
|
-
id: string
|
|
182
|
-
table: Table
|
|
183
|
-
children: React.ReactNode | ((props: ReturnType<typeof useSingleProvider<Table>>) => React.ReactNode)
|
|
184
|
-
}) {
|
|
185
|
-
const value = useSingleProvider(props)
|
|
186
|
-
if (!value.single) return null
|
|
187
|
-
return (
|
|
188
|
-
<SingleContext.Provider value={value}>
|
|
189
|
-
{typeof children === 'function' ? (
|
|
190
|
-
children(value)
|
|
191
|
-
) : (
|
|
192
|
-
children
|
|
193
|
-
)}
|
|
194
|
-
</SingleContext.Provider>
|
|
195
|
-
)
|
|
196
|
-
}
|
|
73
|
+
type Readable = z.infer<TableSchema['readable']>
|
|
197
74
|
|
|
198
|
-
|
|
199
|
-
useContext(SingleContext) as ReturnType<typeof useSingleProvider<Table>>
|
|
75
|
+
const index = useIndex<Readable>({ ...(asAbove ?? {}) })
|
|
200
76
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
77
|
+
useEffect(function soBelow() {
|
|
78
|
+
index.setIndex((prev) => ({ ...prev, ...asAbove }))
|
|
79
|
+
}, [])
|
|
80
|
+
|
|
81
|
+
return useInterface<TSchema>(table, tableInterface, index)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const [TableContextProvider, useTableContext] = createContextFromHook(useTableProvider<any>)
|
|
85
|
+
|
|
86
|
+
export function TableProvider<TSchema extends TableSchema>({ children, ...props }: PropsWithChildren<TableProviderProps<TSchema>>) {
|
|
87
|
+
return <TableContextProvider {...props}>{children}</TableContextProvider>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function useTable<TSchema extends TableSchema>() {
|
|
91
|
+
return useTableContext() as ReturnType<typeof useTableProvider<TSchema>>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function CreateForm<TSchema extends TableSchema>({ table, defaults, onSuccess, children }: {
|
|
95
|
+
table: string
|
|
96
|
+
schema: TSchema
|
|
97
|
+
defaults?: Partial<z.infer<TSchema['writable']>>
|
|
98
|
+
onSuccess?: (result: z.infer<TSchema['readable']>) => void
|
|
212
99
|
children: React.ReactNode | (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
100
|
+
(props: ReturnType<typeof useActionProvider<z.infer<TSchema['writable']>, z.infer<TSchema['readable']>>>
|
|
101
|
+
& ReturnType<typeof useFields<z.infer<TSchema['writable']>>>
|
|
102
|
+
) => React.ReactNode
|
|
216
103
|
)
|
|
217
|
-
|
|
218
|
-
type Readable = z.infer<
|
|
219
|
-
type Writable = z.infer<
|
|
104
|
+
}) {
|
|
105
|
+
type Readable = z.infer<TSchema['readable']>
|
|
106
|
+
type Writable = z.infer<TSchema['writable']>
|
|
220
107
|
|
|
221
|
-
const { create } =
|
|
108
|
+
const { create } = useDatabaseTable(table)
|
|
222
109
|
|
|
223
110
|
const callback = useCallback(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
111
|
+
async (fields: Writable) => {
|
|
112
|
+
const result = await create.trigger({ data: fields })
|
|
113
|
+
if (onSuccess) onSuccess(result as Readable)
|
|
114
|
+
return result
|
|
115
|
+
},
|
|
116
|
+
[create, onSuccess]
|
|
230
117
|
)
|
|
231
118
|
|
|
232
119
|
return (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
120
|
+
<FieldsProvider<Writable> defaults={defaults || ({} as Writable)}>
|
|
121
|
+
{fields => (
|
|
122
|
+
<ActionProvider<Writable, Readable> action={callback} params={fields.fields}>
|
|
123
|
+
{typeof children === 'function' ? (
|
|
124
|
+
form => children({ ...form, ...fields })
|
|
125
|
+
) : (
|
|
126
|
+
children
|
|
127
|
+
)}
|
|
128
|
+
</ActionProvider>
|
|
240
129
|
)}
|
|
241
|
-
|
|
242
|
-
)}
|
|
243
|
-
</FieldsProvider>
|
|
130
|
+
</FieldsProvider>
|
|
244
131
|
)
|
|
245
|
-
|
|
132
|
+
}
|
|
246
133
|
|
|
247
|
-
|
|
134
|
+
export function UpdateForm<TSchema extends TableSchema>({
|
|
135
|
+
schema,
|
|
248
136
|
table,
|
|
249
137
|
id,
|
|
250
138
|
defaults,
|
|
251
139
|
onSuccess,
|
|
252
140
|
children,
|
|
253
|
-
|
|
254
|
-
|
|
141
|
+
}: {
|
|
142
|
+
schema: TSchema
|
|
143
|
+
table: string
|
|
255
144
|
id: string
|
|
256
|
-
defaults?: Partial<z.infer<
|
|
257
|
-
onSuccess?: (result: z.infer<
|
|
145
|
+
defaults?: Partial<z.infer<TSchema['writable']>>
|
|
146
|
+
onSuccess?: (result: z.infer<TSchema['readable']>) => void
|
|
258
147
|
children: React.ReactNode | (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
148
|
+
(props: ReturnType<typeof useActionProvider<Partial<z.infer<TSchema['writable']>>, z.infer<TSchema['readable']>>>
|
|
149
|
+
& ReturnType<typeof useFields<z.infer<TSchema['writable']>>>
|
|
150
|
+
) => React.ReactNode
|
|
262
151
|
)
|
|
263
|
-
|
|
264
|
-
type Readable = z.infer<
|
|
265
|
-
type Writable = z.infer<
|
|
152
|
+
}) {
|
|
153
|
+
type Readable = z.infer<TSchema['readable']>
|
|
154
|
+
type Writable = z.infer<TSchema['writable']>
|
|
266
155
|
|
|
267
|
-
const { update } =
|
|
156
|
+
const { update } = useDatabaseTable(table)
|
|
268
157
|
|
|
269
158
|
const callback = useCallback(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
159
|
+
async (fields: Partial<Writable>) => {
|
|
160
|
+
const result = await update.trigger({ id, data: fields })
|
|
161
|
+
if (onSuccess) onSuccess(result as Readable)
|
|
162
|
+
return result
|
|
163
|
+
},
|
|
164
|
+
[update, id, onSuccess]
|
|
276
165
|
)
|
|
277
166
|
|
|
278
167
|
return (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
168
|
+
<FieldsProvider<Writable>
|
|
169
|
+
defaults={defaults || ({} as Partial<Writable>)}
|
|
170
|
+
>
|
|
171
|
+
{fields => (
|
|
172
|
+
<ActionProvider<Partial<Writable>, Readable> action={callback} params={fields.fields}>
|
|
173
|
+
{typeof children === 'function' ? (
|
|
174
|
+
form => children({ ...form, ...fields })
|
|
175
|
+
) : (
|
|
176
|
+
children
|
|
177
|
+
)}
|
|
178
|
+
</ActionProvider>
|
|
288
179
|
)}
|
|
289
|
-
|
|
290
|
-
)}
|
|
291
|
-
</FieldsProvider>
|
|
180
|
+
</FieldsProvider>
|
|
292
181
|
)
|
|
293
|
-
|
|
182
|
+
}
|
|
294
183
|
|
|
295
|
-
|
|
184
|
+
export function FilterForm<TSchema extends TableSchema>({
|
|
185
|
+
schema,
|
|
296
186
|
table,
|
|
297
187
|
defaults,
|
|
298
188
|
onSuccess,
|
|
299
189
|
children,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
190
|
+
}: {
|
|
191
|
+
schema: TSchema
|
|
192
|
+
table: string
|
|
193
|
+
defaults?: Partial<ListProps<z.infer<TSchema['readable']>>>
|
|
194
|
+
onSuccess?: (result: z.infer<TSchema['readable']>[]) => void
|
|
304
195
|
children: React.ReactNode | (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
196
|
+
(props: ReturnType<typeof useActionProvider<ListProps<z.infer<TSchema['readable']>>, z.infer<TSchema['readable']>[]>>
|
|
197
|
+
& ReturnType<typeof useFields<ListProps<z.infer<TSchema['readable']>>>>
|
|
198
|
+
) => React.ReactNode
|
|
308
199
|
)
|
|
309
|
-
|
|
310
|
-
type Readable = z.infer<
|
|
311
|
-
type Writable = z.infer<Database[T]['writable']>
|
|
200
|
+
}) {
|
|
201
|
+
type Readable = z.infer<TSchema['readable']>
|
|
312
202
|
|
|
313
|
-
const { list } =
|
|
203
|
+
const { list } = useDatabaseTable(table)
|
|
314
204
|
|
|
315
205
|
const callback = useCallback(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
206
|
+
async (fields: Omit<ListProps<Readable>, 'table'>) => {
|
|
207
|
+
const result = await list.trigger(fields)
|
|
208
|
+
if (onSuccess) onSuccess(result)
|
|
209
|
+
return result
|
|
210
|
+
},
|
|
211
|
+
[list, onSuccess]
|
|
322
212
|
)
|
|
323
213
|
|
|
324
214
|
return (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
215
|
+
<FieldsProvider<ListProps<Readable>>
|
|
216
|
+
defaults={(defaults || { query: {} }) as ListProps<Readable>}
|
|
217
|
+
>
|
|
218
|
+
{fields => (
|
|
219
|
+
<ActionProvider<ListProps<Readable>, Readable[]> action={callback} params={fields.fields}>
|
|
220
|
+
{typeof children === 'function' ? (
|
|
221
|
+
form => children({ ...form, ...fields })
|
|
222
|
+
) : (
|
|
223
|
+
children
|
|
224
|
+
)}
|
|
225
|
+
</ActionProvider>
|
|
334
226
|
)}
|
|
335
|
-
|
|
336
|
-
)}
|
|
337
|
-
</FieldsProvider>
|
|
227
|
+
</FieldsProvider>
|
|
338
228
|
)
|
|
339
|
-
|
|
229
|
+
}
|
|
340
230
|
|
|
341
|
-
|
|
231
|
+
export function useCreateForm<TSchema extends TableSchema>(schema: TSchema) {
|
|
342
232
|
return {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
233
|
+
...useFields<z.infer<TSchema['writable']>>(),
|
|
234
|
+
...useAction<
|
|
235
|
+
z.infer<TSchema['writable']>,
|
|
236
|
+
z.infer<TSchema['readable']>
|
|
237
|
+
>()
|
|
348
238
|
}
|
|
349
|
-
|
|
350
|
-
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function useUpdateForm<TSchema extends TableSchema>(schema: TSchema) {
|
|
351
242
|
return {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
243
|
+
...useFields<Partial<z.infer<TSchema['writable']>>>(),
|
|
244
|
+
...useAction<
|
|
245
|
+
Partial<z.infer<TSchema['writable']>>,
|
|
246
|
+
z.infer<TSchema['readable']>
|
|
247
|
+
>()
|
|
357
248
|
}
|
|
358
|
-
|
|
359
|
-
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function useFiltersForm <TSchema extends TableSchema>(schema: TSchema) {
|
|
360
252
|
return {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
253
|
+
...useFields<z.infer<TSchema['readable']>>(),
|
|
254
|
+
...useAction<z.infer<TSchema['readable']>,
|
|
255
|
+
z.infer<TSchema['readable']>[]
|
|
256
|
+
>()
|
|
365
257
|
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return {
|
|
369
|
-
DatabaseProvider,
|
|
370
|
-
useTable,
|
|
371
|
-
useTableProvider,
|
|
372
|
-
TableProvider,
|
|
373
|
-
SingleProvider,
|
|
374
|
-
useSingle,
|
|
375
|
-
CreateForm,
|
|
376
|
-
UpdateForm,
|
|
377
|
-
FilterForm,
|
|
378
|
-
useCreateForm,
|
|
379
|
-
useUpdateForm,
|
|
380
|
-
useFiltersForm,
|
|
381
|
-
}
|
|
382
258
|
}
|
|
383
259
|
|
|
384
|
-
export
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
260
|
+
export function useSingleProvider<T>({
|
|
261
|
+
id,
|
|
262
|
+
table,
|
|
263
|
+
}: {
|
|
264
|
+
id: string
|
|
265
|
+
table: string
|
|
266
|
+
}) {
|
|
267
|
+
const { index, find } = useDatabaseTable(table)
|
|
268
|
+
const [single, setSingle] = useState<T>(
|
|
269
|
+
// @ts-expect-error
|
|
270
|
+
() => index[id as keyof typeof index]
|
|
271
|
+
)
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
// @ts-expect-error
|
|
274
|
+
if (!single) find.trigger({ id }).then(setSingle)
|
|
275
|
+
}, [])
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
// @ts-expect-error
|
|
278
|
+
setSingle(index[id as keyof typeof index])
|
|
279
|
+
}, [index[id as keyof typeof index]])
|
|
280
|
+
return {
|
|
281
|
+
id,
|
|
282
|
+
single,
|
|
283
|
+
setSingle,
|
|
284
|
+
loading: find.loading,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const SingleContext = createContext<
|
|
289
|
+
ReturnType<typeof useSingleProvider> | undefined
|
|
290
|
+
>(undefined)
|
|
291
|
+
|
|
292
|
+
export function SingleProvider<T>({
|
|
293
|
+
children,
|
|
294
|
+
...props
|
|
295
|
+
}: {
|
|
296
|
+
id: string
|
|
297
|
+
table: string
|
|
298
|
+
children: React.ReactNode | ((props: ReturnType<typeof useSingleProvider>) => React.ReactNode)
|
|
299
|
+
}) {
|
|
300
|
+
const value = useSingleProvider(props)
|
|
301
|
+
if (!value.single) return null
|
|
302
|
+
return (
|
|
303
|
+
<SingleContext.Provider value={value}>
|
|
304
|
+
{typeof children === 'function' ? (
|
|
305
|
+
children(value)
|
|
306
|
+
) : (
|
|
307
|
+
children
|
|
308
|
+
)}
|
|
309
|
+
</SingleContext.Provider>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function useSingle<T>() {
|
|
314
|
+
return useContext(SingleContext) as ReturnType<typeof useSingleProvider<T>>
|
|
394
315
|
}
|