asasvirtuais 3.0.0 → 4.0.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 CHANGED
@@ -1,181 +1,495 @@
1
- # asasvirtuais
2
-
3
- React form and action management utilities for building data-driven applications.
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@3.0.0/forms'
15
- import { useFields } from 'https://esm.sh/asasvirtuais@3.0.0/fields'
16
- import { useAction } from 'https://esm.sh/asasvirtuais@3.0.0/action'
17
- ```
18
-
19
- ---
20
-
21
- ## Quick Start: Forms
22
-
23
- ### Simple Form
24
-
25
- ```tsx
26
- import { Form } from 'asasvirtuais/forms'
27
-
28
- type LoginFields = {
29
- email: string
30
- password: string
31
- }
32
-
33
- type LoginResult = {
34
- token: string
35
- }
36
-
37
- async function loginAction(fields: LoginFields): Promise<LoginResult> {
38
- const response = await fetch('/api/login', {
39
- method: 'POST',
40
- body: JSON.stringify(fields)
41
- })
42
- return response.json()
43
- }
44
-
45
- function LoginForm() {
46
- return (
47
- <Form<LoginFields, LoginResult>
48
- defaults={{ email: '', password: '' }}
49
- action={loginAction}
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
- ---
75
-
76
- ## Core Modules
77
-
78
- ### 1. `asasvirtuais/forms`
79
- Self-contained form nodes that manage state and actions. Nest forms to create complex workflows.
80
-
81
- ### 2. `asasvirtuais/interface`
82
- Components and hooks for data-driven React apps.
83
-
84
- #### Sub-modules:
85
- - `asasvirtuais/interface`: Main React CRUD components.
86
- - `asasvirtuais/interface/indexed`: IndexedDB storage via Dexie.
87
- - `asasvirtuais/fetch-interface`: REST API adapter.
88
- - `asasvirtuais/yaml-interface`: Local flat-file adapter.
89
-
90
- ---
91
-
92
- ## Todo App Example
93
-
94
- #### 1. Define Schema
95
-
96
- ```typescript
97
- import { z } from 'zod';
98
-
99
- export const todoSchema = {
100
- readable: z.object({
101
- id: z.string(),
102
- text: z.string(),
103
- completed: z.boolean(),
104
- }),
105
- writable: z.object({
106
- text: z.string(),
107
- completed: z.boolean().optional(),
108
- }),
109
- }
110
- ```
111
-
112
- #### 2. Provide Context
113
-
114
- ```tsx
115
- import { FetchInterfaceProvider } from 'asasvirtuais/fetch-interface'
116
- import { TableProvider } from 'asasvirtuais/interface'
117
- import { todoSchema } from './database'
118
-
119
- export default function RootLayout({ children }) {
120
- return (
121
- <FetchInterfaceProvider schema={todoSchema} baseUrl='/api/v1'>
122
- <TableProvider table='todos' schema={todoSchema} interface={useInterface()}>
123
- {children}
124
- </TableProvider>
125
- </FetchInterfaceProvider>
126
- )
127
- }
128
- ```
129
-
130
- #### 3. Build UI
131
-
132
- ```tsx
133
- 'use client'
134
- import { useTable, CreateForm } from 'asasvirtuais/interface'
135
- import { todoSchema } from '@/app/database'
136
-
137
- function TodoList() {
138
- const { array, remove } = useTable('todos', todoSchema)
139
-
140
- return (
141
- <>
142
- <CreateForm table="todos" schema={todoSchema} defaults={{ text: '' }}>
143
- {({ fields, setField, submit }) => (
144
- <form onSubmit={submit}>
145
- <input
146
- value={fields.text}
147
- onChange={(e) => setField('text', e.target.value)}
148
- />
149
- <button type="submit">Add Todo</button>
150
- </form>
151
- )}
152
- </CreateForm>
153
-
154
- <ul>
155
- {array.map(todo => (
156
- <li key={todo.id}>
157
- {todo.text}
158
- <button onClick={() => remove.trigger({ id: todo.id })}>Delete</button>
159
- </li>
160
- ))}
161
- </ul>
162
- </>
163
- )
164
- }
165
- ```
166
-
167
- ---
168
-
169
- ## Model Package Path
170
-
171
- A model package is a self-contained module for a specific data model.
172
-
173
- ### Structure
174
- ```
175
- packages/[model-name]/
176
- ├── index.ts # Schema + types
177
- ├── fields.tsx # Form fields
178
- ├── forms.tsx # CRUD forms
179
- ├── provider.tsx # Context + hooks
180
- └── components.tsx # Display components
181
- ```
1
+ # asasvirtuais
2
+
3
+ A React framework for building full-stack apps where code is organized by feature, not by layer.
4
+
5
+ ---
6
+
7
+ ## Primitives
8
+
9
+ Three building blocks, each usable on its own.
10
+
11
+ ### `FieldsProvider` — field state
12
+
13
+ ```tsx
14
+ import { FieldsProvider } from 'asasvirtuais/fields'
15
+
16
+ <FieldsProvider defaults={{ title: '', done: false }}>
17
+ {({ fields, setField }) => (
18
+ <div>
19
+ <input value={fields.title} onChange={e => setField('title', e.target.value)} />
20
+ <input type="checkbox" checked={fields.done} onChange={e => setField('done', e.target.checked)} />
21
+ </div>
22
+ )}
23
+ </FieldsProvider>
24
+ ```
25
+
26
+ ### `ActionProvider` async action state
27
+
28
+ ```tsx
29
+ import { ActionProvider } from 'asasvirtuais/action'
30
+
31
+ <ActionProvider params={{ id: todo.id }} action={archiveTodo} onResult={() => router.push('/')}>
32
+ {({ submit, loading, error }) => (
33
+ <button onClick={submit} disabled={loading}>
34
+ {loading ? 'Archiving...' : 'Archive'}
35
+ </button>
36
+ )}
37
+ </ActionProvider>
38
+ ```
39
+
40
+ ### `Form` — fields + action together
41
+
42
+ ```tsx
43
+ import { Form } from 'asasvirtuais/form'
44
+
45
+ <Form defaults={{ email: '', password: '' }} action={login} onResult={handleResult}>
46
+ {({ fields, setField, submit, loading, error }) => (
47
+ <form onSubmit={submit}>
48
+ <input value={fields.email} onChange={e => setField('email', e.target.value)} />
49
+ <input type="password" value={fields.password} onChange={e => setField('password', e.target.value)} />
50
+ <button type="submit" disabled={loading}>Login</button>
51
+ {error && <p>{error.message}</p>}
52
+ </form>
53
+ )}
54
+ </Form>
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Full-stack CRUD
60
+
61
+ The framework provides a schema-first CRUD layer where create, update, and remove operations automatically keep the UI in sync through a reactive index — no manual state updates, no refetching.
62
+
63
+ ### Project structure
64
+
65
+ ```
66
+ app/
67
+ ├── schema.ts # All table schemas in one place
68
+ ├── actions.ts # Server actions — the backend
69
+ ├── providers.tsx # App-level providers
70
+ ├── layout.tsx
71
+ ├── todos/
72
+ │ ├── schema.ts # Schema + types
73
+ │ ├── fields.tsx # Input components
74
+ │ ├── forms.tsx # Create / Update / Delete / Filter forms
75
+ │ ├── components.tsx # Display components
76
+ │ └── provider.tsx # TableProvider + hook
77
+ ```
78
+
79
+ ---
80
+
81
+ ### 1. Schema
82
+
83
+ Each model defines `readable` (what comes out of the database) and `writable` (what users can create or modify):
84
+
85
+ ```ts
86
+ // app/todos/schema.ts
87
+ import z from 'zod'
88
+
89
+ export const readable = z.object({
90
+ id: z.string(),
91
+ title: z.string(),
92
+ done: z.boolean(),
93
+ author: z.string(),
94
+ createdAt: z.string(),
95
+ })
96
+
97
+ export const writable = readable.pick({
98
+ title: true,
99
+ done: true,
100
+ })
101
+
102
+ export const schema = { readable, writable }
103
+
104
+ export type Readable = z.infer<typeof readable>
105
+ export type Writable = z.infer<typeof writable>
106
+ ```
107
+
108
+ All models are assembled into a single database schema file:
109
+
110
+ ```ts
111
+ // app/schema.ts
112
+ import { schema as todosSchema } from './todos/schema'
113
+ import { schema as tagsSchema } from './tags/schema'
114
+
115
+ export const schema = {
116
+ todos: todosSchema,
117
+ tags: tagsSchema,
118
+ }
119
+ ```
120
+
121
+ ---
122
+
123
+ ### 2. Server actions — the backend
124
+
125
+ The backend is plain Next.js server actions. You pass them directly to the provider — no REST routes, no fetch client needed:
126
+
127
+ ```ts
128
+ // app/actions.ts
129
+ 'use server'
130
+
131
+ import { firestoreInterface } from 'asasvirtuais-firebase/interface'
132
+ import { auth0 } from '@/lib/auth0'
133
+
134
+ const db = firestoreInterface()
135
+
136
+ function clean(obj: any): any {
137
+ if (Array.isArray(obj)) return obj.map(clean)
138
+ if (obj !== null && typeof obj === 'object' && obj.constructor === Object) {
139
+ return Object.fromEntries(
140
+ Object.entries(obj)
141
+ .filter(([_, v]) => v !== undefined)
142
+ .map(([k, v]) => [k, clean(v)])
143
+ )
144
+ }
145
+ return obj
146
+ }
147
+
148
+ export const find = async (props: any) => db.find(props)
149
+
150
+ export const list = async (props: any) => db.list(props)
151
+
152
+ export const create = async (props: any) => {
153
+ const session = await auth0.getSession()
154
+ if (!session?.user) throw new Error('Unauthorized')
155
+
156
+ const data = clean({ ...props.data })
157
+
158
+ if (props.table === 'todos') {
159
+ data.author = session.user.id
160
+ data.done = false
161
+ data.createdAt = new Date().toISOString()
162
+ }
163
+
164
+ return db.create({ ...props, data })
165
+ }
166
+
167
+ export const update = async (props: any) => {
168
+ const session = await auth0.getSession()
169
+ if (!session?.user) throw new Error('Unauthorized')
170
+
171
+ const data = clean({ ...props.data })
172
+
173
+ if (props.table === 'todos') {
174
+ const existing = await db.find({ table: 'todos', id: props.id })
175
+ if (existing.author !== session.user.id) throw new Error('Forbidden')
176
+ }
177
+
178
+ return db.update({ ...props, data })
179
+ }
180
+
181
+ export const remove = async (props: any) => {
182
+ const session = await auth0.getSession()
183
+ if (!session?.user) throw new Error('Unauthorized')
184
+
185
+ if (props.table === 'todos') {
186
+ const existing = await db.find({ table: 'todos', id: props.id })
187
+ if (existing.author !== session.user.id) throw new Error('Forbidden')
188
+ }
189
+
190
+ return db.remove(props)
191
+ }
192
+ ```
193
+
194
+ This is where business logic lives: auth, default values, permission checks. All in one place, all readable top to bottom.
195
+
196
+ ---
197
+
198
+ ### 3. Providers
199
+
200
+ ```tsx
201
+ // app/providers.tsx
202
+ import { InterfaceProvider } from 'asasvirtuais/interface-provider'
203
+ import { DatabaseProvider } from 'asasvirtuais/react-interface'
204
+ import { TodosProvider } from '@/app/todos/provider'
205
+ import * as db from '@/app/actions'
206
+
207
+ export default function AppProviders({ children }: { children: React.ReactNode }) {
208
+ return (
209
+ <InterfaceProvider
210
+ find={db.find}
211
+ list={db.list}
212
+ create={db.create}
213
+ update={db.update}
214
+ remove={db.remove}
215
+ >
216
+ <DatabaseProvider>
217
+ <TodosProvider>
218
+ {children}
219
+ </TodosProvider>
220
+ </DatabaseProvider>
221
+ </InterfaceProvider>
222
+ )
223
+ }
224
+ ```
225
+
226
+ ```tsx
227
+ // app/layout.tsx
228
+ import AppProviders from './providers'
229
+
230
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
231
+ return (
232
+ <html lang="en">
233
+ <body>
234
+ <AppProviders>
235
+ {children}
236
+ </AppProviders>
237
+ </body>
238
+ </html>
239
+ )
240
+ }
241
+ ```
242
+
243
+ ---
244
+
245
+ ### 4. Model provider
246
+
247
+ ```tsx
248
+ // app/todos/provider.tsx
249
+ 'use client'
250
+ import { TableProvider, useTable } from 'asasvirtuais/react-interface'
251
+ import { useInterface } from 'asasvirtuais/interface-provider'
252
+ import { schema } from '.'
253
+
254
+ export function useTodos() {
255
+ return useTable('todos', schema)
256
+ }
257
+
258
+ export function TodosProvider({ children }: { children: React.ReactNode }) {
259
+ return (
260
+ <TableProvider table="todos" schema={schema} interface={useInterface()}>
261
+ {children}
262
+ </TableProvider>
263
+ )
264
+ }
265
+ ```
266
+
267
+ ---
268
+
269
+ ### 5. UI
270
+
271
+ ```tsx
272
+ // app/todos/page.tsx
273
+ 'use client'
274
+ import { useEffect } from 'react'
275
+ import { useTodos } from './provider'
276
+ import { SingleProvider } from 'asasvirtuais/react-interface'
277
+ import { schema } from '.'
278
+ import { TodoItem } from './components'
279
+ import { CreateTodo } from './forms'
280
+
281
+ export default function TodosPage() {
282
+ const { array, list } = useTodos()
283
+
284
+ useEffect(() => { list.trigger({}) }, [])
285
+
286
+ return (
287
+ <div>
288
+ <CreateTodo />
289
+ {array.map(todo => (
290
+ <SingleProvider key={todo.id} id={todo.id} table="todos" schema={schema}>
291
+ <TodoItem />
292
+ </SingleProvider>
293
+ ))}
294
+ </div>
295
+ )
296
+ }
297
+ ```
298
+
299
+ When `create` resolves, the item appears in `array` immediately. Same for `update` and `remove`.
300
+
301
+ ---
302
+
303
+ ## Listing vs. filtering
304
+
305
+ ### `useTable().list` — reactive, global
306
+
307
+ Use this when you want all records in the reactive index. Results live in `array` and stay in sync with every create, update, and remove automatically:
308
+
309
+ ```tsx
310
+ const { array, list } = useTodos()
311
+
312
+ useEffect(() => { list.trigger({}) }, [])
313
+
314
+ // array updates automatically when any todo is created, updated, or removed
315
+ return array.map(todo => (
316
+ <SingleProvider key={todo.id} id={todo.id} table="todos" schema={schema}>
317
+ <TodoItem />
318
+ </SingleProvider>
319
+ ))
320
+ ```
321
+
322
+ ### `FilterForm` — local, paginated, or conditional
323
+
324
+ Use `FilterForm` when you need pagination, live search, or results that belong to the component rather than the global index. Results live in `form.result` and only update when `submit` is called:
325
+
326
+ ```tsx
327
+ import { FilterForm } from 'asasvirtuais/form'
328
+ import { schema } from '.'
329
+
330
+ <FilterForm table="todos" schema={schema} defaults={{ query: { done: false } }} autoTrigger>
331
+ {({ result, loading, fields, setField, submit }) => (
332
+ <div>
333
+ <input
334
+ placeholder="Search..."
335
+ value={fields.query?.title ?? ''}
336
+ onChange={e => {
337
+ setField('query', { title: e.target.value })
338
+ submit()
339
+ }}
340
+ />
341
+ {loading && <p>Loading...</p>}
342
+ {result?.map(todo => <p key={todo.id}>{todo.title}</p>)}
343
+ </div>
344
+ )}
345
+ </FilterForm>
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Async selector fields
351
+
352
+ When a form needs the user to pick a record from another table, `FilterForm` composes naturally inside a field component. The field reads and writes to the parent form's context via `useFields()` — no props needed to bridge them.
353
+
354
+ Say a todo can be tagged, and the user needs to search and select a tag while creating the todo:
355
+
356
+ ```tsx
357
+ // app/todos/fields.tsx
358
+ import { useFields } from 'asasvirtuais/fields'
359
+ import { FilterForm } from 'asasvirtuais/form'
360
+ import { schema as tagsSchema } from '@/app/tags'
361
+
362
+ export function TagSelectorField() {
363
+ // reads/writes to whatever Form or FieldsProvider this is rendered inside
364
+ const { fields, setField } = useFields<{ tagId: string }>()
365
+
366
+ return (
367
+ <FilterForm table="tags" schema={tagsSchema} defaults={{ query: {} }}>
368
+ {({ fields: search, setField: setSearch, submit, result }) => (
369
+ <div>
370
+ <input
371
+ placeholder="Search tags..."
372
+ onChange={e => {
373
+ setSearch('query', { name: e.target.value })
374
+ submit()
375
+ }}
376
+ />
377
+ <ul>
378
+ {result?.map(tag => (
379
+ <li
380
+ key={tag.id}
381
+ onClick={() => setField('tagId', tag.id)}
382
+ style={{ fontWeight: fields.tagId === tag.id ? 'bold' : 'normal' }}
383
+ >
384
+ {tag.name}
385
+ </li>
386
+ ))}
387
+ </ul>
388
+ </div>
389
+ )}
390
+ </FilterForm>
391
+ )
392
+ }
393
+ ```
394
+
395
+ Use it inside any form — it just works:
396
+
397
+ ```tsx
398
+ // app/todos/forms.tsx
399
+ import { CreateForm } from 'asasvirtuais/form'
400
+ import { schema } from '.'
401
+ import { TitleField, TagSelectorField } from './fields'
402
+
403
+ export function CreateTodo({ onSuccess }: { onSuccess?: () => void }) {
404
+ return (
405
+ <CreateForm table="todos" schema={schema} defaults={{ title: '', tagId: '' }} onSuccess={onSuccess}>
406
+ {({ submit, loading }) => (
407
+ <div>
408
+ <TitleField />
409
+ <TagSelectorField />
410
+ <button onClick={submit} disabled={loading}>
411
+ {loading ? 'Creating...' : 'Create Todo'}
412
+ </button>
413
+ </div>
414
+ )}
415
+ </CreateForm>
416
+ )
417
+ }
418
+ ```
419
+
420
+ The `FilterForm` queries the `tags` table asynchronously. The `CreateForm` owns the selected `tagId`. Neither knows about the other.
421
+
422
+ ---
423
+
424
+ ## The single record pattern
425
+
426
+ `SingleProvider` makes a record available to all its descendants without prop drilling. When multiple components share one record, wrap them all in one provider:
427
+
428
+ ```tsx
429
+ import { SingleProvider, useSingle } from 'asasvirtuais/react-interface'
430
+
431
+ // Detail page
432
+ <SingleProvider id={params.id} table="todos" schema={schema}>
433
+ <TodoDetail />
434
+ <UpdateTodoForm />
435
+ <DeleteTodoButton />
436
+ </SingleProvider>
437
+
438
+ // Inside any of those:
439
+ function TodoDetail() {
440
+ const { single } = useSingle(schema, 'todos')
441
+ return <h1>{single.title}</h1>
442
+ }
443
+ ```
444
+
445
+ If the record isn't in the reactive index yet, `SingleProvider` fetches it automatically.
446
+
447
+ ---
448
+
449
+ ## Effects
450
+
451
+ There is no middleware or lifecycle configuration. Effects are code written around the action:
452
+
453
+ ```tsx
454
+ // Before submit
455
+ <button onClick={() => {
456
+ validateForm(form.fields)
457
+ form.submit()
458
+ }}>
459
+ Save
460
+ </button>
461
+
462
+ // After success
463
+ <CreateForm
464
+ table="todos"
465
+ schema={schema}
466
+ onSuccess={todo => {
467
+ router.push(`/todos/${todo.id}`)
468
+ showNotification('Todo created!')
469
+ }}
470
+ >
471
+ {/* ... */}
472
+ </CreateForm>
473
+
474
+ // Using field values without submitting
475
+ <button onClick={() => saveDraftLocally(form.fields)}>
476
+ Save Draft
477
+ </button>
478
+ ```
479
+
480
+ ---
481
+
482
+ ## Naming pattern examples
483
+
484
+ | Concept | Pattern | Example |
485
+ |---|---|---|
486
+ | Table name | lowercase plural | `'todos'` |
487
+ | Schema types | `Readable`, `Writable` | `type Readable = z.infer<...>` |
488
+ | Field components | `{Field}Field` | `TitleField`, `DoneField` |
489
+ | Provider | `{Model}sProvider` | `TodosProvider` |
490
+ | Hook | `use{Model}s()` | `useTodos()` |
491
+ | Create form | `Create{Model}` | `CreateTodo` |
492
+ | Update form | `Update{Model}` | `UpdateTodo` |
493
+ | Delete action | `Delete{Model}` | `DeleteTodo` |
494
+ | Item component | `{Model}Item` | `TodoItem` |
495
+ | Detail component | `Single{Model}` | `SingleTodo` |