asasvirtuais 0.4.4 → 0.5.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,222 +1,421 @@
1
-
2
- 1st: Declare your database schema
3
- ```ts
4
- import { z } from 'zod'
5
- export const schema = {
6
- users: {
7
- readable: {
8
- id: z.string(),
9
- name: z.string(),
10
- email: z.string().email(),
11
- },
12
- writable: {
13
- name: z.string(),
14
- email: z.string().email(),
15
- }
16
- }
17
- }
18
- ```
19
-
20
- 2nd: Write your adapters (or use existing ones)
21
- ```ts
22
- import { tableInterface } from 'asasvirtuais/interface'
23
- const myDatabaseAdapter = tableInterface(
24
- schema,
25
- null, // Null for generic interface,
26
- {
27
- find(props) {...},
28
- list(props) {...},
29
- create(props) {...},
30
- update(props) {...},
31
- delete(props) {...},
32
- }
33
- )
34
- myDatabaseAdapter.create({table: '', data: {...}})
35
- const myTableAdapter = tableInterface(schema, 'table', {...})
36
- myTableAdapter.create({data: {...}})
37
- ```
38
-
39
- 3rd: expose the API endpoints
40
- ```ts
41
- import { myDatabaseAdapter } from './adapter'
42
- import { createDynamicRoute } from 'asasvirtuais/next-interface'
43
- export const GET = createDynamicRoute(myDatabaseAdapter)
44
- export const POST = createDynamicRoute(myDatabaseAdapter)
45
- export const PATCH = createDynamicRoute(myDatabaseAdapter)
46
- export const DELETE = createDynamicRoute(myDatabaseAdapter)
47
- ```
48
-
49
- 4th: consume the RESTful API
50
- ```ts
51
- 'use client'
52
- import { fetchInterface } from '@/packages/fetch-interface'
53
- import { schema } from './database'
54
- import { database } from '@/packages/react-interface'
55
-
56
- const fethcher = fetchInterface({schema, baseURL: 'http://localhost:3000/api/v1', headers: {} })
57
- const { find, list, create, update, remove } = fetcher
58
-
59
- // Super useful
60
- export const { DatabaseProvider, useDatabase, useTable, CreateForm, UpdateForm, FilterForm, SingleProvider, useSingle } = database<typeof schema>(schema, fetcher)
61
- ```
62
-
63
- 5th: Using the DatabaseProvider
64
- ```tsx
65
- // You are required to write all the table names you'll use beneath this context provider
66
- <DatabaseProvider users={index} chats={{[id]: chat}} messages={{}}>
67
- {...}
68
- </DatabaseProvider>
69
- ```
70
-
71
- 6th: Provide and consume the context
72
- ```tsx
73
- // CreateForm, UpdateForm and FiltersForm use asasvirtuais/form and asasvirtuais/fields
74
- <CreateForm table='table' onSuccess={handleResult}>
75
- {/* Note: callback accepts the props of the method (list, create, update) */}
76
- {({loading, result, error, submit, callback}) => {
77
- return (
78
- <form onSubmit={submit}>
79
- <FiltersFields/>
80
- <SubmitButton disabled={loading} loading={loading}>
81
- {loading ? <Spinner/> : 'Apply Filters'}
82
- </SubmitButton>
83
- {result.map(item => <ViewItem key={item.id} item={item} />)}
84
- </form>
85
- )
86
- }}
87
- </CreateForm>
88
-
89
- // To avoid prop drilling of data models
90
- <SingleProvider table='users' id={id}>
91
- {
92
- const { single: user } = useSingle()
93
- }
94
- </SingleProvider>
95
- ```
96
-
97
- 7th: Use our fields on your form
98
- ```tsx
99
- import { fields } from 'asasvirtuais-fields'
100
- <CreateForm table='table' defaults={{belongsTo: user.id}}>
101
- {(props) => (
102
- <Stack>
103
- <fields.string.Edit value={props.fields.name} setValue={value => props.setField('name', value)} />
104
- <Button type='submit' loading={props.loading}>
105
- </Stack>
106
- )}
107
- </CreateForm>
108
- ```
109
-
110
- Querying the database
111
- ```ts
112
- const array = await myTableAdapter.list({query: { id }})
113
- ```
114
-
115
- Form and Fields packages
116
- ```tsx
117
- import { FormProvider } from 'asasvirtuais/form'
118
- import { FieldsProvider } from 'asasvirtuais/fields'
119
- <FormProvider<Props, Result> data={FormDataOrObject} onSuccess={handleResult}>
120
- {({error, result, loading, values, submit, callback}) => {
121
- return (
122
- <FieldsProvider<{key: value}> defaults={[propKey]: propValue}>
123
- {({field, setField, setFields}) => (
124
- <SomeFieldComponent value={field} onChange={value => setField(value)} />
125
- )}
126
- </FieldsProvider>
127
- )
128
- }}
129
- </FormProvider>
130
- ```
131
-
132
- Effects Example
133
- ```ts
134
- import { authenticate } from '@/packages/auth0'
135
- import { createDynamicRoute } from '@/packages/next-interface'
136
- import { firestoreInterface } from '@/packages/firestore'
137
-
138
- const dbInterface = firestoreInterface()
139
-
140
- const apiInterface = {
141
- async create(props) {
142
- await authenticate()
143
- return dbInterface.create(props)
144
- },
145
- async find(props) {
146
- await authenticate()
147
- return dbInterface.find(props)
148
- },
149
- async update(props) {
150
- await authenticate()
151
- return dbInterface.update(props)
152
- },
153
- async remove(props) {
154
- await authenticate()
155
- return dbInterface.remove(props)
156
- },
157
- async list(props) {
158
- await authenticate()
159
- return dbInterface.list(props)
160
- },
161
- } as typeof dbInterface
162
- ```
163
-
164
- Forms Example
165
- ```tsx
166
- <UpdateForm table='characters' id={characterId} defaults={{
167
- name: character.name,
168
- avatar: (character).avatar ?? '',
169
- personality: character.personality,
170
- appearance: character.appearance,
171
- definition: character.definition,
172
- }} onSuccess={(character) => addCharacterToChat(character)}>
173
- {(props) => (
174
- <Stack asChild>
175
- <form onSubmit={props.submit}>
176
- <Stack gap={3}>
177
- <div>
178
- <Text>Name</Text>
179
- <Input value={(props.fields).name ?? ''} onChange={e => props.setField('name', e.target.value)} />
180
- </div>
181
- <div>
182
- <Text>Avatar</Text>
183
- <AvatarUpload value={(props.fields).avatar ?? ''} onChange={(url: string) => props.setField('avatar' , url)} />
184
- </div>
185
- <div>
186
- <Text>Personality</Text>
187
- <Textarea value={(props.fields).personality ?? ''} onChange={e => props.setField('personality', e.target.value)} />
188
- </div>
189
- <div>
190
- <Text>Appearance</Text>
191
- <Textarea value={(props.fields).appearance ?? ''} onChange={e => props.setField('appearance', e.target.value)} />
192
- </div>
193
- <div>
194
- <Text>Definition</Text>
195
- <Textarea value={(props.fields).definition ?? ''} onChange={e => props.setField('definition', e.target.value)} />
196
- </div>
197
- <HStack justify='flex-end' gap={2}>
198
- <Button type='button' size='sm' variant='outline' colorPalette='red' disabled={remove.loading} onClick={handleDelete}>
199
- {remove.loading ? <Spinner size='sm' /> : 'Delete'}
200
- </Button>
201
- <Button type='submit' size='sm' disabled={props.loading}>
202
- {props.loading ? <Spinner size='sm' /> : 'Save'}
203
- </Button>
204
- </HStack>
205
- </Stack>
206
- </form>
207
- </Stack>
208
- )}
209
- </UpdateForm>
210
- ```
211
-
212
- Roadmap:
213
- - Relationships, linked records, TableLookup field
214
- - Realtime database with feathersjs
215
-
216
-
217
- TODO, add this to github actions and set the secret for this cron job authorization
218
- "path": "/api/cron/diary-greeting"
219
- "schedule": "*/5 * * * *"
220
- "path": "/api/cron/diary-summarize"
221
- "schedule": "55 23 * * *"
222
-
1
+ # Asas Virtuais Framework
2
+
3
+ Asas Virtuais is a full-stack, schema-driven framework for Next.js designed for rapid, type-safe, and maintainable application development. It provides a powerful abstraction layer for data management, allowing you to define your data shape once with Zod and get automatically generated API endpoints, client-side hooks, and form components.
4
+
5
+ ## Core Principles
6
+
7
+ - **Schema First**: Define your data models using Zod schemas. This provides a single source of truth for validation, type safety, and data shape across the entire stack.
8
+ - **Interface-Driven**: Code against a standardized `TableInterface` on both the client and server. This decouples your application logic from the specific database implementation, allowing for easy swapping of backends.
9
+ - **Convention over Configuration**: The framework automatically generates RESTful API endpoints and client-side hooks from your schema, minimizing boilerplate and accelerating development.
10
+ - **Extensible by Design**: Every layer is built to be wrapped or replaced. You can easily add custom logic like authentication to API endpoints, create new database adapters, or build bespoke form field components.
11
+
12
+ ## Step-by-Step Guide
13
+
14
+ This guide will walk you through building a feature using the Asas Virtuais framework.
15
+
16
+ ### Step 1: Define Your Schema
17
+
18
+ Everything starts with your data schema. Use Zod to define `readable` (data sent to the client) and `writable` (data accepted from the client) shapes for each table.
19
+
20
+ `app/database.ts`:
21
+ ```ts
22
+ import { z } from 'zod'
23
+
24
+ export const schema = {
25
+ // A generic 'users' table
26
+ users: {
27
+ readable: z.object({
28
+ id: z.string(),
29
+ name: z.string(),
30
+ email: z.string().email(),
31
+ }),
32
+ writable: z.object({
33
+ name: z.string().nonempty('Name is required'),
34
+ email: z.string().email(),
35
+ })
36
+ },
37
+ // A generic 'posts' table
38
+ posts: {
39
+ readable: z.object({
40
+ id: z.string(),
41
+ title: z.string(),
42
+ content: z.string().optional(),
43
+ authorId: z.string(),
44
+ }),
45
+ writable: z.object({
46
+ title: z.string().nonempty('Title is required'),
47
+ content: z.string().optional(),
48
+ authorId: z.string(),
49
+ })
50
+ },
51
+ }
52
+ ```
53
+
54
+ ### Step 2: Create a Server-Side Adapter
55
+
56
+ Implement the `TableInterface` for your chosen database. The framework is backend-agnostic. This project uses a Firestore adapter.
57
+
58
+ `packages/firestore.ts`:
59
+ ```ts
60
+ import { TableInterface } from './interface'
61
+ import { firestore } from './firebase'
62
+
63
+ // A generic Firestore adapter implementing the standard interface
64
+ export function firestoreInterface<...>(defaultTable?: T): TableInterface<...> {
65
+ return {
66
+ async find({ table = defaultTable, id }) {
67
+ const docSnap = await firestore.collection(table).doc(id).get()
68
+ if (docSnap.exists) return { id: docSnap.id, ...docSnap.data() }
69
+ throw new Error(`Record not found`)
70
+ },
71
+ // ... list, create, update, remove implementations
72
+ }
73
+ }
74
+ ```
75
+
76
+ Then, instantiate the adapter for each of your tables.
77
+
78
+ `app/server.ts`:
79
+ ```ts
80
+ import { firestoreInterface } from '@/packages/firestore'
81
+ import { schema } from './database'
82
+
83
+ // Create a server-side data access object
84
+ export const server = {
85
+ users: firestoreInterface<typeof schema, 'users'>('users'),
86
+ posts: firestoreInterface<typeof schema, 'posts'>('posts'),
87
+ }
88
+ ```
89
+
90
+ ### Step 3: Expose API Endpoints
91
+
92
+ With the server adapter ready, you can expose it as a RESTful API using `createDynamicRoute`. This automatically creates `GET`, `POST`, `PATCH`, and `DELETE` handlers. You can easily wrap the base implementation to add cross-cutting concerns like authentication.
93
+
94
+ `app/api/v1/[...params]/route.ts`:
95
+ ```ts
96
+ import { authenticate } from '@/packages/auth0' // Your authentication logic
97
+ import { createDynamicRoute } from '@/packages/next-interface'
98
+ import { firestoreInterface } from '@/packages/firestore'
99
+
100
+ const dbInterface = firestoreInterface()
101
+
102
+ // Wrap the base interface to enforce business logic and authentication
103
+ const apiInterface = {
104
+ async create(props) {
105
+ const user = await authenticate()
106
+ // Example: Inject the authorId automatically
107
+ if (props.table === 'posts') {
108
+ props.data.authorId = user.id
109
+ }
110
+ return dbInterface.create(props)
111
+ },
112
+ async find(props) {
113
+ const user = await authenticate()
114
+ const result = await dbInterface.find(props)
115
+ // Example: Check for ownership on 'posts'
116
+ if (result.authorId && result.authorId !== user.id) {
117
+ throw new Error('Unauthorized')
118
+ }
119
+ return result
120
+ },
121
+ // ... implementations for update, remove, and list with auth checks
122
+ } as typeof dbInterface
123
+
124
+ // Export the dynamic route handlers
125
+ export const GET = createDynamicRoute(apiInterface)
126
+ export const POST = createDynamicRoute(apiInterface)
127
+ export const DELETE = createDynamicRoute(apiInterface)
128
+ export const PATCH = createDynamicRoute(apiInterface)
129
+ ```
130
+
131
+ ### Step 4: Initialize the Client Interface
132
+
133
+ On the client, initialize the React interface. This will provide all the hooks and components you need to interact with your API.
134
+
135
+ `app/interface.tsx`:
136
+ ```tsx
137
+ 'use client'
138
+ import { fetchInterface } from '@/packages/fetch-interface'
139
+ import { schema } from './database'
140
+ import { reactInterface } from '@/packages/react-interface'
141
+
142
+ // Create the client-side fetcher
143
+ const fetcher = fetchInterface({ schema, baseUrl: '/api/v1' })
144
+
145
+ // Export all the React hooks and components for the app
146
+ export const {
147
+ DatabaseProvider,
148
+ useTable,
149
+ CreateForm,
150
+ UpdateForm,
151
+ SingleProvider,
152
+ useSingle,
153
+ // ... and more
154
+ } = reactInterface<typeof schema>(schema, fetcher)
155
+ ```
156
+
157
+ ### Step 5: Provide Data with `DatabaseProvider`
158
+
159
+ In your layouts, use `DatabaseProvider` to fetch initial data and make it available to all components underneath it via React Context.
160
+
161
+ `app/posts/[id]/layout.tsx`:
162
+ ```tsx
163
+ import { DatabaseProvider, SingleProvider } from '@/app/interface'
164
+ import { server } from '@/app/server'
165
+ import { layout } from '@/packages/next'
166
+
167
+ export default layout<{ id: string }>( async ({ id, children }) => {
168
+ // Fetch initial data on the server
169
+ const post = await server.posts.find({ id })
170
+
171
+ // Provide the fetched data to the client-side context
172
+ return (
173
+ <DatabaseProvider posts={{ [id]: post }} users={{}}>
174
+ <SingleProvider id={id} table='posts'>
175
+ {children}
176
+ </SingleProvider>
177
+ </DatabaseProvider>
178
+ )
179
+ } )
180
+ ```
181
+
182
+ ### Step 6: Consume Data with Hooks
183
+
184
+ Use the provided hooks to access and manipulate data in your components.
185
+
186
+ #### `useTable`
187
+ To work with a collection of records (list, create, update, delete).
188
+
189
+ `app/posts/page.tsx`:
190
+ ```tsx
191
+ 'use client'
192
+ import { useTable } from '@/app/interface'
193
+ import Link from 'next/link'
194
+ import { IconButton } from '@chakra-ui/react'
195
+ import { LuTrash } from 'react-icons/lu'
196
+
197
+ export default function PostsPage() {
198
+ const { array: posts, remove, list } = useTable('posts')
199
+
200
+ useEffect(() => {
201
+ list.trigger({}) // Fetch all posts
202
+ }, [])
203
+
204
+ return (
205
+ <VStack>
206
+ {posts.map((post) => (
207
+ <HStack key={post.id}>
208
+ <Link href={`/posts/${post.id}`}>
209
+ <Text>{post.title}</Text>
210
+ </Link>
211
+ <IconButton
212
+ onClick={() => remove.trigger({id: post.id})}
213
+ disabled={remove.loading}
214
+ icon={<LuTrash />}
215
+ />
216
+ </HStack>
217
+ ))}
218
+ </VStack>
219
+ )
220
+ }
221
+ ```
222
+
223
+ #### `useSingle`
224
+ To work with a single record, often used within a `SingleProvider`.
225
+
226
+ `app/posts/[id]/page.tsx`:
227
+ ```tsx
228
+ 'use client'
229
+ import { useSingle } from '@/app/interface'
230
+
231
+ export default function PostPage() {
232
+ // Get the current post from the context provided by SingleProvider
233
+ const { single: post } = useSingle('posts')
234
+
235
+ return (
236
+ <article>
237
+ <h1>{post.title}</h1>
238
+ <p>{post.content}</p>
239
+ </article>
240
+ )
241
+ }
242
+ ```
243
+
244
+ ### Step 7: High-Level Abstracted Forms
245
+
246
+ The framework provides `CreateForm` and `UpdateForm` components that handle form state, submission, and API calls for you.
247
+
248
+ `app/posts/new/page.tsx`:
249
+ ```tsx
250
+ 'use client'
251
+ import { CreateForm } from '@/app/interface'
252
+ import { Input, Button, Textarea } from '@chakra-ui/react'
253
+
254
+ export function NewPostPage() {
255
+ const { user } = useUser() // from @auth0/nextjs-auth0/client
256
+
257
+ return (
258
+ <CreateForm
259
+ table='posts'
260
+ // Set default values for the form
261
+ defaults={{ authorId: user.id, title: 'New Post' }}
262
+ onSuccess={(post) => router.push(`/posts/${post.id}`)}
263
+ >
264
+ {/* The child is a render prop with form state and field state */}
265
+ {({ loading, fields, setField, submit }) => (
266
+ <form onSubmit={submit}>
267
+ <Input
268
+ value={fields.title}
269
+ onChange={(e) => setField('title', e.target.value)}
270
+ />
271
+ <Textarea
272
+ value={fields.content}
273
+ onChange={(e) => setField('content', e.target.value)}
274
+ />
275
+ <Button type='submit' disabled={loading}>
276
+ {loading ? 'Saving...' : 'Create Post'}
277
+ </Button>
278
+ </form>
279
+ )}
280
+ </CreateForm>
281
+ )
282
+ }
283
+ ```
284
+
285
+ #### `FilterForm`
286
+ Use `FilterForm` to easily create search and filter functionality. It wraps the `list` method of a table and displays the results.
287
+
288
+ `app/posts/SearchPosts.tsx`:
289
+ ```tsx
290
+ 'use client'
291
+ import { FilterForm } from '@/app/interface'
292
+ import { Input, Button, VStack, Text, Box } from '@chakra-ui/react'
293
+
294
+ function SearchPosts() {
295
+ return (
296
+ <Box>
297
+ <FilterForm
298
+ table="posts"
299
+ // Set default filter values. The form will initially load with these.
300
+ defaults={{ title: '' }}
301
+ >
302
+ {({ loading, fields, setField, submit, result }) => (
303
+ <VStack as="form" onSubmit={submit} align="stretch" spacing={4}>
304
+ <Input
305
+ placeholder="Search by title..."
306
+ value={fields.title}
307
+ onChange={(e) => setField('title', e.target.value)}
308
+ />
309
+ <Button type="submit" disabled={loading} alignSelf="flex-start">
310
+ {loading ? 'Searching...' : 'Search'}
311
+ </Button>
312
+
313
+ {/* Display the results from the list call */}
314
+ <VStack align="stretch" mt={4} spacing={2}>
315
+ {result && result.length > 0 ? (
316
+ result.map(post => (
317
+ <Text key={post.id}>- {post.title}</Text>
318
+ ))
319
+ ) : (
320
+ <Text color="gray.500">No results found.</Text>
321
+ )}
322
+ </VStack>
323
+ </VStack>
324
+ )}
325
+ </FilterForm>
326
+ </Box>
327
+ );
328
+ }
329
+ ```
330
+
331
+ ## Form and Field Primitives
332
+
333
+ The `CreateForm` and `UpdateForm` components are convenient abstractions. For more control, you can use the underlying `FormProvider` and `FieldsProvider` primitives directly.
334
+
335
+ ### `packages/form.tsx`
336
+
337
+ `FormProvider` is a render-prop component that wraps your form submission logic. It manages the `loading`, `error`, and `result` state of an async callback.
338
+
339
+ - **Props**:
340
+ - `callback`: An async function that takes the form data and performs the submission.
341
+ - `data`: The initial data for the form.
342
+ - `onSuccess`: An optional function to call with the result of a successful submission.
343
+ - **Render Prop State**:
344
+ - `loading`: `boolean` - True while the callback is executing.
345
+ - `error`: `Error | null` - The error object if the submission fails.
346
+ - `result`: `Result | null` - The data returned from a successful submission.
347
+ - `submit`: `function` - The function to trigger the form submission.
348
+ - `values`: `Fields` - The current data object for the form.
349
+
350
+ ### `packages/fields.tsx`
351
+
352
+ `FieldsProvider` manages the state of your form's input fields. It holds the `fields` object and provides `setField` and `setFields` methods to update it.
353
+
354
+ - **Props**:
355
+ - `defaults`: The initial values for the form fields.
356
+ - **Render Prop State**:
357
+ - `fields`: `T` - The object containing the current state of all form fields.
358
+ - `setField`: `(name, value)` - A function to update a single field's value.
359
+ - `setFields`: `(newFields)` - A function to replace the entire fields object.
360
+
361
+ ### Using `FormProvider` and `FieldsProvider` Together
362
+
363
+ Here is how you can build a form from scratch using the low-level primitives. This pattern is what `CreateForm` uses internally.
364
+
365
+ ```tsx
366
+ import { FormProvider } from 'asasvirtuais/form'
367
+ import { FieldsProvider } from 'asasvirtuais/fields'
368
+ import { Input, Button, Spinner, Text, Textarea, Stack } from '@chakra-ui/react'
369
+
370
+ function NewPostForm({ onCreate }) {
371
+ // The function that will be called on form submission
372
+ const createPostCallback = async (fields) => {
373
+ // This would typically be an API call, e.g., from useTable()
374
+ const newPost = await api.posts.create({ data: fields });
375
+ return newPost;
376
+ };
377
+
378
+ return (
379
+ // 1. FormProvider handles the submission process
380
+ <FormProvider callback={createPostCallback} data={{}} onSuccess={onCreate}>
381
+ {({ loading, error, submit }) => (
382
+ // 2. FieldsProvider manages the state of the form's inputs
383
+ <FieldsProvider defaults={{ title: '', content: '', authorId: '1' }}>
384
+ {({ fields, setField }) => (
385
+ <form onSubmit={(e) => {
386
+ e.preventDefault();
387
+ // Pass the current field state to the submit function
388
+ submit(fields);
389
+ }}>
390
+ <Stack>
391
+ <label>Title</label>
392
+ <Input
393
+ value={fields.title}
394
+ onChange={(e) => setField('title', e.target.value)}
395
+ />
396
+
397
+ <label>Content</label>
398
+ <Textarea
399
+ value={fields.content}
400
+ onChange={(e) => setField('content', e.target.value)}
401
+ />
402
+
403
+ {error && <Text color="red.500">{error.message}</Text>}
404
+
405
+ <Button type="submit" disabled={loading}>
406
+ {loading ? <Spinner /> : 'Create Post'}
407
+ </Button>
408
+ </Stack>
409
+ </form>
410
+ )}
411
+ </FieldsProvider>
412
+ )}
413
+ </FormProvider>
414
+ );
415
+ }
416
+ ```
417
+
418
+ ## Roadmap
419
+
420
+ - [ ] Relationships, linked records, TableLookup field
421
+ - [ ] Realtime database with feathersjs
@@ -1,80 +1,80 @@
1
- 'use client'
2
- import { useState, useEffect } from 'react'
3
-
4
- export interface OpenRouterModel {
5
- id: string
6
- name: string
7
- description?: string
8
- context_length?: number
9
- pricing?: {
10
- prompt?: string
11
- completion?: string
12
- }
13
- architecture?: {
14
- modality?: string
15
- }
16
- }
17
-
18
- interface UseOpenRouterModelsResult {
19
- models: OpenRouterModel[]
20
- loading: boolean
21
- error: string | null
22
- }
23
-
24
- export function useOpenRouterModels(): UseOpenRouterModelsResult {
25
- const [models, setModels] = useState<OpenRouterModel[]>([])
26
- const [loading, setLoading] = useState(true)
27
- const [error, setError] = useState<string | null>(null)
28
-
29
- useEffect(() => {
30
- async function fetchModels() {
31
- try {
32
- setLoading(true)
33
- setError(null)
34
-
35
- const response = await fetch('https://openrouter.ai/api/v1/models', {
36
- headers: {
37
- 'Content-Type': 'application/json',
38
- },
39
- })
40
-
41
- if (!response.ok) {
42
- throw new Error(`Failed to fetch models: ${response.statusText}`)
43
- }
44
-
45
- const data = await response.json()
46
-
47
- // Filter and sort models for better UX
48
- const filteredModels = data.data
49
- ?.filter((model: any) =>
50
- model.id &&
51
- model.name &&
52
- !model.id.includes(':free') // Exclude free models for cleaner list
53
- )
54
- ?.sort((a: any, b: any) => a.name.localeCompare(b.name))
55
- ?.slice(0, 50) || [] // Limit to 50 models for performance
56
-
57
- setModels(filteredModels)
58
- } catch (err) {
59
- console.error('Error fetching OpenRouter models:', err)
60
- setError(err instanceof Error ? err.message : 'Unknown error occurred')
61
-
62
- // Fallback to default models
63
- setModels([
64
- { id: 'openrouter/auto', name: 'Auto (Recommended)' },
65
- { id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet' },
66
- { id: 'openai/gpt-4o', name: 'GPT-4o' },
67
- { id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini' },
68
- { id: 'google/gemini-pro', name: 'Gemini Pro' },
69
- { id: 'meta-llama/llama-3.1-8b-instruct', name: 'Llama 3.1 8B' },
70
- ])
71
- } finally {
72
- setLoading(false)
73
- }
74
- }
75
-
76
- fetchModels()
77
- }, [])
78
-
79
- return { models, loading, error }
1
+ 'use client'
2
+ import { useState, useEffect } from 'react'
3
+
4
+ export interface OpenRouterModel {
5
+ id: string
6
+ name: string
7
+ description?: string
8
+ context_length?: number
9
+ pricing?: {
10
+ prompt?: string
11
+ completion?: string
12
+ }
13
+ architecture?: {
14
+ modality?: string
15
+ }
16
+ }
17
+
18
+ interface UseOpenRouterModelsResult {
19
+ models: OpenRouterModel[]
20
+ loading: boolean
21
+ error: string | null
22
+ }
23
+
24
+ export function useOpenRouterModels(): UseOpenRouterModelsResult {
25
+ const [models, setModels] = useState<OpenRouterModel[]>([])
26
+ const [loading, setLoading] = useState(true)
27
+ const [error, setError] = useState<string | null>(null)
28
+
29
+ useEffect(() => {
30
+ async function fetchModels() {
31
+ try {
32
+ setLoading(true)
33
+ setError(null)
34
+
35
+ const response = await fetch('https://openrouter.ai/api/v1/models', {
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ },
39
+ })
40
+
41
+ if (!response.ok) {
42
+ throw new Error(`Failed to fetch models: ${response.statusText}`)
43
+ }
44
+
45
+ const data = await response.json()
46
+
47
+ // Filter and sort models for better UX
48
+ const filteredModels = data.data
49
+ ?.filter((model: any) =>
50
+ model.id &&
51
+ model.name &&
52
+ !model.id.includes(':free') // Exclude free models for cleaner list
53
+ )
54
+ ?.sort((a: any, b: any) => a.name.localeCompare(b.name))
55
+ ?.slice(0, 50) || [] // Limit to 50 models for performance
56
+
57
+ setModels(filteredModels)
58
+ } catch (err) {
59
+ console.error('Error fetching OpenRouter models:', err)
60
+ setError(err instanceof Error ? err.message : 'Unknown error occurred')
61
+
62
+ // Fallback to default models
63
+ setModels([
64
+ { id: 'openrouter/auto', name: 'Auto (Recommended)' },
65
+ { id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet' },
66
+ { id: 'openai/gpt-4o', name: 'GPT-4o' },
67
+ { id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini' },
68
+ { id: 'google/gemini-pro', name: 'Gemini Pro' },
69
+ { id: 'meta-llama/llama-3.1-8b-instruct', name: 'Llama 3.1 8B' },
70
+ ])
71
+ } finally {
72
+ setLoading(false)
73
+ }
74
+ }
75
+
76
+ fetchModels()
77
+ }, [])
78
+
79
+ return { models, loading, error }
80
80
  }
package/package.json CHANGED
@@ -1,66 +1,67 @@
1
- {
2
- "name": "asasvirtuais",
3
- "type": "module",
4
- "version": "0.4.4",
5
- "directories": {
6
- "packages": "./packages"
7
- },
8
- "exports": {
9
- "./package.json": "./package.json",
10
- "./*": "./packages/*",
11
- "./*/*": "./packages/*/*"
12
- },
13
- "scripts": {
14
- "code": "tmux new-session -d 'termux-wake-lock && sh dev-tunnel.sh'",
15
- "dev": "next dev --port 3000 --hostname 0.0.0.0",
16
- "build": "next build",
17
- "start": "next start",
18
- "lint": "next lint",
19
- "claude": "claude --dangerously-skip-permissions --mcp-config ./mcp.json",
20
- "gemini": "gemini --yolo --all_files",
21
- "env:pull": "vercel env pull .env"
22
- },
23
- "dependencies": {
24
- "@ai-sdk/react": "^2.0.59",
25
- "@asasvirtuais/airtable": "github:asasvirtuais/airtable#main",
26
- "@asasvirtuais/chat": "github:asasvirtuais/chat#main",
27
- "@asasvirtuais/crud": "github:asasvirtuais/crud#main",
28
- "@asasvirtuais/react": "github:asasvirtuais/react#main",
29
- "@auth0/nextjs-auth0": "^4.10.0",
30
- "@chakra-ui/react": "^3.21.0",
31
- "@emotion/react": "^11.14.0",
32
- "@google-cloud/storage": "^7.16.0",
33
- "@openrouter/ai-sdk-provider": "^1.0.0-beta.1",
34
- "ai": "^5.0.49",
35
- "asasvirtuais": "^0.2.0",
36
- "date-fns": "^4.1.0",
37
- "firebase": "^12.3.0",
38
- "firebase-admin": "^13.5.0",
39
- "google-auth-library": "^10.1.0",
40
- "googleapis": "^153.0.0",
41
- "jszip": "^3.10.1",
42
- "knex": "^3.1.0",
43
- "next": "15.6.0-canary.34",
44
- "next-themes": "^0.4.6",
45
- "openai": "^5.9.0",
46
- "react": "^19.0.0",
47
- "react-dom": "^19.0.0",
48
- "react-icons": "^5.5.0",
49
- "react-markdown": "^10.1.0",
50
- "react-use": "^17.6.0",
51
- "remark-breaks": "^4.0.0",
52
- "remark-gfm": "^4.0.1",
53
- "search-params": "^4.0.1",
54
- "wretch": "^2.11.0",
55
- "zod": "^3.25.76"
56
- },
57
- "devDependencies": {
58
- "@anthropic-ai/claude-code": "^1.0.60",
59
- "@google/gemini-cli": "^0.1.9",
60
- "@types/node": "^20",
61
- "@types/react": "^19",
62
- "@types/react-dom": "^19",
63
- "typescript": "^5",
64
- "vercel": "^44.3.0"
65
- }
66
- }
1
+ {
2
+ "name": "asasvirtuais",
3
+ "type": "module",
4
+ "version": "0.5.0",
5
+ "directories": {
6
+ "packages": "./packages"
7
+ },
8
+ "exports": {
9
+ "./package.json": "./package.json",
10
+ "./*": "./packages/*",
11
+ "./*/*": "./packages/*/*"
12
+ },
13
+ "scripts": {
14
+ "code": "tmux new-session -d 'termux-wake-lock && sh dev-tunnel.sh'",
15
+ "dev": "next dev --port 3000 --hostname 0.0.0.0",
16
+ "build": "next build",
17
+ "start": "next start",
18
+ "lint": "next lint",
19
+ "claude": "claude --dangerously-skip-permissions --mcp-config ./mcp.json",
20
+ "gemini": "gemini --yolo --all-files",
21
+ "env:pull": "vercel env pull .env"
22
+ },
23
+ "dependencies": {
24
+ "@ai-sdk/react": "^2.0.60",
25
+ "@asasvirtuais/airtable": "github:asasvirtuais/airtable#main",
26
+ "@asasvirtuais/chat": "github:asasvirtuais/chat#main",
27
+ "@asasvirtuais/crud": "github:asasvirtuais/crud#main",
28
+ "@asasvirtuais/react": "github:asasvirtuais/react#main",
29
+ "@auth0/nextjs-auth0": "^4.10.0",
30
+ "@chakra-ui/react": "^3.27.0",
31
+ "@emotion/react": "^11.14.0",
32
+ "@google-cloud/storage": "^7.17.1",
33
+ "@openrouter/ai-sdk-provider": "^1.2.0",
34
+ "ai": "^5.0.60",
35
+ "asasvirtuais": "^0.4.4",
36
+ "date-fns": "^4.1.0",
37
+ "dexie": "^4.2.0",
38
+ "firebase": "^12.3.0",
39
+ "firebase-admin": "^13.5.0",
40
+ "google-auth-library": "^10.4.0",
41
+ "googleapis": "^161.0.0",
42
+ "jszip": "^3.10.1",
43
+ "knex": "^3.1.0",
44
+ "next": "15.6.0-canary.34",
45
+ "next-themes": "^0.4.6",
46
+ "openai": "^6.1.0",
47
+ "react": "^19.2.0",
48
+ "react-dom": "^19.2.0",
49
+ "react-icons": "^5.5.0",
50
+ "react-markdown": "^10.1.0",
51
+ "react-use": "^17.6.0",
52
+ "remark-breaks": "^4.0.0",
53
+ "remark-gfm": "^4.0.1",
54
+ "search-params": "^4.0.1",
55
+ "wretch": "^2.11.0",
56
+ "zod": "^4.1.11"
57
+ },
58
+ "devDependencies": {
59
+ "@anthropic-ai/claude-code": "^2.0.8",
60
+ "@google/gemini-cli": "^0.7.0",
61
+ "@types/node": "^24.6.2",
62
+ "@types/react": "^19.2.0",
63
+ "@types/react-dom": "^19.2.0",
64
+ "typescript": "^5.9.3",
65
+ "vercel": "^48.2.0"
66
+ }
67
+ }
@@ -0,0 +1,126 @@
1
+ import Dexie, { type Table } from 'dexie'
2
+ import { z } from 'zod'
3
+ import type { DatabaseInterface, TableInterface, Query } from 'asasvirtuais/interface'
4
+
5
+ /**
6
+ * Creates a TableInterface adapter for IndexedDB using Dexie.js.
7
+ * This allows the framework to be used in a client-side only context.
8
+ *
9
+ * @param dbName The name of the IndexedDB database.
10
+ * @param schema The Zod schema definition for the database tables.
11
+ * @returns A TableInterface implementation for Dexie.
12
+ */
13
+ export function dexieInterface<Schema extends DatabaseInterface>(
14
+ dbName: string,
15
+ schema: Schema
16
+ ): TableInterface<z.infer<Schema[keyof Schema]['readable']>, z.infer<Schema[keyof Schema]['writable']>> {
17
+
18
+ const db = new Dexie(dbName)
19
+
20
+ // Dynamically define the database schema for Dexie from the Zod schema.
21
+ // It marks 'id' as the primary key and indexes all other top-level readable fields.
22
+ const dexieSchema = Object.fromEntries(
23
+ Object.keys(schema).map(tableName => {
24
+ const fields = Object.keys(schema[tableName].readable.shape)
25
+ // 'id' is the primary key, the rest are indexed fields.
26
+ const indexedFields = fields.filter(f => f !== 'id').join(', ')
27
+ return [tableName, `id, ${indexedFields}`]
28
+ })
29
+ )
30
+
31
+ db.version(1).stores(dexieSchema)
32
+
33
+ type GenericReadable = z.infer<Schema[keyof Schema]['readable']>
34
+ type GenericWritable = z.infer<Schema[keyof Schema]['writable']>
35
+
36
+ return {
37
+ async find({ table, id }) {
38
+ if (!table) throw new Error('Table name must be provided.')
39
+ const result = await db.table(table).get(id)
40
+ if (!result) throw new Error(`Record with id ${id} not found in ${table}`)
41
+ return result
42
+ },
43
+
44
+ async list({ table, query }) {
45
+ if (!table) throw new Error('Table name must be provided.')
46
+ let collection = db.table(table).toCollection()
47
+
48
+ const { $limit, $skip, $sort, ...filters } = query ?? {}
49
+
50
+ // Handle filtering
51
+ if (Object.keys(filters).length > 0) {
52
+ collection = collection.filter(item => {
53
+ return Object.entries(filters).every(([key, value]) => {
54
+ const itemValue = item[key as keyof typeof item]
55
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
56
+ return Object.entries(value).every(([op, opValue]) => {
57
+ switch (op) {
58
+ case '$ne': return itemValue !== opValue
59
+ case '$in': return Array.isArray(opValue) && opValue.includes(itemValue)
60
+ case '$nin': return Array.isArray(opValue) && !opValue.includes(itemValue)
61
+ case '$lt': return itemValue < (opValue as number)
62
+ case '$lte': return itemValue <= (opValue as number)
63
+ case '$gt': return itemValue > (opValue as number)
64
+ case '$gte': return itemValue >= (opValue as number)
65
+ default: return true // Ignore unknown operators
66
+ }
67
+ })
68
+ } else {
69
+ return itemValue === value
70
+ }
71
+ })
72
+ })
73
+ }
74
+
75
+ // Handle sorting
76
+ if ($sort) {
77
+ const sortKey = Object.keys($sort)[0] as keyof GenericReadable
78
+ const direction = $sort[sortKey] === -1 ? 'desc' : 'asc'
79
+ if (direction === 'desc') {
80
+ collection = collection.reverse()
81
+ }
82
+ // @ts-expect-error
83
+ collection = await collection.sortBy(sortKey)
84
+ }
85
+
86
+ // Handle pagination
87
+ if ($skip) {
88
+ collection = collection.offset($skip)
89
+ }
90
+ if ($limit) {
91
+ collection = collection.limit($limit)
92
+ }
93
+
94
+ return collection.toArray()
95
+ },
96
+
97
+ async create({ table, data }) {
98
+ if (!table) throw new Error('Table name must be provided.')
99
+ // Use existing id or generate a new UUID
100
+ const id = (data as any).id || crypto.randomUUID()
101
+ const record = { ...data, id }
102
+ await db.table(table).add(record)
103
+ return record as GenericReadable
104
+ },
105
+
106
+ async update({ table, id, data }) {
107
+ if (!table) throw new Error('Table name must be provided.')
108
+ const updatedCount = await db.table(table).update(id, data)
109
+ if (updatedCount === 0) {
110
+ throw new Error(`Record with id ${id} not found in ${table}, cannot update.`)
111
+ }
112
+ const result = await this.find({ table, id })
113
+ return result as GenericReadable
114
+ },
115
+
116
+ async remove({ table, id }) {
117
+ if (!table) throw new Error('Table name must be provided.')
118
+ const record = await this.find({ table, id })
119
+ if (!record) {
120
+ throw new Error(`Record with id ${id} not found in ${table}, cannot remove.`)
121
+ }
122
+ await db.table(table).delete(id)
123
+ return record as GenericReadable
124
+ },
125
+ } as TableInterface<GenericReadable, GenericWritable>
126
+ }
package/tsconfig.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "incremental": true,
16
16
  "target": "ESNext",
17
17
  "module": "ESNext",
18
- "moduleResolution": "node",
18
+ "moduleResolution": "bundler",
19
19
  "skipLibCheck": true,
20
20
  "plugins": [
21
21
  {