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 +421 -222
- package/hooks/useOpenRouterModels.ts +79 -79
- package/package.json +67 -66
- package/packages/dexie.ts +126 -0
- package/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -1,222 +1,421 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
</
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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.
|
|
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 --
|
|
21
|
-
"env:pull": "vercel env pull .env"
|
|
22
|
-
},
|
|
23
|
-
"dependencies": {
|
|
24
|
-
"@ai-sdk/react": "^2.0.
|
|
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.
|
|
31
|
-
"@emotion/react": "^11.14.0",
|
|
32
|
-
"@google-cloud/storage": "^7.
|
|
33
|
-
"@openrouter/ai-sdk-provider": "^1.
|
|
34
|
-
"ai": "^5.0.
|
|
35
|
-
"asasvirtuais": "^0.
|
|
36
|
-
"date-fns": "^4.1.0",
|
|
37
|
-
"
|
|
38
|
-
"firebase
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"next
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"react
|
|
48
|
-
"react-
|
|
49
|
-
"react-
|
|
50
|
-
"react-
|
|
51
|
-
"
|
|
52
|
-
"remark-
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"@
|
|
60
|
-
"@
|
|
61
|
-
"@types/
|
|
62
|
-
"@types/react
|
|
63
|
-
"
|
|
64
|
-
"
|
|
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
|
+
}
|