asasvirtuais 1.0.2 → 1.1.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.
- package/README.md +600 -0
- package/package.json +54 -33
- package/packages/action.tsx +0 -1
- package/packages/fields.tsx +0 -1
- package/packages/hooks.tsx +120 -0
- package/packages/interface.ts +50 -0
- package/packages/next-interface.ts +120 -0
- package/packages/react-interface.tsx +394 -0
package/README.md
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
# asasvirtuais
|
|
2
|
+
|
|
3
|
+
React form and action management utilities for building complex forms with async validation and multi-step workflows.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### From npm
|
|
8
|
+
```bash
|
|
9
|
+
npm install asasvirtuais
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### From esm.sh
|
|
13
|
+
```typescript
|
|
14
|
+
import { Form } from 'https://esm.sh/asasvirtuais@latest/form'
|
|
15
|
+
import { useFields } from 'https://esm.sh/asasvirtuais@latest/fields'
|
|
16
|
+
import { useAction } from 'https://esm.sh/asasvirtuais@latest/action'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
### Simple Form
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { Form } from 'asasvirtuais/form'
|
|
25
|
+
|
|
26
|
+
type LoginFields = {
|
|
27
|
+
email: string
|
|
28
|
+
password: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type LoginResult = {
|
|
32
|
+
token: string
|
|
33
|
+
user: { id: string; name: string }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function loginAction(fields: LoginFields): Promise<LoginResult> {
|
|
37
|
+
const response = await fetch('/api/login', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
body: JSON.stringify(fields)
|
|
40
|
+
})
|
|
41
|
+
return response.json()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function LoginForm() {
|
|
45
|
+
return (
|
|
46
|
+
<Form<LoginFields, LoginResult>
|
|
47
|
+
defaults={{ email: '', password: '' }}
|
|
48
|
+
action={loginAction}
|
|
49
|
+
onResult={(result) => console.log('Logged in:', result.user.name)}
|
|
50
|
+
>
|
|
51
|
+
{({ fields, setField, submit, loading, error }) => (
|
|
52
|
+
<form onSubmit={submit}>
|
|
53
|
+
<input
|
|
54
|
+
type="email"
|
|
55
|
+
value={fields.email}
|
|
56
|
+
onChange={(e) => setField('email', e.target.value)}
|
|
57
|
+
/>
|
|
58
|
+
<input
|
|
59
|
+
type="password"
|
|
60
|
+
value={fields.password}
|
|
61
|
+
onChange={(e) => setField('password', e.target.value)}
|
|
62
|
+
/>
|
|
63
|
+
<button type="submit" disabled={loading}>
|
|
64
|
+
{loading ? 'Logging in...' : 'Login'}
|
|
65
|
+
</button>
|
|
66
|
+
{error && <p>Error: {error.message}</p>}
|
|
67
|
+
</form>
|
|
68
|
+
)}
|
|
69
|
+
</Form>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Using Fields Only
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { FieldsProvider, useFields } from 'asasvirtuais/fields'
|
|
78
|
+
|
|
79
|
+
type ProfileFields = {
|
|
80
|
+
name: string
|
|
81
|
+
bio: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ProfileEditor() {
|
|
85
|
+
return (
|
|
86
|
+
<FieldsProvider<ProfileFields> defaults={{ name: '', bio: '' }}>
|
|
87
|
+
{({ fields, setField }) => (
|
|
88
|
+
<div>
|
|
89
|
+
<input
|
|
90
|
+
value={fields.name}
|
|
91
|
+
onChange={(e) => setField('name', e.target.value)}
|
|
92
|
+
/>
|
|
93
|
+
<textarea
|
|
94
|
+
value={fields.bio}
|
|
95
|
+
onChange={(e) => setField('bio', e.target.value)}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</FieldsProvider>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Using Actions Only
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { ActionProvider, useAction } from 'asasvirtuais/action'
|
|
108
|
+
|
|
109
|
+
async function deleteAccount(params: { userId: string }) {
|
|
110
|
+
await fetch(`/api/users/${params.userId}`, { method: 'DELETE' })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function DeleteButton({ userId }: { userId: string }) {
|
|
114
|
+
return (
|
|
115
|
+
<ActionProvider
|
|
116
|
+
params={{ userId }}
|
|
117
|
+
action={deleteAccount}
|
|
118
|
+
onResult={() => alert('Account deleted')}
|
|
119
|
+
>
|
|
120
|
+
{({ submit, loading }) => (
|
|
121
|
+
<button onClick={submit} disabled={loading}>
|
|
122
|
+
{loading ? 'Deleting...' : 'Delete Account'}
|
|
123
|
+
</button>
|
|
124
|
+
)}
|
|
125
|
+
</ActionProvider>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## `react-interface` for Data-Driven Applications
|
|
131
|
+
|
|
132
|
+
The `react-interface` package provides a powerful, high-level abstraction for building data-driven React applications. It's designed to work with a standardized `TableInterface` to provide a consistent, type-safe, and efficient way to interact with your data backend. It gives you React hooks and components that are automatically wired up to your API, handling data fetching, caching, and state management for you.
|
|
133
|
+
|
|
134
|
+
### Todo App Example
|
|
135
|
+
|
|
136
|
+
Let's walk through building a simple Todo app to demonstrate how to use `react-interface`.
|
|
137
|
+
|
|
138
|
+
#### 1. Define Your Schema
|
|
139
|
+
|
|
140
|
+
First, define the schema for your `todos` table using `zod`. This is the single source of truth for your data shapes.
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// app/database.ts
|
|
144
|
+
import { z } from 'zod';
|
|
145
|
+
|
|
146
|
+
export const schema = {
|
|
147
|
+
todos: {
|
|
148
|
+
readable: z.object({
|
|
149
|
+
id: z.string(),
|
|
150
|
+
text: z.string(),
|
|
151
|
+
completed: z.boolean(),
|
|
152
|
+
createdAt: z.date(),
|
|
153
|
+
}),
|
|
154
|
+
writable: z.object({
|
|
155
|
+
text: z.string(),
|
|
156
|
+
completed: z.boolean().optional(),
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### 2. Initialize the React Interface
|
|
163
|
+
|
|
164
|
+
Create an `interface.ts` file in your `app` directory to initialize the `reactInterface` and export the hooks and components.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// app/interface.ts
|
|
168
|
+
import { reactInterface } from '@asasvirtuais/interface';
|
|
169
|
+
import { schema } from './database';
|
|
170
|
+
|
|
171
|
+
// This would typically be a fetch or other data source implementation
|
|
172
|
+
const yourDataInterface = {
|
|
173
|
+
find: async (props) => { /* ... */ },
|
|
174
|
+
create: async (props) => { /* ... */ },
|
|
175
|
+
update: async (props) => { /* ... */ },
|
|
176
|
+
remove: async (props) => { /* ... */ },
|
|
177
|
+
list: async (props) => { /* ... */ },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const {
|
|
181
|
+
DatabaseProvider,
|
|
182
|
+
useTable,
|
|
183
|
+
CreateForm,
|
|
184
|
+
UpdateForm,
|
|
185
|
+
FilterForm, // Or ListForm
|
|
186
|
+
SingleProvider,
|
|
187
|
+
useSingle,
|
|
188
|
+
} = reactInterface<typeof schema>(schema, yourDataInterface);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### 3. Provide the Data Context
|
|
192
|
+
|
|
193
|
+
Wrap your application (or the relevant part of it) with the `DatabaseProvider` to make the data available to all child components.
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
// app/layout.tsx
|
|
197
|
+
import { DatabaseProvider } from '@/app/interface';
|
|
198
|
+
import { fetchTodos } from '@/app/api'; // Example API call
|
|
199
|
+
|
|
200
|
+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
201
|
+
const initialTodos = await fetchTodos(); // Fetch initial data on the server
|
|
202
|
+
return (
|
|
203
|
+
<html lang="en">
|
|
204
|
+
<body>
|
|
205
|
+
<DatabaseProvider todos={initialTodos}>
|
|
206
|
+
{children}
|
|
207
|
+
</DatabaseProvider>
|
|
208
|
+
</body>
|
|
209
|
+
</html>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### 4. Listing Todos and Using `useTable`
|
|
215
|
+
|
|
216
|
+
The `useTable` hook is the primary way to interact with your table's data. It gives you access to the data index (a key-value store of your items) and the CRUD methods.
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
// app/todos/page.tsx
|
|
220
|
+
'use client';
|
|
221
|
+
|
|
222
|
+
import { useTable } from '@/app/interface';
|
|
223
|
+
|
|
224
|
+
function TodoList() {
|
|
225
|
+
const { array: todos, remove, update } = useTable('todos');
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<ul>
|
|
229
|
+
{todos.map(todo => (
|
|
230
|
+
<li key={todo.id}>
|
|
231
|
+
<span
|
|
232
|
+
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
|
|
233
|
+
onClick={() => update.trigger({ id: todo.id, data: { completed: !todo.completed } })}
|
|
234
|
+
>
|
|
235
|
+
{todo.text}
|
|
236
|
+
</span>
|
|
237
|
+
<button onClick={() => remove.trigger({ id: todo.id })}>Delete</button>
|
|
238
|
+
</li>
|
|
239
|
+
))}
|
|
240
|
+
</ul>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### 5. Creating Todos with `CreateForm`
|
|
246
|
+
|
|
247
|
+
The `CreateForm` component provides a ready-to-use form for creating new items. It handles form state, submission, and updates the local data index on success.
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
// app/todos/page.tsx (continued)
|
|
251
|
+
import { CreateForm, useTable } from '@/app/interface';
|
|
252
|
+
import { z } from 'zod';
|
|
253
|
+
import { schema } from '@/app/database';
|
|
254
|
+
|
|
255
|
+
function AddTodoForm() {
|
|
256
|
+
return (
|
|
257
|
+
<CreateForm<typeof schema, 'todos'>
|
|
258
|
+
table="todos"
|
|
259
|
+
defaults={{ text: '' }}
|
|
260
|
+
onSuccess={(newTodo) => {
|
|
261
|
+
console.log('Successfully created:', newTodo.text);
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
{({ fields, setField, submit, loading }) => (
|
|
265
|
+
<form onSubmit={submit}>
|
|
266
|
+
<input
|
|
267
|
+
value={fields.text}
|
|
268
|
+
onChange={(e) => setField('text', e.target.value)}
|
|
269
|
+
placeholder="What needs to be done?"
|
|
270
|
+
/>
|
|
271
|
+
<button type="submit" disabled={loading}>
|
|
272
|
+
{loading ? 'Adding...' : 'Add Todo'}
|
|
273
|
+
</button>
|
|
274
|
+
</form>
|
|
275
|
+
)}
|
|
276
|
+
</CreateForm>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### 6. Updating Todos with `UpdateForm`
|
|
282
|
+
|
|
283
|
+
Similarly, `UpdateForm` is used for editing existing items. It's often used in combination with `SingleProvider` and `useSingle` to work with a specific item.
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
// app/todos/[id]/edit/page.tsx
|
|
287
|
+
'use client';
|
|
288
|
+
|
|
289
|
+
import { UpdateForm, useSingle } from '@/app/interface';
|
|
290
|
+
import { z } from 'zod';
|
|
291
|
+
import { schema } from '@/app/database';
|
|
292
|
+
|
|
293
|
+
function EditTodoForm() {
|
|
294
|
+
const { single: todo } = useSingle('todos');
|
|
295
|
+
|
|
296
|
+
if (!todo) return <div>Loading...</div>;
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<UpdateForm<typeof schema, 'todos'>
|
|
300
|
+
table="todos"
|
|
301
|
+
id={todo.id}
|
|
302
|
+
defaults={{ text: todo.text }}
|
|
303
|
+
onSuccess={() => {
|
|
304
|
+
// Redirect or show a success message
|
|
305
|
+
}}
|
|
306
|
+
>
|
|
307
|
+
{({ fields, setField, submit, loading }) => (
|
|
308
|
+
<form onSubmit={submit}>
|
|
309
|
+
<input
|
|
310
|
+
value={fields.text}
|
|
311
|
+
onChange={(e) => setField('text', e.target.value)}
|
|
312
|
+
/>
|
|
313
|
+
<button type="submit" disabled={loading}>
|
|
314
|
+
{loading ? 'Saving...' : 'Save Changes'}
|
|
315
|
+
</button>
|
|
316
|
+
</form>
|
|
317
|
+
)}
|
|
318
|
+
</UpdateForm>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### 7. Filtering Todos with `FilterForm` (ListForm)
|
|
324
|
+
|
|
325
|
+
`FilterForm` allows you to build UIs for filtering and searching your data. It works just like other forms, but its action triggers the `list` method of your `TableInterface`.
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
// app/todos/page.tsx (continued)
|
|
329
|
+
import { FilterForm, useTable } from '@/app/interface';
|
|
330
|
+
import { z } from 'zod';
|
|
331
|
+
import { schema } from '@/app/database';
|
|
332
|
+
|
|
333
|
+
function TodoFilter() {
|
|
334
|
+
const { list } = useTable('todos');
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<FilterForm<typeof schema, 'todos'>
|
|
338
|
+
table="todos"
|
|
339
|
+
defaults={{ query: { completed: false } }}
|
|
340
|
+
onSuccess={(results) => {
|
|
341
|
+
console.log(`Found ${results.length} matching todos.`);
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
{({ fields, setField, submit }) => (
|
|
345
|
+
<div>
|
|
346
|
+
<label>
|
|
347
|
+
<input
|
|
348
|
+
type="checkbox"
|
|
349
|
+
checked={fields.query.completed}
|
|
350
|
+
onChange={(e) => setField('query.completed', e.target.checked)}
|
|
351
|
+
/>
|
|
352
|
+
Show completed
|
|
353
|
+
</label>
|
|
354
|
+
<button onClick={submit}>Apply Filter</button>
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
</FilterForm>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Advanced Usage
|
|
363
|
+
|
|
364
|
+
### Nested Forms: Multi-Step Async Validation
|
|
365
|
+
|
|
366
|
+
Use nested forms to validate intermediate steps and populate parent form fields based on child form results.
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
import { Form } from 'asasvirtuais/form'
|
|
370
|
+
|
|
371
|
+
type EmailFields = { email: string }
|
|
372
|
+
type EmailResult = { userId: string; exists: boolean }
|
|
373
|
+
|
|
374
|
+
type PasswordFields = { userId: string; password: string }
|
|
375
|
+
type PasswordResult = { token: string }
|
|
376
|
+
|
|
377
|
+
async function checkEmail(fields: EmailFields): Promise<EmailResult> {
|
|
378
|
+
const response = await fetch('/api/check-email', {
|
|
379
|
+
method: 'POST',
|
|
380
|
+
body: JSON.stringify(fields)
|
|
381
|
+
})
|
|
382
|
+
return response.json()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function verifyPassword(fields: PasswordFields): Promise<PasswordResult> {
|
|
386
|
+
const response = await fetch('/api/verify-password', {
|
|
387
|
+
method: 'POST',
|
|
388
|
+
body: JSON.stringify(fields)
|
|
389
|
+
})
|
|
390
|
+
return response.json()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function MultiStepLogin() {
|
|
394
|
+
return (
|
|
395
|
+
<Form<EmailFields, EmailResult>
|
|
396
|
+
defaults={{ email: '' }}
|
|
397
|
+
action={checkEmail}
|
|
398
|
+
>
|
|
399
|
+
{(emailForm) => (
|
|
400
|
+
<div>
|
|
401
|
+
<h2>Step 1: Enter Email</h2>
|
|
402
|
+
<input
|
|
403
|
+
type="email"
|
|
404
|
+
value={emailForm.fields.email}
|
|
405
|
+
onChange={(e) => emailForm.setField('email', e.target.value)}
|
|
406
|
+
/>
|
|
407
|
+
<button onClick={emailForm.submit} disabled={emailForm.loading}>
|
|
408
|
+
{emailForm.loading ? 'Checking...' : 'Next'}
|
|
409
|
+
</button>
|
|
410
|
+
{emailForm.error && <p>Error: {emailForm.error.message}</p>}
|
|
411
|
+
|
|
412
|
+
{emailForm.result && emailForm.result.exists && (
|
|
413
|
+
<Form<PasswordFields, PasswordResult>
|
|
414
|
+
defaults={{
|
|
415
|
+
userId: emailForm.result.userId,
|
|
416
|
+
password: ''
|
|
417
|
+
}}
|
|
418
|
+
action={verifyPassword}
|
|
419
|
+
onResult={(result) => {
|
|
420
|
+
localStorage.setItem('token', result.token)
|
|
421
|
+
window.location.href = '/dashboard'
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
{(passwordForm) => (
|
|
425
|
+
<div>
|
|
426
|
+
<h2>Step 2: Enter Password</h2>
|
|
427
|
+
<input
|
|
428
|
+
type="password"
|
|
429
|
+
value={passwordForm.fields.password}
|
|
430
|
+
onChange={(e) => passwordForm.setField('password', e.target.value)}
|
|
431
|
+
/>
|
|
432
|
+
<button onClick={passwordForm.submit} disabled={passwordForm.loading}>
|
|
433
|
+
{passwordForm.loading ? 'Verifying...' : 'Login'}
|
|
434
|
+
</button>
|
|
435
|
+
{passwordForm.error && <p>Error: {passwordForm.error.message}</p>}
|
|
436
|
+
</div>
|
|
437
|
+
)}
|
|
438
|
+
</Form>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
)}
|
|
442
|
+
</Form>
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Complex Multi-Step: Address Validation
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
type AddressLookupFields = { zipCode: string }
|
|
451
|
+
type AddressLookupResult = {
|
|
452
|
+
city: string
|
|
453
|
+
state: string
|
|
454
|
+
country: string
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
type FullAddressFields = {
|
|
458
|
+
zipCode: string
|
|
459
|
+
city: string
|
|
460
|
+
state: string
|
|
461
|
+
country: string
|
|
462
|
+
street: string
|
|
463
|
+
number: string
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
type OrderResult = { orderId: string }
|
|
467
|
+
|
|
468
|
+
async function lookupAddress(fields: AddressLookupFields): Promise<AddressLookupResult> {
|
|
469
|
+
const response = await fetch(`/api/address/lookup?zip=${fields.zipCode}`)
|
|
470
|
+
return response.json()
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function createOrder(fields: FullAddressFields): Promise<OrderResult> {
|
|
474
|
+
const response = await fetch('/api/orders', {
|
|
475
|
+
method: 'POST',
|
|
476
|
+
body: JSON.stringify(fields)
|
|
477
|
+
})
|
|
478
|
+
return response.json()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function CheckoutForm() {
|
|
482
|
+
return (
|
|
483
|
+
<Form<AddressLookupFields, AddressLookupResult>
|
|
484
|
+
defaults={{ zipCode: '' }}
|
|
485
|
+
action={lookupAddress}
|
|
486
|
+
>
|
|
487
|
+
{(zipForm) => (
|
|
488
|
+
<div>
|
|
489
|
+
<h3>Enter ZIP Code</h3>
|
|
490
|
+
<input
|
|
491
|
+
type="text"
|
|
492
|
+
value={zipForm.fields.zipCode}
|
|
493
|
+
onChange={(e) => zipForm.setField('zipCode', e.target.value)}
|
|
494
|
+
placeholder="ZIP Code"
|
|
495
|
+
/>
|
|
496
|
+
<button onClick={zipForm.submit} disabled={zipForm.loading}>
|
|
497
|
+
{zipForm.loading ? 'Looking up...' : 'Lookup Address'}
|
|
498
|
+
</button>
|
|
499
|
+
|
|
500
|
+
{zipForm.result && (
|
|
501
|
+
<Form<FullAddressFields, OrderResult>
|
|
502
|
+
defaults={{
|
|
503
|
+
zipCode: zipForm.fields.zipCode,
|
|
504
|
+
city: zipForm.result.city,
|
|
505
|
+
state: zipForm.result.state,
|
|
506
|
+
country: zipForm.result.country,
|
|
507
|
+
street: '',
|
|
508
|
+
number: ''
|
|
509
|
+
}}
|
|
510
|
+
action={createOrder}
|
|
511
|
+
onResult={(result) => alert(`Order created: ${result.orderId}`)}
|
|
512
|
+
>
|
|
513
|
+
{(addressForm) => (
|
|
514
|
+
<div>
|
|
515
|
+
<h3>Complete Address</h3>
|
|
516
|
+
<p>City: {addressForm.fields.city}</p>
|
|
517
|
+
<p>State: {addressForm.fields.state}</p>
|
|
518
|
+
<input
|
|
519
|
+
value={addressForm.fields.street}
|
|
520
|
+
onChange={(e) => addressForm.setField('street', e.target.value)}
|
|
521
|
+
placeholder="Street"
|
|
522
|
+
/>
|
|
523
|
+
<input
|
|
524
|
+
value={addressForm.fields.number}
|
|
525
|
+
onChange={(e) => addressForm.setField('number', e.target.value)}
|
|
526
|
+
placeholder="Number"
|
|
527
|
+
/>
|
|
528
|
+
<button onClick={addressForm.submit} disabled={addressForm.loading}>
|
|
529
|
+
{addressForm.loading ? 'Creating Order...' : 'Place Order'}
|
|
530
|
+
</button>
|
|
531
|
+
{addressForm.error && <p>Error: {addressForm.error.message}</p>}
|
|
532
|
+
</div>
|
|
533
|
+
)}
|
|
534
|
+
</Form>
|
|
535
|
+
)}
|
|
536
|
+
</div>
|
|
537
|
+
)}
|
|
538
|
+
</Form>
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## API Reference
|
|
544
|
+
|
|
545
|
+
### `Form<Fields, Result>`
|
|
546
|
+
|
|
547
|
+
Combined fields and action management.
|
|
548
|
+
|
|
549
|
+
**Props:**
|
|
550
|
+
- `defaults?: Partial<Fields>` - Initial field values
|
|
551
|
+
- `action: (fields: Fields) => Promise<Result>` - Async action to perform
|
|
552
|
+
- `onResult?: (result: Result) => void` - Success callback
|
|
553
|
+
- `onError?: (error: Error) => void` - Error callback
|
|
554
|
+
- `autoTrigger?: boolean` - Auto-trigger action on mount
|
|
555
|
+
- `children: ReactNode | (props) => ReactNode` - Render prop or children
|
|
556
|
+
|
|
557
|
+
**Render Props:**
|
|
558
|
+
- `fields: Fields` - Current field values
|
|
559
|
+
- `setField: (name, value) => void` - Update single field
|
|
560
|
+
- `setFields: (fields) => void` - Update multiple fields
|
|
561
|
+
- `submit: (e?) => Promise<void>` - Trigger action
|
|
562
|
+
- `loading: boolean` - Action loading state
|
|
563
|
+
- `result: Result | null` - Action result
|
|
564
|
+
- `error: Error | null` - Action error
|
|
565
|
+
|
|
566
|
+
### `FieldsProvider<T>`
|
|
567
|
+
|
|
568
|
+
Field state management only.
|
|
569
|
+
|
|
570
|
+
**Props:**
|
|
571
|
+
- `defaults?: Partial<T>` - Initial field values
|
|
572
|
+
- `children: ReactNode | (props) => ReactNode` - Render prop or children
|
|
573
|
+
|
|
574
|
+
**Hook: `useFields<T>()`**
|
|
575
|
+
- `fields: T` - Current field values
|
|
576
|
+
- `setField: (name, value) => void` - Update single field
|
|
577
|
+
- `setFields: (fields) => void` - Update multiple fields
|
|
578
|
+
|
|
579
|
+
### `ActionProvider<Params, Result>`
|
|
580
|
+
|
|
581
|
+
Action management only.
|
|
582
|
+
|
|
583
|
+
**Props:**
|
|
584
|
+
- `params: Partial<Params>` - Action parameters
|
|
585
|
+
- `action: (params) => Promise<Result>` - Async action
|
|
586
|
+
- `onResult?: (result: Result) => void` - Success callback
|
|
587
|
+
- `onError?: (error: Error) => void` - Error callback
|
|
588
|
+
- `autoTrigger?: boolean` - Auto-trigger on mount
|
|
589
|
+
- `children: ReactNode | (props) => ReactNode` - Render prop or children
|
|
590
|
+
|
|
591
|
+
**Hook: `useAction<Params, Result>()`**
|
|
592
|
+
- `params: Partial<Params>` - Current parameters
|
|
593
|
+
- `submit: (e?) => Promise<void>` - Trigger action
|
|
594
|
+
- `loading: boolean` - Loading state
|
|
595
|
+
- `result: Result | null` - Action result
|
|
596
|
+
- `error: Error | null` - Action error
|
|
597
|
+
|
|
598
|
+
## License
|
|
599
|
+
|
|
600
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,33 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "asasvirtuais",
|
|
3
|
-
"type": "module",
|
|
4
|
-
"version": "1.0
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"./
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "asasvirtuais",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.1.0",
|
|
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.25.76"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/react": "^19.2.7"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/packages/action.tsx
CHANGED
package/packages/fields.tsx
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useState, useCallback, useEffect, useMemo } from 'react'
|
|
3
|
+
|
|
4
|
+
type StoreProps<T> = {
|
|
5
|
+
[table: string]: (T & { id: string} )[]
|
|
6
|
+
}
|
|
7
|
+
function useStoreProvider<T>(props: StoreProps<T>) {
|
|
8
|
+
return Object.fromEntries(
|
|
9
|
+
Object.entries(props).map(
|
|
10
|
+
([table, initial]) => [table, useIndex<T>({ initial: initial as T & { id: string} [] })]
|
|
11
|
+
)
|
|
12
|
+
) as {
|
|
13
|
+
[table: string]: ReturnType<typeof useIndex<T>>
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const [StoreProvider, useStore] = createContextFromHook((useStoreProvider<any>))
|
|
18
|
+
|
|
19
|
+
export function useAction<Props, Result, Defaults = Partial<Props>>(action: (props: Props) => Promise<Result>, {
|
|
20
|
+
onSuccess, autoTrigger, ...props
|
|
21
|
+
}: {
|
|
22
|
+
defaults?: Defaults
|
|
23
|
+
onSuccess?: (result: Result, props?: Props) => void
|
|
24
|
+
autoTrigger?: boolean
|
|
25
|
+
} = {}) {
|
|
26
|
+
const [loading, setLoading] = useState<boolean>(false)
|
|
27
|
+
const [error, setError] = useState()
|
|
28
|
+
const [result, setResult] = useState<Result>()
|
|
29
|
+
const [defaults, setDefaults] = useState<Defaults>(props.defaults ?? {} as Defaults)
|
|
30
|
+
|
|
31
|
+
const trigger = useCallback(async (props: Omit<Props, keyof Defaults>): Promise<Result> => {
|
|
32
|
+
try {
|
|
33
|
+
setLoading(true)
|
|
34
|
+
const result = await action({
|
|
35
|
+
...props,
|
|
36
|
+
...defaults,
|
|
37
|
+
} as Props)
|
|
38
|
+
|
|
39
|
+
setResult(result)
|
|
40
|
+
if (onSuccess)
|
|
41
|
+
onSuccess(result)
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// @ts-expect-error
|
|
46
|
+
setError(error)
|
|
47
|
+
throw error
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false)
|
|
50
|
+
}
|
|
51
|
+
}, [defaults, onSuccess, loading, setLoading])
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (autoTrigger)
|
|
55
|
+
trigger(props as Omit<Props, keyof Defaults>)
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
trigger,
|
|
60
|
+
loading,
|
|
61
|
+
error,
|
|
62
|
+
result,
|
|
63
|
+
defaults,
|
|
64
|
+
setDefaults,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function useIndex<T>(value: Record<string, any>) {
|
|
69
|
+
|
|
70
|
+
type readable = T
|
|
71
|
+
|
|
72
|
+
const [index, setIndex] = useState<Record<string, T>>(() => value)
|
|
73
|
+
|
|
74
|
+
const array = useMemo(() => Object.values(index) as readable[], [index])
|
|
75
|
+
|
|
76
|
+
const set = useCallback((...params: readable[]) => {
|
|
77
|
+
setIndex(prev => ({
|
|
78
|
+
...prev,
|
|
79
|
+
...Object.fromEntries(params.map(data => ([(data as readable & { id: string} ).id, data])))
|
|
80
|
+
}))
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
const remove = useCallback((...params: readable[]) => {
|
|
84
|
+
setIndex(prev => {
|
|
85
|
+
const newState = { ...prev }
|
|
86
|
+
for (const data of params) {
|
|
87
|
+
const id = (data as readable & { id: string} ).id
|
|
88
|
+
if (newState[id])
|
|
89
|
+
delete newState[id]
|
|
90
|
+
}
|
|
91
|
+
return newState
|
|
92
|
+
})
|
|
93
|
+
}, [])
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
index,
|
|
97
|
+
array,
|
|
98
|
+
set,
|
|
99
|
+
setIndex,
|
|
100
|
+
remove,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createContextFromHook<Props, Result>(useHook: (props: Props) => Result) {
|
|
105
|
+
|
|
106
|
+
const Context = React.createContext<Result | undefined>(undefined)
|
|
107
|
+
|
|
108
|
+
function Provider({ children, ...props }: React.PropsWithChildren<Props>) {
|
|
109
|
+
|
|
110
|
+
const value = useHook(props as Props) as Result
|
|
111
|
+
|
|
112
|
+
return <Context.Provider value={value}>{children}</Context.Provider>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function useContext() {
|
|
116
|
+
return React.useContext(Context) as Result
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return [Provider, useContext] as const
|
|
120
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export interface TableInterface<Readable, Writable = Readable> {
|
|
4
|
+
find (props: FindProps) : Promise<Readable>
|
|
5
|
+
create (props: CreateProps<Writable>) : Promise<Readable>
|
|
6
|
+
update (props: UpdateProps<Writable>) : Promise<Readable>
|
|
7
|
+
remove (props: RemoveProps) : Promise<Readable>
|
|
8
|
+
list (props: ListProps<Readable>) : Promise<Readable[]>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function tableInterface<Schema extends DatabaseInterface, Table extends keyof Schema & string>(schema: Schema, table?: Table | null, tableInterface?: TableInterface<z.infer<Schema[Table]['readable']>, z.infer<Schema[Table]['writable']>>) {
|
|
12
|
+
return tableInterface
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DatabaseInterface {
|
|
16
|
+
[T: string]: { readable: z.ZodObject<z.ZodRawShape>, writable: z.ZodObject<z.ZodRawShape> }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type BasicOperators<T, K extends keyof T> = {
|
|
20
|
+
'$ne' ?: T[K]
|
|
21
|
+
'$lt' ?: T[K]
|
|
22
|
+
'$lte' ?: T[K]
|
|
23
|
+
'$gt' ?: T[K]
|
|
24
|
+
'$gte' ?: T[K]
|
|
25
|
+
'$in' ?: T[K][]
|
|
26
|
+
'$nin' ?: T[K][]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type Filters<T> = {
|
|
30
|
+
'$limit' ?: number
|
|
31
|
+
'$skip' ?: number
|
|
32
|
+
'$select' ?: Array<keyof T>
|
|
33
|
+
'$sort' ?: { [K in keyof T]?: 1 | -1 }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Query<T = any> = { [K in keyof T]?: T[K] | BasicOperators<T, K> } & Filters<T> & {
|
|
37
|
+
'$or' ?: Array<Query<T>>
|
|
38
|
+
'$and' ?: Array<Query<T>>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type Operators<T, K extends keyof T> = BasicOperators<T, K> & {
|
|
42
|
+
'$or' ?: Array<Query<T>>
|
|
43
|
+
'$and' ?: Array<Query<T>>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FindProps { table?: string, id: string }
|
|
47
|
+
export interface RemoveProps { table?: string, id: string }
|
|
48
|
+
export interface CreateProps<T = any> { table?: string, data: T }
|
|
49
|
+
export interface UpdateProps<T = any> { table?: string, id: string, data: Partial<T> }
|
|
50
|
+
export interface ListProps<T = any> { table?: string, query?: Query<T> }
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server'
|
|
2
|
+
import { TableInterface } from './interface'
|
|
3
|
+
|
|
4
|
+
export function routes(implementation: TableInterface<any, any>) {
|
|
5
|
+
return {
|
|
6
|
+
// GET /api/v1/[table]/[id]
|
|
7
|
+
find: (
|
|
8
|
+
request: NextRequest,
|
|
9
|
+
{ params }: { params: Promise<{ table: string; id: string} >}
|
|
10
|
+
) => handleRoute(request, params, ({ table, id }) => implementation.find({ table, id })
|
|
11
|
+
),
|
|
12
|
+
|
|
13
|
+
// GET /api/v1/[table]
|
|
14
|
+
list: (
|
|
15
|
+
request: NextRequest,
|
|
16
|
+
{ params }: { params: Promise<{ table: string} >}
|
|
17
|
+
) => handleRoute(request, params, ({ table }) => {
|
|
18
|
+
const url = new URL(request.url)
|
|
19
|
+
const query = Object.fromEntries(url.searchParams)
|
|
20
|
+
return implementation.list({ table, query })
|
|
21
|
+
}),
|
|
22
|
+
|
|
23
|
+
// POST /api/v1/[table]
|
|
24
|
+
create: (
|
|
25
|
+
request: NextRequest,
|
|
26
|
+
{ params }: { params: Promise<{ table: string} >}
|
|
27
|
+
) => handleRoute(request, params, async ({ table }) => {
|
|
28
|
+
const data = await request.json()
|
|
29
|
+
return implementation.create({ table, data })
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
// PATCH /api/v1/[table]/[id]
|
|
33
|
+
update: (
|
|
34
|
+
request: NextRequest,
|
|
35
|
+
{ params }: { params: Promise<{ table: string; id: string} >}
|
|
36
|
+
) => handleRoute(request, params, async ({ table, id }) => {
|
|
37
|
+
const data = await request.json()
|
|
38
|
+
return implementation.update({ table, id, data })
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
// DELETE /api/v1/[table]/[id]
|
|
42
|
+
remove: (
|
|
43
|
+
request: NextRequest,
|
|
44
|
+
{ params }: { params: Promise<{ table: string; id: string} >}
|
|
45
|
+
) => handleRoute(request, params, ({ table, id }) => implementation.remove({ table, id })
|
|
46
|
+
),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function handleRoute<P, R>(
|
|
51
|
+
request: NextRequest,
|
|
52
|
+
params: Promise<P>,
|
|
53
|
+
handler: (resolvedParams: any) => Promise<R>
|
|
54
|
+
) {
|
|
55
|
+
try {
|
|
56
|
+
const resolvedParams = await params
|
|
57
|
+
const result = await handler(resolvedParams)
|
|
58
|
+
return Response.json(result)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error("Route error:", error)
|
|
61
|
+
return Response.json(
|
|
62
|
+
{ error: error instanceof Error ? error.message : "Unknown error" },
|
|
63
|
+
{ status: 500 }
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Utility for creating a single dynamic route handler
|
|
68
|
+
export function createDynamicRoute(implementation: TableInterface<any, any>) {
|
|
69
|
+
const routeHandlers = routes(implementation)
|
|
70
|
+
|
|
71
|
+
return async function dynamicRoute(
|
|
72
|
+
request: NextRequest,
|
|
73
|
+
{ params: promise }: { params: Promise<{ params: string[]} >}
|
|
74
|
+
) {
|
|
75
|
+
const { params } = await promise
|
|
76
|
+
const [table, id] = params
|
|
77
|
+
|
|
78
|
+
const method = request.method
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
switch (method) {
|
|
82
|
+
case "GET":
|
|
83
|
+
if (id) {
|
|
84
|
+
return routeHandlers.find(request, {
|
|
85
|
+
params: Promise.resolve({ table, id }),
|
|
86
|
+
})
|
|
87
|
+
} else {
|
|
88
|
+
return routeHandlers.list(request, {
|
|
89
|
+
params: Promise.resolve({
|
|
90
|
+
table,
|
|
91
|
+
...Object.fromEntries(request.nextUrl.searchParams.entries()),
|
|
92
|
+
}),
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
case "POST":
|
|
96
|
+
return routeHandlers.create(request, {
|
|
97
|
+
params: Promise.resolve({ table }),
|
|
98
|
+
})
|
|
99
|
+
case "PATCH":
|
|
100
|
+
if (!id) throw new Error("ID required for PATCH")
|
|
101
|
+
return routeHandlers.update(request, {
|
|
102
|
+
params: Promise.resolve({ table, id }),
|
|
103
|
+
})
|
|
104
|
+
case "DELETE":
|
|
105
|
+
if (!id) throw new Error("ID required for DELETE")
|
|
106
|
+
return routeHandlers.remove(request, {
|
|
107
|
+
params: Promise.resolve({ table, id }),
|
|
108
|
+
})
|
|
109
|
+
default:
|
|
110
|
+
return Response.json({ error: "Method not allowed" }, { status: 405 })
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(error)
|
|
114
|
+
return Response.json(
|
|
115
|
+
{ error: error instanceof Error ? error.message : "Unknown error" },
|
|
116
|
+
{ status: 500 }
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
useState,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
createContext,
|
|
8
|
+
useContext,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import { useAction as useAsyncAction, useIndex } from './hooks'
|
|
11
|
+
import { ActionProvider, useAction, useActionProvider } from './action'
|
|
12
|
+
import { TableInterface, ListProps, DatabaseInterface } from './interface'
|
|
13
|
+
import { FieldsProvider, useFields } from './fields'
|
|
14
|
+
|
|
15
|
+
export function reactInterface<
|
|
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 ?? {}) })
|
|
44
|
+
|
|
45
|
+
const array = useMemo(
|
|
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
|
+
>
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...index,
|
|
61
|
+
array,
|
|
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),
|
|
82
|
+
}
|
|
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
|
+
}
|
|
105
|
+
|
|
106
|
+
function TableProvider<Table extends TableKey>({ children, ...props }: TableProviderProps<Table> & {
|
|
107
|
+
children: React.ReactNode | ((props: ReturnType<typeof useTableProvider<Table>>) => React.ReactNode)
|
|
108
|
+
}) {
|
|
109
|
+
const context = useTableProvider(props);
|
|
110
|
+
const TableContext = getTableContext(props.table);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<TableContext.Provider value={context}>
|
|
114
|
+
{typeof children === 'function' ? children(context) : children}
|
|
115
|
+
</TableContext.Provider>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function DatabaseProvider({
|
|
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
|
+
}
|
|
136
|
+
|
|
137
|
+
function useTable<T extends TableKey>(name: T) {
|
|
138
|
+
const TableContext = getTableContext(name);
|
|
139
|
+
const context = useContext(TableContext);
|
|
140
|
+
|
|
141
|
+
if (!context) {
|
|
142
|
+
throw new Error(`useTable("${String(name)}") must be used within a TableProvider or DatabaseProvider with table="${String(name)}"`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return context as ReturnType<typeof useTableProvider<T>>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function useSingleProvider<Table extends TableKey>({
|
|
149
|
+
id,
|
|
150
|
+
table,
|
|
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]])
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
single,
|
|
168
|
+
setSingle,
|
|
169
|
+
loading: find.loading,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const SingleContext = createContext<
|
|
174
|
+
ReturnType<typeof useSingleProvider<any>> | undefined
|
|
175
|
+
>(undefined)
|
|
176
|
+
|
|
177
|
+
function SingleProvider<Table extends TableKey>({
|
|
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
|
+
}
|
|
197
|
+
|
|
198
|
+
const useSingle = <Table extends TableKey>(table: Table) =>
|
|
199
|
+
useContext(SingleContext) as ReturnType<typeof useSingleProvider<Table>>
|
|
200
|
+
|
|
201
|
+
function CreateForm<
|
|
202
|
+
T extends TableKey
|
|
203
|
+
>({
|
|
204
|
+
table,
|
|
205
|
+
defaults,
|
|
206
|
+
onSuccess,
|
|
207
|
+
children,
|
|
208
|
+
}: {
|
|
209
|
+
table: T
|
|
210
|
+
defaults?: Partial<z.infer<Database[T]['writable']>>
|
|
211
|
+
onSuccess?: (result: z.infer<Database[T]['readable']>) => void
|
|
212
|
+
children: React.ReactNode | (
|
|
213
|
+
( props: ReturnType<typeof useActionProvider<z.infer<Database[T]['writable']>, z.infer<Database[T]['readable']>>>
|
|
214
|
+
& ReturnType<typeof useFields<z.infer<Database[T]['writable']>>>
|
|
215
|
+
) => React.ReactNode
|
|
216
|
+
)
|
|
217
|
+
}) {
|
|
218
|
+
type Readable = z.infer<Database[T]['readable']>
|
|
219
|
+
type Writable = z.infer<Database[T]['writable']>
|
|
220
|
+
|
|
221
|
+
const { create } = useTable(table)
|
|
222
|
+
|
|
223
|
+
const callback = useCallback(
|
|
224
|
+
async (fields: Writable) => {
|
|
225
|
+
const result = await create.trigger({ data: fields })
|
|
226
|
+
if (onSuccess) onSuccess(result as Readable)
|
|
227
|
+
return result
|
|
228
|
+
},
|
|
229
|
+
[create, onSuccess]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<FieldsProvider<Writable> defaults={defaults || ({} as Writable)}>
|
|
234
|
+
{fields => (
|
|
235
|
+
<ActionProvider<Writable, Readable> action={callback} params={fields.fields}>
|
|
236
|
+
{typeof children === 'function' ? (
|
|
237
|
+
form => children({ ...form, ...fields })
|
|
238
|
+
) : (
|
|
239
|
+
children
|
|
240
|
+
)}
|
|
241
|
+
</ActionProvider>
|
|
242
|
+
)}
|
|
243
|
+
</FieldsProvider>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function UpdateForm<T extends TableKey>({
|
|
248
|
+
table,
|
|
249
|
+
id,
|
|
250
|
+
defaults,
|
|
251
|
+
onSuccess,
|
|
252
|
+
children,
|
|
253
|
+
}: {
|
|
254
|
+
table: T
|
|
255
|
+
id: string
|
|
256
|
+
defaults?: Partial<z.infer<Database[T]['writable']>>
|
|
257
|
+
onSuccess?: (result: z.infer<Database[T]['readable']>) => void
|
|
258
|
+
children: React.ReactNode | (
|
|
259
|
+
(props: ReturnType<typeof useActionProvider<Partial<z.infer<Database[T]['writable']>>, z.infer<Database[T]['readable']>>>
|
|
260
|
+
& ReturnType<typeof useFields<z.infer<Database[T]['writable']>>>
|
|
261
|
+
) => React.ReactNode
|
|
262
|
+
)
|
|
263
|
+
}) {
|
|
264
|
+
type Readable = z.infer<Database[T]['readable']>
|
|
265
|
+
type Writable = z.infer<Database[T]['writable']>
|
|
266
|
+
|
|
267
|
+
const { update } = useTable(table)
|
|
268
|
+
|
|
269
|
+
const callback = useCallback(
|
|
270
|
+
async (fields: Partial<Writable>) => {
|
|
271
|
+
const result = await update.trigger({ id, data: fields })
|
|
272
|
+
if (onSuccess) onSuccess(result as Readable)
|
|
273
|
+
return result
|
|
274
|
+
},
|
|
275
|
+
[update, id, onSuccess]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<FieldsProvider<Writable>
|
|
280
|
+
defaults={defaults || ({} as Partial<Writable>)}
|
|
281
|
+
>
|
|
282
|
+
{fields => (
|
|
283
|
+
<ActionProvider<Partial<Writable>, Readable> action={callback} params={fields.fields}>
|
|
284
|
+
{typeof children === 'function' ? (
|
|
285
|
+
form => children({ ...form, ...fields })
|
|
286
|
+
) : (
|
|
287
|
+
children
|
|
288
|
+
)}
|
|
289
|
+
</ActionProvider>
|
|
290
|
+
)}
|
|
291
|
+
</FieldsProvider>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function FilterForm<T extends TableKey>({
|
|
296
|
+
table,
|
|
297
|
+
defaults,
|
|
298
|
+
onSuccess,
|
|
299
|
+
children,
|
|
300
|
+
}: {
|
|
301
|
+
table: T
|
|
302
|
+
defaults?: Partial<ListProps<z.infer<Database[T]['readable']>>>
|
|
303
|
+
onSuccess?: (result: z.infer<Database[T]['readable']>[]) => void
|
|
304
|
+
children: React.ReactNode | (
|
|
305
|
+
( props: ReturnType<typeof useActionProvider<ListProps<z.infer<Database[T]['readable']>>, z.infer<Database[T]['readable']>[]>>
|
|
306
|
+
& ReturnType<typeof useFields<ListProps<z.infer<Database[T]['readable']>>>>
|
|
307
|
+
) => React.ReactNode
|
|
308
|
+
)
|
|
309
|
+
}) {
|
|
310
|
+
type Readable = z.infer<Database[T]['readable']>
|
|
311
|
+
type Writable = z.infer<Database[T]['writable']>
|
|
312
|
+
|
|
313
|
+
const { list } = useTable(table)
|
|
314
|
+
|
|
315
|
+
const callback = useCallback(
|
|
316
|
+
async (fields: Omit<ListProps<Readable>, 'table'>) => {
|
|
317
|
+
const result = await list.trigger(fields)
|
|
318
|
+
if (onSuccess) onSuccess(result)
|
|
319
|
+
return result
|
|
320
|
+
},
|
|
321
|
+
[list, onSuccess]
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<FieldsProvider<ListProps<Readable>>
|
|
326
|
+
defaults={(defaults || { query: {} }) as ListProps<Readable>}
|
|
327
|
+
>
|
|
328
|
+
{fields => (
|
|
329
|
+
<ActionProvider<ListProps<Readable>, Readable[]> action={callback} params={fields.fields}>
|
|
330
|
+
{typeof children === 'function' ? (
|
|
331
|
+
form => children({ ...form, ...fields })
|
|
332
|
+
) : (
|
|
333
|
+
children
|
|
334
|
+
)}
|
|
335
|
+
</ActionProvider>
|
|
336
|
+
)}
|
|
337
|
+
</FieldsProvider>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const useCreateForm = <T extends TableKey>(table: T) => {
|
|
342
|
+
return {
|
|
343
|
+
...useFields<z.infer<Database[T]['writable']>>(),
|
|
344
|
+
...useAction<
|
|
345
|
+
z.infer<Database[T]['writable']>,
|
|
346
|
+
z.infer<Database[T]['readable']>
|
|
347
|
+
>(),
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const useUpdateForm = <T extends TableKey>(table: T) => {
|
|
351
|
+
return {
|
|
352
|
+
...useFields<Partial<z.infer<Database[T]['writable']>>>(),
|
|
353
|
+
...useAction<
|
|
354
|
+
Partial<z.infer<Database[T]['writable']>>,
|
|
355
|
+
z.infer<Database[T]['readable']>
|
|
356
|
+
>(),
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const useFiltersForm = <T extends TableKey>(table: T) => {
|
|
360
|
+
return {
|
|
361
|
+
...useFields<z.infer<Database[T]['readable']>>(),
|
|
362
|
+
...useAction<z.infer<Database[T]['readable']>,
|
|
363
|
+
z.infer<Database[T]['readable']>[]
|
|
364
|
+
>(),
|
|
365
|
+
}
|
|
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
|
+
}
|
|
383
|
+
|
|
384
|
+
export class ReactInterface<Database extends DatabaseInterface> {
|
|
385
|
+
constructor(
|
|
386
|
+
database: Database,
|
|
387
|
+
tableInterface: TableInterface<
|
|
388
|
+
z.infer<Database[keyof Database]['readable']>,
|
|
389
|
+
z.infer<Database[keyof Database]['writable']>
|
|
390
|
+
>
|
|
391
|
+
) {
|
|
392
|
+
return reactInterface(database, tableInterface)
|
|
393
|
+
}
|
|
394
|
+
}
|