asasvirtuais 2.5.1 → 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,894 +1,495 @@
1
1
  # asasvirtuais
2
2
 
3
- **A React framework for building maintainable web applications without the architectural debt.**
3
+ A React framework for building full-stack apps where code is organized by feature, not by layer.
4
4
 
5
- After 7 years of wrestling with complex tech stacks, I built asasvirtuais to solve a problem nobody seems to talk about: the elephant under the carpet of modern web development. Every framework gives you components and state management, but none of them solve the fundamental challenge every project faces—connecting CRUD APIs to UI forms with clean, maintainable state management.
6
-
7
- This isn't about fancy animations or advanced performance optimization. This is about making codebases simple enough that you (or an AI) can focus on business logic instead of wrestling with architectural patterns.
5
+ ---
8
6
 
9
- ## The Problem
7
+ ## Primitives
10
8
 
11
- Software development has convinced itself that complexity is inevitable. We've been taught that proper applications require:
9
+ Three building blocks, each usable on its own.
12
10
 
13
- - State scattered across dozens of files
14
- - Design patterns that make simple things complicated
15
- - Dependencies injected through layers of abstraction
16
- - Code that's impossible to reason about without opening 10 files
11
+ ### `FieldsProvider` field state
17
12
 
18
- But here's the thing: **complexity exists, but overengineering is a human tendency, not a technical requirement.**
13
+ ```tsx
14
+ import { FieldsProvider } from 'asasvirtuais/fields'
19
15
 
20
- ## The Solution
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
+ ```
21
25
 
22
- asasvirtuais is built on a simple foundation: **React + RESTful APIs**. No magic, no over-abstraction. Just a library that makes the right architectural decisions obvious.
26
+ ### `ActionProvider` async action state
23
27
 
24
- The core insight: **developers and AI shouldn't need to think about state management—just focus on business logic.**
28
+ ```tsx
29
+ import { ActionProvider } from 'asasvirtuais/action'
25
30
 
26
- ### What Makes This Different
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
+ ```
27
39
 
28
- 1. **Nested forms that actually make sense** - Build multi-step async validation workflows without the pain
29
- 2. **CRUD operations as a solved problem** - Filter, create, update with zero boilerplate
30
- 3. **Code in one place** - Business logic lives in readable, single files, not scattered across a dependency tree
31
- 4. **AI-friendly patterns** - Simple enough that AI can generate complex forms correctly on the first try
40
+ ### `Form` fields + action together
32
41
 
33
- ## Installation
42
+ ```tsx
43
+ import { Form } from 'asasvirtuais/form'
34
44
 
35
- ### From npm
36
- ```bash
37
- npm install asasvirtuais
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>
38
55
  ```
39
56
 
40
- ### From esm.sh
41
- ```typescript
42
- import { Form } from 'https://esm.sh/asasvirtuais@latest/form'
43
- import { useFields } from 'https://esm.sh/asasvirtuais@latest/fields'
44
- import { useAction } from 'https://esm.sh/asasvirtuais@latest/action'
45
- ```
57
+ ---
46
58
 
47
- ## Quick Start
59
+ ## Full-stack CRUD
48
60
 
49
- ### Simple Form
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.
50
62
 
51
- ```tsx
52
- import { Form } from 'asasvirtuais/form'
63
+ ### Project structure
53
64
 
54
- type LoginFields = {
55
- email: string
56
- password: string
57
- }
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
+ ```
58
78
 
59
- type LoginResult = {
60
- token: string
61
- user: { id: string; name: string }
62
- }
79
+ ---
63
80
 
64
- async function loginAction(fields: LoginFields): Promise<LoginResult> {
65
- const response = await fetch('/api/login', {
66
- method: 'POST',
67
- body: JSON.stringify(fields)
68
- })
69
- return response.json()
70
- }
81
+ ### 1. Schema
71
82
 
72
- function LoginForm() {
73
- return (
74
- <Form<LoginFields, LoginResult>
75
- defaults={{ email: '', password: '' }}
76
- action={loginAction}
77
- onResult={(result) => console.log('Logged in:', result.user.name)}
78
- >
79
- {({ fields, setField, submit, loading, error }) => (
80
- <form onSubmit={submit}>
81
- <input
82
- type="email"
83
- value={fields.email}
84
- onChange={(e) => setField('email', e.target.value)}
85
- />
86
- <input
87
- type="password"
88
- value={fields.password}
89
- onChange={(e) => setField('password', e.target.value)}
90
- />
91
- <button type="submit" disabled={loading}>
92
- {loading ? 'Logging in...' : 'Login'}
93
- </button>
94
- {error && <p>Error: {error.message}</p>}
95
- </form>
96
- )}
97
- </Form>
98
- )
99
- }
100
- ```
83
+ Each model defines `readable` (what comes out of the database) and `writable` (what users can create or modify):
101
84
 
102
- ## Core Concepts
85
+ ```ts
86
+ // app/todos/schema.ts
87
+ import z from 'zod'
103
88
 
104
- ### 1. Forms: The N8N for React
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
+ })
105
96
 
106
- Think of forms like nodes in a visual workflow builder. Each form is self-contained, knows its state, and can trigger actions. Nest them to create complex workflows without state management headaches.
97
+ export const writable = readable.pick({
98
+ title: true,
99
+ done: true,
100
+ })
107
101
 
108
- ```tsx
109
- // Multi-step form with async validation between steps
110
- <Form<EmailFields, EmailResult>
111
- defaults={{ email: '' }}
112
- action={checkEmail}
113
- >
114
- {(emailForm) => (
115
- <div>
116
- <input
117
- value={emailForm.fields.email}
118
- onChange={(e) => emailForm.setField('email', e.target.value)}
119
- />
120
- <button onClick={emailForm.submit}>Next</button>
121
-
122
- {emailForm.result?.exists && (
123
- <Form<PasswordFields, PasswordResult>
124
- defaults={{ userId: emailForm.result.userId, password: '' }}
125
- action={verifyPassword}
126
- >
127
- {(passwordForm) => (
128
- <input
129
- type="password"
130
- value={passwordForm.fields.password}
131
- onChange={(e) => passwordForm.setField('password', e.target.value)}
132
- />
133
- )}
134
- </Form>
135
- )}
136
- </div>
137
- )}
138
- </Form>
139
- ```
102
+ export const schema = { readable, writable }
140
103
 
141
- ### 2. Fields: State Without the Ceremony
104
+ export type Readable = z.infer<typeof readable>
105
+ export type Writable = z.infer<typeof writable>
106
+ ```
142
107
 
143
- Need just state management? Use `FieldsProvider`:
108
+ All models are assembled into a single database schema file:
144
109
 
145
- ```tsx
146
- import { FieldsProvider, useFields } from 'asasvirtuais/fields'
110
+ ```ts
111
+ // app/schema.ts
112
+ import { schema as todosSchema } from './todos/schema'
113
+ import { schema as tagsSchema } from './tags/schema'
147
114
 
148
- function ProfileEditor() {
149
- return (
150
- <FieldsProvider<ProfileFields> defaults={{ name: '', bio: '' }}>
151
- {({ fields, setField }) => (
152
- <div>
153
- <input
154
- value={fields.name}
155
- onChange={(e) => setField('name', e.target.value)}
156
- />
157
- <textarea
158
- value={fields.bio}
159
- onChange={(e) => setField('bio', e.target.value)}
160
- />
161
- </div>
162
- )}
163
- </FieldsProvider>
164
- )
115
+ export const schema = {
116
+ todos: todosSchema,
117
+ tags: tagsSchema,
165
118
  }
166
119
  ```
167
120
 
168
- ### 3. Actions: Async Operations Made Simple
121
+ ---
169
122
 
170
- Need just action handling? Use `ActionProvider`:
123
+ ### 2. Server actions the backend
171
124
 
172
- ```tsx
173
- import { ActionProvider } from 'asasvirtuais/action'
125
+ The backend is plain Next.js server actions. You pass them directly to the provider — no REST routes, no fetch client needed:
174
126
 
175
- function DeleteButton({ userId }: { userId: string }) {
176
- return (
177
- <ActionProvider
178
- params={{ userId }}
179
- action={deleteAccount}
180
- onResult={() => alert('Account deleted')}
181
- >
182
- {({ submit, loading }) => (
183
- <button onClick={submit} disabled={loading}>
184
- {loading ? 'Deleting...' : 'Delete Account'}
185
- </button>
186
- )}
187
- </ActionProvider>
188
- )
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
189
146
  }
190
- ```
191
147
 
192
- ## React Interface: Data-Driven Applications
148
+ export const find = async (props: any) => db.find(props)
193
149
 
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.
150
+ export const list = async (props: any) => db.list(props)
195
151
 
196
- ### Complete Todo App Example
152
+ export const create = async (props: any) => {
153
+ const session = await auth0.getSession()
154
+ if (!session?.user) throw new Error('Unauthorized')
197
155
 
198
- #### 1. Define Your Schema
156
+ const data = clean({ ...props.data })
199
157
 
200
- ```typescript
201
- // app/database.ts
202
- import { z } from 'zod';
158
+ if (props.table === 'todos') {
159
+ data.author = session.user.id
160
+ data.done = false
161
+ data.createdAt = new Date().toISOString()
162
+ }
203
163
 
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
- }),
164
+ return db.create({ ...props, data })
215
165
  }
216
166
 
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
- }),
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)
228
191
  }
229
192
  ```
230
193
 
231
- #### 2. Provide the Interface Context
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
232
199
 
233
200
  ```tsx
234
- // app/layout.tsx
235
- import { InterfaceProvider } from 'asasvirtuais/fetch-interface'
236
- import { DatabaseProvider, TableProvider } from 'asasvirtuais/react-interface'
237
- import { todoSchema } from './database'
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'
238
206
 
239
- export default function RootLayout({ children }) {
207
+ export default function AppProviders({ children }: { children: React.ReactNode }) {
240
208
  return (
241
- <DatabaseProvider>
242
- <InterfaceProvider schema={todoSchema} baseUrl='/api/v1'>
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>
243
217
  <TodosProvider>
244
218
  {children}
245
219
  </TodosProvider>
246
- </InterfaceProvider>
247
- </DatabaseProvider>
220
+ </DatabaseProvider>
221
+ </InterfaceProvider>
248
222
  )
249
223
  }
250
224
  ```
251
225
 
252
- #### 3. Create Your Table Provider
253
-
254
226
  ```tsx
255
- // app/todos/provider.tsx
256
- 'use client'
257
- import { TableProvider } from 'asasvirtuais/react-interface'
258
- import { useInterface } from 'asasvirtuais/fetch-interface'
259
- import { todoSchema } from './database'
227
+ // app/layout.tsx
228
+ import AppProviders from './providers'
260
229
 
261
- export function TodosProvider({ children }) {
230
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
262
231
  return (
263
- <TableProvider
264
- table='todos'
265
- schema={todoSchema}
266
- interface={useInterface()}
267
- >
268
- {children}
269
- </TableProvider>
232
+ <html lang="en">
233
+ <body>
234
+ <AppProviders>
235
+ {children}
236
+ </AppProviders>
237
+ </body>
238
+ </html>
270
239
  )
271
240
  }
272
241
  ```
273
242
 
274
- #### 4. Build Your UI
243
+ ---
244
+
245
+ ### 4. Model provider
275
246
 
276
247
  ```tsx
277
- // app/todos/page.tsx
248
+ // app/todos/provider.tsx
278
249
  'use client'
279
- import { useTable, CreateForm } from '@asasvirtuais/react-interface'
280
- import { todoSchema } from '@/app/database'
250
+ import { TableProvider, useTable } from 'asasvirtuais/react-interface'
251
+ import { useInterface } from 'asasvirtuais/interface-provider'
252
+ import { schema } from '.'
281
253
 
282
- function TodoList() {
283
- const { index, remove, update } = useTable('todos', todoSchema)
284
- const todos = Object.values(index.index)
254
+ export function useTodos() {
255
+ return useTable('todos', schema)
256
+ }
285
257
 
258
+ export function TodosProvider({ children }: { children: React.ReactNode }) {
286
259
  return (
287
- <>
288
- <CreateForm
289
- table="todos"
290
- schema={todoSchema}
291
- defaults={{ text: '' }}
292
- >
293
- {({ fields, setField, submit, loading }) => (
294
- <form onSubmit={submit}>
295
- <input
296
- value={fields.text}
297
- onChange={(e) => setField('text', e.target.value)}
298
- placeholder="What needs to be done?"
299
- />
300
- <button type="submit" disabled={loading}>
301
- {loading ? 'Adding...' : 'Add Todo'}
302
- </button>
303
- </form>
304
- )}
305
- </CreateForm>
306
-
307
- <ul>
308
- {todos.map(todo => (
309
- <li key={todo.id}>
310
- <span
311
- style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
312
- onClick={() => update.trigger({
313
- id: todo.id,
314
- data: { completed: !todo.completed }
315
- })}
316
- >
317
- {todo.text}
318
- </span>
319
- <button onClick={() => remove.trigger({ id: todo.id })}>
320
- Delete
321
- </button>
322
- </li>
323
- ))}
324
- </ul>
325
- </>
260
+ <TableProvider table="todos" schema={schema} interface={useInterface()}>
261
+ {children}
262
+ </TableProvider>
326
263
  )
327
264
  }
328
265
  ```
329
266
 
330
- #### 5. Multiple Tables with DatabaseProvider
267
+ ---
331
268
 
332
- For apps with multiple tables, wrap them all in a DatabaseProvider:
269
+ ### 5. UI
333
270
 
334
271
  ```tsx
335
- // app/layout.tsx
336
- import { DatabaseProvider, TableProvider } from '@asasvirtuais/react-interface'
337
- import { todoSchema, userSchema } from './database'
338
- import { todosInterface, usersInterface } from './interface'
339
-
340
- export default async function RootLayout({ children }) {
341
- const [initialTodos, initialUsers] = await Promise.all([
342
- fetchTodos(),
343
- fetchUsers()
344
- ])
345
-
346
- return (
347
- <DatabaseProvider>
348
- <TableProvider table="todos" schema={todoSchema} interface={todosInterface} asAbove={initialTodos}>
349
- <TableProvider table="users" schema={userSchema} interface={usersInterface} asAbove={initialUsers}>
350
- {children}
351
- </TableProvider>
352
- </TableProvider>
353
- </DatabaseProvider>
354
- )
355
- }
356
-
357
- // Now any component can access tables:
358
- function MyComponent() {
359
- const todos = useTable('todos', todoSchema)
360
- const users = useTable('users', userSchema)
361
- // ...
362
- }
363
- ```
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'
364
280
 
365
- ## Advanced Examples
281
+ export default function TodosPage() {
282
+ const { array, list } = useTodos()
366
283
 
367
- ### Multi-Step Address Validation
284
+ useEffect(() => { list.trigger({}) }, [])
368
285
 
369
- ```tsx
370
- // Complete checkout flow with async address lookup
371
- <Form<AddressLookupFields, AddressLookupResult>
372
- defaults={{ zipCode: '' }}
373
- action={lookupAddress}
374
- >
375
- {(zipForm) => (
286
+ return (
376
287
  <div>
377
- <h3>Enter ZIP Code</h3>
378
- <input
379
- value={zipForm.fields.zipCode}
380
- onChange={(e) => zipForm.setField('zipCode', e.target.value)}
381
- />
382
- <button onClick={zipForm.submit}>Lookup Address</button>
383
-
384
- {zipForm.result && (
385
- <Form<FullAddressFields, OrderResult>
386
- defaults={{
387
- zipCode: zipForm.fields.zipCode,
388
- city: zipForm.result.city,
389
- state: zipForm.result.state,
390
- country: zipForm.result.country,
391
- street: '',
392
- number: ''
393
- }}
394
- action={createOrder}
395
- onResult={(result) => alert(`Order created: ${result.orderId}`)}
396
- >
397
- {(addressForm) => (
398
- <div>
399
- <h3>Complete Address</h3>
400
- <p>City: {addressForm.fields.city}</p>
401
- <p>State: {addressForm.fields.state}</p>
402
- <input
403
- value={addressForm.fields.street}
404
- onChange={(e) => addressForm.setField('street', e.target.value)}
405
- placeholder="Street"
406
- />
407
- <input
408
- value={addressForm.fields.number}
409
- onChange={(e) => addressForm.setField('number', e.target.value)}
410
- placeholder="Number"
411
- />
412
- <button onClick={addressForm.submit}>Place Order</button>
413
- </div>
414
- )}
415
- </Form>
416
- )}
288
+ <CreateTodo />
289
+ {array.map(todo => (
290
+ <SingleProvider key={todo.id} id={todo.id} table="todos" schema={schema}>
291
+ <TodoItem />
292
+ </SingleProvider>
293
+ ))}
417
294
  </div>
418
- )}
419
- </Form>
295
+ )
296
+ }
420
297
  ```
421
298
 
422
- ## Effects and Side Effects
423
-
424
- One of asasvirtuais's core strengths is making effects simple. No middleware arrays, no lifecycle hooks—just write code where it belongs.
299
+ When `create` resolves, the item appears in `array` immediately. Same for `update` and `remove`.
425
300
 
426
- ### Frontend Effects (React)
301
+ ---
427
302
 
428
- In React, you control exactly when effects happen by writing code around form actions.
303
+ ## Listing vs. filtering
429
304
 
430
- #### Pre-flight Effects
305
+ ### `useTable().list` — reactive, global
431
306
 
432
- Run code before submission:
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:
433
308
 
434
309
  ```tsx
435
- import { CreateForm } from '@asasvirtuais/react-interface'
436
- import { messageSchema } from '@/app/database'
310
+ const { array, list } = useTodos()
437
311
 
438
- <CreateForm
439
- table="messages"
440
- schema={messageSchema}
441
- defaults={{ content: '' }}
442
- >
443
- {({ fields, setField, submit, loading }) => (
444
- <form onSubmit={submit}>
445
- <textarea
446
- value={fields.content}
447
- onChange={(e) => setField('content', e.target.value)}
448
- />
449
- <button
450
- onClick={() => {
451
- // Pre-flight effect - runs before submit
452
- trackEvent('message_submit_clicked')
453
- validateContent(fields.content)
454
- submit()
455
- }}
456
- disabled={loading}
457
- >
458
- Send
459
- </button>
460
- </form>
461
- )}
462
- </CreateForm>
463
- ```
312
+ useEffect(() => { list.trigger({}) }, [])
464
313
 
465
- #### Post-flight Effects
466
-
467
- Run code after successful submission:
468
-
469
- ```tsx
470
- <CreateForm
471
- table="messages"
472
- schema={messageSchema}
473
- defaults={{ content: '' }}
474
- onSuccess={(message) => {
475
- // Post-flight effects - run after success
476
- notifyUser('Message sent!')
477
- scrollToBottom()
478
- trackAnalytics('message_created', { id: message.id })
479
- }}
480
- >
481
- {({ fields, setField, submit }) => (
482
- <form onSubmit={submit}>
483
- {/* form fields */}
484
- </form>
485
- )}
486
- </CreateForm>
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
+ ))
487
320
  ```
488
321
 
489
- #### Using Field Values Without Submitting
322
+ ### `FilterForm` local, paginated, or conditional
490
323
 
491
- Sometimes you want to use the form's field values without calling the server action:
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:
492
325
 
493
326
  ```tsx
494
- <CreateForm
495
- table="messages"
496
- schema={messageSchema}
497
- defaults={{ content: '' }}
498
- >
499
- {(form) => (
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 }) => (
500
332
  <div>
501
- <textarea
502
- value={form.fields.content}
503
- onChange={(e) => form.setField('content', e.target.value)}
333
+ <input
334
+ placeholder="Search..."
335
+ value={fields.query?.title ?? ''}
336
+ onChange={e => {
337
+ setField('query', { title: e.target.value })
338
+ submit()
339
+ }}
504
340
  />
505
-
506
- {/* This button calls the server action */}
507
- <button onClick={form.submit}>Send to Server</button>
508
-
509
- {/* This button uses field values without calling the action */}
510
- <button onClick={() => {
511
- // Just use the field values directly for local operations
512
- saveToLocalStorage(form.fields)
513
- showPreview(form.fields)
514
- }}>
515
- Save Draft Locally
516
- </button>
341
+ {loading && <p>Loading...</p>}
342
+ {result?.map(todo => <p key={todo.id}>{todo.title}</p>)}
517
343
  </div>
518
344
  )}
519
- </CreateForm>
520
- ```
521
-
522
- ### Backend Effects (API Routes)
523
-
524
- On the backend, effects are just functions wrapping other functions. No framework magic.
525
-
526
- #### Using tableInterface for Business Logic
527
-
528
- ```typescript
529
- // app/api/v1/[...params]/route.ts
530
- import { tableInterface } from '@asasvirtuais/interface'
531
- import { firestoreInterface } from '@/lib/firestore'
532
- import { messageSchema } from '@/app/database'
533
-
534
- // Wrap your base interface with business logic
535
- const messagesInterface = tableInterface(messageSchema, 'messages', {
536
- async create(props) {
537
- // Pre-flight validation
538
- await checkUserQuota(props.data.userId)
539
- await moderateContent(props.data.content)
540
-
541
- // The actual database operation
542
- const message = await firestoreInterface.create(props)
543
-
544
- // Post-flight side effects
545
- await updateConversationTimestamp(message.conversationId)
546
- await notifyParticipants(message.conversationId, message.id)
547
- await trackMessageCreated(message)
548
-
549
- return message
550
- },
551
-
552
- async update(props) {
553
- const existing = await firestoreInterface.find(props)
554
-
555
- // Business rule enforcement
556
- if (existing.role === 'assistant') {
557
- throw new Error("Cannot edit assistant messages")
558
- }
559
-
560
- if (existing.userId !== getCurrentUserId()) {
561
- throw new Error("Cannot edit other users' messages")
562
- }
563
-
564
- return firestoreInterface.update(props)
565
- },
566
-
567
- async remove(props) {
568
- const message = await firestoreInterface.find(props)
569
-
570
- // Cascade deletion
571
- await deleteMessageAttachments(message.id)
572
- await updateConversationCount(message.conversationId, -1)
573
-
574
- return firestoreInterface.remove(props)
575
- },
576
-
577
- // Pass through operations that don't need custom logic
578
- find: firestoreInterface.find,
579
- list: firestoreInterface.list,
580
- })
581
- ```
582
-
583
- ### Key Principles
584
-
585
- 1. **Effects are just code** - No special lifecycle methods or middleware patterns
586
- 2. **Control flow is visible** - Reading the code tells you exactly what runs and when
587
- 3. **Composition over configuration** - Wrap functions to add behavior, don't configure hooks
588
- 4. **Backend and frontend mirror each other** - The same compositional patterns work everywhere
589
-
590
- ## API Reference
591
-
592
- ### `Form<Fields, Result>`
593
-
594
- Combined fields and action management.
595
-
596
- **Props:**
597
- - `defaults?: Partial<Fields>` - Initial field values
598
- - `action: (fields: Fields) => Promise<Result>` - Async action to perform
599
- - `onResult?: (result: Result) => void` - Success callback
600
- - `onError?: (error: Error) => void` - Error callback
601
- - `autoTrigger?: boolean` - Auto-trigger action on mount
602
- - `children: ReactNode | (props) => ReactNode` - Render prop or children
603
-
604
- **Render Props:**
605
- - `fields: Fields` - Current field values
606
- - `setField: (name, value) => void` - Update single field
607
- - `setFields: (fields) => void` - Update multiple fields
608
- - `submit: (e?) => Promise<void>` - Trigger action
609
- - `loading: boolean` - Action loading state
610
- - `result: Result | null` - Action result
611
- - `error: Error | null` - Action error
612
-
613
- ### `FieldsProvider<T>`
614
-
615
- Field state management only.
616
-
617
- **Props:**
618
- - `defaults?: Partial<T>` - Initial field values
619
- - `children: ReactNode | (props) => ReactNode` - Render prop or children
620
-
621
- **Hook: `useFields<T>()`**
622
- - `fields: T` - Current field values
623
- - `setField: (name, value) => void` - Update single field
624
- - `setFields: (fields) => void` - Update multiple fields
625
-
626
- ### `ActionProvider<Params, Result>`
627
-
628
- Action management only.
629
-
630
- **Props:**
631
- - `params: Partial<Params>` - Action parameters
632
- - `action: (params) => Promise<Result>` - Async action
633
- - `onResult?: (result: Result) => void` - Success callback
634
- - `onError?: (error: Error) => void` - Error callback
635
- - `autoTrigger?: boolean` - Auto-trigger on mount
636
- - `children: ReactNode | (props) => ReactNode` - Render prop or children
637
-
638
- **Hook: `useAction<Params, Result>()`**
639
- - `params: Partial<Params>` - Current parameters
640
- - `submit: (e?) => Promise<void>` - Trigger action
641
- - `loading: boolean` - Loading state
642
- - `result: Result | null` - Action result
643
- - `error: Error | null` - Action error
644
-
645
- ## Philosophy
646
-
647
- ### Code Maintainability Over Everything
648
-
649
- The industry has normalized spreading code across dozens of files with dependency injection, decorators, and "clean architecture" patterns that make simple things complicated. asasvirtuais takes the opposite approach:
650
-
651
- **Keep business logic in single, readable files.**
652
-
653
- When you can see all the logic in one place, you can reason about it. When logic is scattered, every change becomes archaeology.
654
-
655
- ### Made for Humans and AI
656
-
657
- The patterns in asasvirtuais are simple enough that:
658
- - Junior developers can understand them in minutes
659
- - Senior developers appreciate the lack of ceremony
660
- - AI assistants can generate correct implementations on the first try
661
-
662
- This isn't about dumbing down—it's about removing accidental complexity.
663
-
664
- ### Against "Babel Towering"
665
-
666
- The AI trend seems focused on generating massive codebases quickly, stacking abstraction on abstraction. That's how you build towers that fall.
667
-
668
- asasvirtuais is designed for the opposite: codebases that stay maintainable even as they grow.
669
-
670
- ## Real-World Use
671
-
672
- I've used asasvirtuais with Airtable for data modeling on production projects. The combination of a simple frontend framework and a flexible backend lets you focus on solving actual problems instead of fighting your tools.
673
-
674
- ## AI Integration
675
-
676
- Give AI the asasvirtuais documentation and watch it generate multi-step forms with async validation in a single file—something that would normally require multiple files, complex state management, and careful coordination.
677
-
678
- Try it with [Google AI Studio](https://ai.studio/apps/drive/1-MwQzpbgMZhRqSbpqQYX1IRpvj61F_l8).
679
-
680
-
681
- # Model Package Instructions
682
-
683
- A model package is a self-contained module that defines a data model and provides React components for interacting with that data. Based on the chat example, here's how to structure a model package:
684
-
685
- ## File Structure
686
-
687
- ```
688
- app/[model-name]/
689
- ├── index.ts # Schema definitions and types
690
- ├── fields.tsx # Individual form field components
691
- ├── forms.tsx # Complete form components
692
- └── table.tsx # Provider and hooks for data access
345
+ </FilterForm>
693
346
  ```
694
347
 
695
- ## 1. Schema Definition (`index.ts`)
696
-
697
- Define your data model using Zod schemas:
698
-
699
- ```typescript
700
- import z from 'zod'
701
-
702
- // Define the complete object structure (what comes from the database)
703
- export const readable = z.object({
704
- id: z.string(),
705
- // ... other fields that can be read
706
- })
707
-
708
- // Define which fields can be written/modified
709
- export const writable = readable.pick({
710
- // ... fields that can be created/updated
711
- })
712
-
713
- // Export the schema object
714
- export const schema = { readable, writable }
715
-
716
- // Export TypeScript types
717
- export type Readable = z.infer<typeof readable>
718
- export type Writable = z.infer<typeof writable>
719
- ```
348
+ ---
720
349
 
721
- **Key Points:**
722
- - `readable`: Full object schema including `id` and all readable fields
723
- - `writable`: Subset of fields that users can create/modify (typically excludes `id`)
724
- - Use `.pick()` to select fields from readable for writable
725
- - Export both the schema object and TypeScript types
350
+ ## Async selector fields
726
351
 
727
- ## 2. Field Components (`fields.tsx`)
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.
728
353
 
729
- Create reusable field components for individual form inputs:
354
+ Say a todo can be tagged, and the user needs to search and select a tag while creating the todo:
730
355
 
731
- ```typescript
732
- import { Input, InputProps } from '@chakra-ui/react'
356
+ ```tsx
357
+ // app/todos/fields.tsx
733
358
  import { useFields } from 'asasvirtuais/fields'
359
+ import { FilterForm } from 'asasvirtuais/form'
360
+ import { schema as tagsSchema } from '@/app/tags'
734
361
 
735
- export function [FieldName]Field(props: InputProps) {
736
- const { fields, setField } = useFields<{fieldName: type}>()
737
-
738
- return (
739
- <Input
740
- name='fieldName'
741
- value={fields.fieldName}
742
- onChange={e => setField('fieldName', e.target.value)}
743
- {...props}
744
- />
745
- )
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
+ )
746
392
  }
747
393
  ```
748
394
 
749
- **Key Points:**
750
- - Use `useFields` hook with type annotation matching your field
751
- - Pass through additional props using spread operator
752
- - Set appropriate `name` attribute
753
- - Handle `onChange` with `setField`
754
-
755
- ## 3. Form Components (`forms.tsx`)
395
+ Use it inside any form — it just works:
756
396
 
757
- Create complete forms for creating and filtering data:
758
-
759
- ```typescript
760
- import { CreateForm, FilterForm, useTableInterface } from 'asasvirtuais/react-interface'
397
+ ```tsx
398
+ // app/todos/forms.tsx
399
+ import { CreateForm } from 'asasvirtuais/form'
761
400
  import { schema } from '.'
762
- import { [Field]Field } from './fields'
763
- import { Stack, Button } from '@chakra-ui/react'
764
-
765
- // Create form
766
- export function Create[Model]() {
767
- return (
768
- <CreateForm table='tableName' schema={schema}>
769
- {form => (
770
- <Stack as='form' onSubmit={form.submit}>
771
- <[Field]Field />
772
- <Button type='submit'>Create [Model]</Button>
773
- </Stack>
774
- )}
775
- </CreateForm>
776
- )
777
- }
401
+ import { TitleField, TagSelectorField } from './fields'
778
402
 
779
- // Filter/List form
780
- export function Filter[Models]() {
781
- const { remove } = useTableInterface('tableName', schema)
782
-
783
- return (
784
- <FilterForm table='tableName' schema={schema}>
785
- {form => (
786
- <Stack>
787
- {form.result?.map(item => (
788
- <div key={item.id}>
789
- {/* Display item data */}
790
- <Button onClick={() => remove.trigger({id: item.id})}>
791
- Delete
792
- </Button>
793
- </div>
794
- ))}
795
- </Stack>
796
- )}
797
- </FilterForm>
798
- )
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
+ )
799
417
  }
800
418
  ```
801
419
 
802
- **Key Points:**
803
- - `CreateForm` handles creation logic, provides `form.submit`
804
- - `FilterForm` provides `form.result` with filtered data
805
- - Use `useTableInterface` for operations like `remove`
806
- - Always provide `table` name and `schema` to forms
807
-
808
- ## 4. Provider and Hooks (`table.tsx`)
420
+ The `FilterForm` queries the `tags` table asynchronously. The `CreateForm` owns the selected `tagId`. Neither knows about the other.
809
421
 
810
- Set up the data provider and custom hooks using `InterfaceProvider` and `useInterface`:
811
-
812
- ```typescript
813
- 'use client'
814
- import { TableProvider, useTableInterface } from 'asasvirtuais/react-interface'
815
- import { useInterface } from 'asasvirtuais/fetch-interface'
816
- import { schema } from '.'
422
+ ---
817
423
 
818
- export function use[Models]() {
819
- return useTableInterface('tableName', schema)
820
- }
424
+ ## The single record pattern
821
425
 
822
- export function [Models]Provider({ children }: { children: React.ReactNode }) {
823
- return (
824
- <TableProvider
825
- table='tableName'
826
- schema={schema}
827
- interface={useInterface()}
828
- >
829
- {children}
830
- </TableProvider>
831
- )
832
- }
833
- ```
834
-
835
- Then in your layout, wrap with `InterfaceProvider` to set up the context:
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:
836
427
 
837
428
  ```tsx
838
- import { InterfaceProvider } from 'asasvirtuais/fetch-interface'
839
- import { DatabaseProvider } from 'asasvirtuais/react-interface'
840
- import { [Models]Provider } from './table'
841
- import { schema } from '.'
842
-
843
- <DatabaseProvider>
844
- <InterfaceProvider schema={schema} baseUrl='/api/v1'>
845
- <[Models]Provider>
846
- {children}
847
- </[Models]Provider>
848
- </InterfaceProvider>
849
- </DatabaseProvider>
850
- ```
851
-
852
- **Key Points:**
853
- - Mark table.tsx as `'use client'` for Next.js
854
- - `InterfaceProvider` creates the fetch interface and provides it via context
855
- - `useInterface()` retrieves the interface from context inside `TableProvider`
856
- - Create a custom hook for easy access to table interface
857
-
858
- ## Naming Conventions
859
-
860
- - **Model name**: Singular (e.g., `chat`, `user`, `product`)
861
- - **Table name**: Plural (e.g., `chats`, `users`, `products`)
862
- - **Field components**: `[Field]Field` (e.g., `TitleField`, `EmailField`)
863
- - **Form components**: `Create[Model]`, `Filter[Models]` (e.g., `CreateChat`, `FilterChats`)
864
- - **Provider**: `[Models]Provider` (e.g., `ChatsProvider`)
865
- - **Hook**: `use[Models]` (e.g., `useChats`)
866
-
867
- ## Usage Example
868
-
869
- ```typescript
870
- import { ChatsProvider } from './chat/table'
871
- import { CreateChat, FilterChats } from './chat/forms'
872
-
873
- function App() {
874
- return (
875
- <ChatsProvider>
876
- <CreateChat />
877
- <FilterChats />
878
- </ChatsProvider>
879
- )
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>
880
442
  }
881
443
  ```
882
444
 
445
+ If the record isn't in the reactive index yet, `SingleProvider` fetches it automatically.
883
446
 
884
- ## Contributing
447
+ ---
448
+
449
+ ## Effects
885
450
 
886
- This is the result of years of meditation on overengineering. If you see ways to make it simpler (not more feature-rich, simpler), I'm interested.
451
+ There is no middleware or lifecycle configuration. Effects are code written around the action:
887
452
 
888
- ## License
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>
889
473
 
890
- MIT
474
+ // Using field values without submitting
475
+ <button onClick={() => saveDraftLocally(form.fields)}>
476
+ Save Draft
477
+ </button>
478
+ ```
891
479
 
892
480
  ---
893
481
 
894
- *Built by someone who spent 7 years learning that the hard way is usually the wrong way.*
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` |