asasvirtuais 2.1.5 → 2.2.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 +52 -34
- package/package.json +15 -2
- package/packages/fetch-interface.tsx +11 -1
- package/packages/indexed-interface.tsx +139 -0
- package/packages/interface-provider.tsx +11 -0
- package/packages/mem-interface.tsx +104 -0
- package/packages/react-interface.tsx +7 -2
package/README.md
CHANGED
|
@@ -228,37 +228,42 @@ export const userSchema = {
|
|
|
228
228
|
}
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
-
#### 2.
|
|
231
|
+
#### 2. Provide the Interface Context
|
|
232
232
|
|
|
233
|
-
```
|
|
234
|
-
// app/
|
|
235
|
-
import {
|
|
233
|
+
```tsx
|
|
234
|
+
// app/layout.tsx
|
|
235
|
+
import { InterfaceProvider } from 'asasvirtuais/fetch-interface'
|
|
236
|
+
import { DatabaseProvider, TableProvider } from 'asasvirtuais/react-interface'
|
|
236
237
|
import { todoSchema } from './database'
|
|
237
238
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
239
|
+
export default function RootLayout({ children }) {
|
|
240
|
+
return (
|
|
241
|
+
<DatabaseProvider>
|
|
242
|
+
<InterfaceProvider schema={todoSchema} baseUrl='/api/v1'>
|
|
243
|
+
<TodosProvider>
|
|
244
|
+
{children}
|
|
245
|
+
</TodosProvider>
|
|
246
|
+
</InterfaceProvider>
|
|
247
|
+
</DatabaseProvider>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
244
250
|
```
|
|
245
251
|
|
|
246
|
-
#### 3.
|
|
252
|
+
#### 3. Create Your Table Provider
|
|
247
253
|
|
|
248
254
|
```tsx
|
|
249
|
-
// app/todos/
|
|
250
|
-
|
|
251
|
-
import {
|
|
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'
|
|
252
260
|
|
|
253
|
-
export
|
|
254
|
-
const initialTodos = await fetchTodos()
|
|
255
|
-
|
|
261
|
+
export function TodosProvider({ children }) {
|
|
256
262
|
return (
|
|
257
263
|
<TableProvider
|
|
258
|
-
table=
|
|
264
|
+
table='todos'
|
|
259
265
|
schema={todoSchema}
|
|
260
|
-
interface={
|
|
261
|
-
asAbove={initialTodos}
|
|
266
|
+
interface={useInterface()}
|
|
262
267
|
>
|
|
263
268
|
{children}
|
|
264
269
|
</TableProvider>
|
|
@@ -802,28 +807,24 @@ export function Filter[Models]() {
|
|
|
802
807
|
|
|
803
808
|
## 4. Provider and Hooks (`table.tsx`)
|
|
804
809
|
|
|
805
|
-
Set up the data provider and custom hooks
|
|
810
|
+
Set up the data provider and custom hooks using `InterfaceProvider` and `useInterface`:
|
|
806
811
|
|
|
807
812
|
```typescript
|
|
808
813
|
'use client'
|
|
809
814
|
import { TableProvider, useTableInterface } from 'asasvirtuais/react-interface'
|
|
810
|
-
import {
|
|
815
|
+
import { useInterface } from 'asasvirtuais/fetch-interface'
|
|
811
816
|
import { schema } from '.'
|
|
812
817
|
|
|
813
818
|
export function use[Models]() {
|
|
814
|
-
return useTableInterface
|
|
819
|
+
return useTableInterface('tableName', schema)
|
|
815
820
|
}
|
|
816
821
|
|
|
817
822
|
export function [Models]Provider({ children }: { children: React.ReactNode }) {
|
|
818
823
|
return (
|
|
819
|
-
<TableProvider
|
|
820
|
-
table='tableName'
|
|
821
|
-
schema={schema}
|
|
822
|
-
interface={
|
|
823
|
-
schema,
|
|
824
|
-
baseUrl: '/api/v1',
|
|
825
|
-
defaultTable: 'tableName'
|
|
826
|
-
})}
|
|
824
|
+
<TableProvider
|
|
825
|
+
table='tableName'
|
|
826
|
+
schema={schema}
|
|
827
|
+
interface={useInterface()}
|
|
827
828
|
>
|
|
828
829
|
{children}
|
|
829
830
|
</TableProvider>
|
|
@@ -831,11 +832,28 @@ export function [Models]Provider({ children }: { children: React.ReactNode }) {
|
|
|
831
832
|
}
|
|
832
833
|
```
|
|
833
834
|
|
|
835
|
+
Then in your layout, wrap with `InterfaceProvider` to set up the context:
|
|
836
|
+
|
|
837
|
+
```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
|
+
|
|
834
852
|
**Key Points:**
|
|
835
|
-
- Mark as `'use client'` for Next.js
|
|
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`
|
|
836
856
|
- Create a custom hook for easy access to table interface
|
|
837
|
-
- Provide `fetchInterface` configuration with baseUrl and table name
|
|
838
|
-
- Wrap your app/components with the Provider to enable data access
|
|
839
857
|
|
|
840
858
|
## Naming Conventions
|
|
841
859
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asasvirtuais",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"description": "React form and action management utilities",
|
|
6
6
|
"directories": {
|
|
7
7
|
"packages": "./packages"
|
|
@@ -46,6 +46,18 @@
|
|
|
46
46
|
"./fetch-interface": {
|
|
47
47
|
"types": "./packages/fetch-interface.tsx",
|
|
48
48
|
"default": "./packages/fetch-interface.tsx"
|
|
49
|
+
},
|
|
50
|
+
"./interface-provider": {
|
|
51
|
+
"types": "./packages/interface-provider.tsx",
|
|
52
|
+
"default": "./packages/interface-provider.tsx"
|
|
53
|
+
},
|
|
54
|
+
"./mem-interface": {
|
|
55
|
+
"types": "./packages/mem-interface.tsx",
|
|
56
|
+
"default": "./packages/mem-interface.tsx"
|
|
57
|
+
},
|
|
58
|
+
"./indexed-interface": {
|
|
59
|
+
"types": "./packages/indexed-interface.tsx",
|
|
60
|
+
"default": "./packages/indexed-interface.tsx"
|
|
49
61
|
}
|
|
50
62
|
},
|
|
51
63
|
"peerDependencies": {
|
|
@@ -53,6 +65,7 @@
|
|
|
53
65
|
},
|
|
54
66
|
"dependencies": {
|
|
55
67
|
"@chakra-ui/react": "^3.30.0",
|
|
68
|
+
"dexie": "^4.3.0",
|
|
56
69
|
"next": "^16.1.1",
|
|
57
70
|
"next-themes": "^0.4.6",
|
|
58
71
|
"react-icons": "^5.5.0",
|
|
@@ -64,4 +77,4 @@
|
|
|
64
77
|
"@types/react-dom": "^19.2.3",
|
|
65
78
|
"typescript": "^5.9.3"
|
|
66
79
|
}
|
|
67
|
-
}
|
|
80
|
+
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
'use client'
|
|
1
2
|
import { z } from 'zod'
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
import { createContextFromHook } from './hooks'
|
|
2
5
|
import type { TableInterface, TableSchema } from './interface'
|
|
3
6
|
|
|
4
|
-
export
|
|
7
|
+
export function useFetchInterfaceProvider<TSchema extends TableSchema>({
|
|
8
|
+
schema, baseUrl = '/api/v1', headers = {},
|
|
9
|
+
}: {
|
|
10
|
+
schema: TSchema
|
|
5
11
|
baseUrl?: string
|
|
6
12
|
headers?: Record<string, string>
|
|
13
|
+
}) {
|
|
14
|
+
return useMemo(() => fetchInterface({ schema, baseUrl, headers }), [schema, baseUrl])
|
|
7
15
|
}
|
|
8
16
|
|
|
17
|
+
export const [FetchInterfaceProvider, useFetchInterface] = createContextFromHook(useFetchInterfaceProvider<any>)
|
|
18
|
+
|
|
9
19
|
export function fetchInterface<Schema extends TableSchema, Table extends string>({
|
|
10
20
|
schema, defaultTable, baseUrl = '/api/v1', headers = {}
|
|
11
21
|
}: {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import Dexie from 'dexie'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { useMemo } from 'react'
|
|
5
|
+
import { createContextFromHook } from './hooks'
|
|
6
|
+
import type { DatabaseSchema, TableInterface } from './interface'
|
|
7
|
+
|
|
8
|
+
export function useIndexedInterfaceProvider<Schema extends DatabaseSchema>({
|
|
9
|
+
dbName, schema,
|
|
10
|
+
}: {
|
|
11
|
+
dbName: string
|
|
12
|
+
schema: Schema
|
|
13
|
+
}) {
|
|
14
|
+
return useMemo(() => indexedInterface(dbName, schema), [dbName, schema])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const [IndexedInterfaceProvider, useIndexedInterface] = createContextFromHook(useIndexedInterfaceProvider<any>)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a TableInterface adapter for IndexedDB using Dexie.js.
|
|
21
|
+
* This allows the framework to be used in a client-side only context.
|
|
22
|
+
*
|
|
23
|
+
* @param dbName The name of the IndexedDB database.
|
|
24
|
+
* @param schema The Zod schema definition for the database tables.
|
|
25
|
+
* @returns A TableInterface implementation for Dexie.
|
|
26
|
+
*/
|
|
27
|
+
export function indexedInterface<Schema extends DatabaseSchema>(
|
|
28
|
+
dbName: string,
|
|
29
|
+
schema: Schema
|
|
30
|
+
): TableInterface<z.infer<Schema[keyof Schema]['readable']>, z.infer<Schema[keyof Schema]['writable']>> {
|
|
31
|
+
|
|
32
|
+
const db = new Dexie(dbName)
|
|
33
|
+
|
|
34
|
+
// Dynamically define the database schema for Dexie from the Zod schema.
|
|
35
|
+
// It marks 'id' as the primary key and indexes all other top-level readable fields.
|
|
36
|
+
const dexieSchema = Object.fromEntries(
|
|
37
|
+
Object.keys(schema).map(tableName => {
|
|
38
|
+
const fields = Object.keys(schema[tableName].readable.shape)
|
|
39
|
+
// 'id' is the primary key, the rest are indexed fields.
|
|
40
|
+
const indexedFields = fields.filter(f => f !== 'id').join(', ')
|
|
41
|
+
return [tableName, `id, ${indexedFields}`]
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
db.version(1).stores(dexieSchema)
|
|
46
|
+
|
|
47
|
+
type GenericReadable = z.infer<Schema[keyof Schema]['readable']>
|
|
48
|
+
type GenericWritable = z.infer<Schema[keyof Schema]['writable']>
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
async find({ table, id }) {
|
|
52
|
+
if (!table) throw new Error('Table name must be provided.')
|
|
53
|
+
const result = await db.table(table).get(id)
|
|
54
|
+
if (!result) throw new Error(`Record with id ${id} not found in ${table}`)
|
|
55
|
+
return result
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async list({ table, query }) {
|
|
59
|
+
if (!table) throw new Error('Table name must be provided.')
|
|
60
|
+
let collection = db.table(table).toCollection()
|
|
61
|
+
|
|
62
|
+
const { $limit, $skip, $sort, ...filters } = query ?? {}
|
|
63
|
+
|
|
64
|
+
// Handle filtering
|
|
65
|
+
if (Object.keys(filters).length > 0) {
|
|
66
|
+
collection = collection.filter(item => {
|
|
67
|
+
return Object.entries(filters).every(([key, value]) => {
|
|
68
|
+
const itemValue = item[key as keyof typeof item]
|
|
69
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
70
|
+
return Object.entries(value).every(([op, opValue]) => {
|
|
71
|
+
switch (op) {
|
|
72
|
+
case '$ne': return itemValue !== opValue
|
|
73
|
+
case '$in': return Array.isArray(opValue) && opValue.includes(itemValue)
|
|
74
|
+
case '$nin': return Array.isArray(opValue) && !opValue.includes(itemValue)
|
|
75
|
+
case '$lt': return itemValue < (opValue as number)
|
|
76
|
+
case '$lte': return itemValue <= (opValue as number)
|
|
77
|
+
case '$gt': return itemValue > (opValue as number)
|
|
78
|
+
case '$gte': return itemValue >= (opValue as number)
|
|
79
|
+
default: return true // Ignore unknown operators
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
} else {
|
|
83
|
+
return itemValue === value
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Handle sorting
|
|
90
|
+
if ($sort) {
|
|
91
|
+
const sortKey = Object.keys($sort)[0] as keyof GenericReadable
|
|
92
|
+
const direction = $sort[sortKey] === -1 ? 'desc' : 'asc'
|
|
93
|
+
if (direction === 'desc')
|
|
94
|
+
collection = collection.reverse()
|
|
95
|
+
// @ts-expect-error
|
|
96
|
+
collection = await collection.sortBy(sortKey)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle pagination
|
|
100
|
+
if ($skip) {
|
|
101
|
+
collection = collection.offset($skip)
|
|
102
|
+
}
|
|
103
|
+
if ($limit) {
|
|
104
|
+
collection = collection.limit($limit)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return collection.toArray()
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async create({ table, data }) {
|
|
111
|
+
if (!table) throw new Error('Table name must be provided.')
|
|
112
|
+
// Use existing id or generate a new UUID
|
|
113
|
+
const id = (data as any).id || crypto.randomUUID()
|
|
114
|
+
const record = { ...data, id }
|
|
115
|
+
await db.table(table).add(record)
|
|
116
|
+
return record as GenericReadable
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async update({ table, id, data }) {
|
|
120
|
+
if (!table) throw new Error('Table name must be provided.')
|
|
121
|
+
const updatedCount = await db.table(table).update(id, data)
|
|
122
|
+
if (updatedCount === 0) {
|
|
123
|
+
throw new Error(`Record with id ${id} not found in ${table}, cannot update.`)
|
|
124
|
+
}
|
|
125
|
+
const result = await this.find({ table, id })
|
|
126
|
+
return result as GenericReadable
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async remove({ table, id }) {
|
|
130
|
+
if (!table) throw new Error('Table name must be provided.')
|
|
131
|
+
const record = await this.find({ table, id })
|
|
132
|
+
if (!record) {
|
|
133
|
+
throw new Error(`Record with id ${id} not found in ${table}, cannot remove.`)
|
|
134
|
+
}
|
|
135
|
+
await db.table(table).delete(id)
|
|
136
|
+
return record as GenericReadable
|
|
137
|
+
},
|
|
138
|
+
} as TableInterface<GenericReadable, GenericWritable>
|
|
139
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { createContextFromHook } from './hooks'
|
|
3
|
+
import type { TableInterface } from './interface'
|
|
4
|
+
|
|
5
|
+
function useInterfaceProvider({ interface: tableInterface }: {
|
|
6
|
+
interface: TableInterface<any, any>
|
|
7
|
+
}) {
|
|
8
|
+
return tableInterface
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const [InterfaceProvider, useInterface] = createContextFromHook(useInterfaceProvider)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { createContextFromHook } from './hooks'
|
|
4
|
+
import type { TableInterface, TableSchema } from './interface'
|
|
5
|
+
|
|
6
|
+
export function useMemInterfaceProvider<TSchema extends TableSchema>({
|
|
7
|
+
schema,
|
|
8
|
+
}: {
|
|
9
|
+
schema: TSchema
|
|
10
|
+
}) {
|
|
11
|
+
return useMemo(() => memInterface<TSchema>(), [schema])
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const [MemInterfaceProvider, useMemInterface] = createContextFromHook(useMemInterfaceProvider<any>)
|
|
15
|
+
|
|
16
|
+
export function memInterface<TSchema extends TableSchema>(): TableInterface<any, any> {
|
|
17
|
+
|
|
18
|
+
const store: Record<string, Record<string, any>> = {}
|
|
19
|
+
|
|
20
|
+
function getTable(table?: string) {
|
|
21
|
+
if (!table) throw new Error('Table name must be provided.')
|
|
22
|
+
if (!store[table]) store[table] = {}
|
|
23
|
+
return store[table]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
async find({ table, id }) {
|
|
28
|
+
const t = getTable(table)
|
|
29
|
+
const record = t[id]
|
|
30
|
+
if (!record) throw new Error(`Record with id ${id} not found in ${table}`)
|
|
31
|
+
return record
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async create({ table, data }) {
|
|
35
|
+
const t = getTable(table)
|
|
36
|
+
const id = (data as any).id || crypto.randomUUID()
|
|
37
|
+
const record = { ...data, id }
|
|
38
|
+
t[id] = record
|
|
39
|
+
return record
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async update({ table, id, data }) {
|
|
43
|
+
const t = getTable(table)
|
|
44
|
+
if (!t[id]) throw new Error(`Record with id ${id} not found in ${table}, cannot update.`)
|
|
45
|
+
t[id] = { ...t[id], ...data }
|
|
46
|
+
return t[id]
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async remove({ table, id }) {
|
|
50
|
+
const t = getTable(table)
|
|
51
|
+
const record = t[id]
|
|
52
|
+
if (!record) throw new Error(`Record with id ${id} not found in ${table}, cannot remove.`)
|
|
53
|
+
delete t[id]
|
|
54
|
+
return record
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async list({ table, query }) {
|
|
58
|
+
const t = getTable(table)
|
|
59
|
+
let results = Object.values(t)
|
|
60
|
+
|
|
61
|
+
if (query) {
|
|
62
|
+
const { $limit, $skip, $sort, ...filters } = query
|
|
63
|
+
|
|
64
|
+
if (Object.keys(filters).length > 0) {
|
|
65
|
+
results = results.filter(item =>
|
|
66
|
+
Object.entries(filters).every(([key, value]) => {
|
|
67
|
+
const itemValue = item[key]
|
|
68
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
69
|
+
return Object.entries(value).every(([op, opValue]) => {
|
|
70
|
+
switch (op) {
|
|
71
|
+
case '$ne': return itemValue !== opValue
|
|
72
|
+
case '$in': return Array.isArray(opValue) && opValue.includes(itemValue)
|
|
73
|
+
case '$nin': return Array.isArray(opValue) && !opValue.includes(itemValue)
|
|
74
|
+
case '$lt': return itemValue < (opValue as number)
|
|
75
|
+
case '$lte': return itemValue <= (opValue as number)
|
|
76
|
+
case '$gt': return itemValue > (opValue as number)
|
|
77
|
+
case '$gte': return itemValue >= (opValue as number)
|
|
78
|
+
default: return true
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
return itemValue === value
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if ($sort) {
|
|
88
|
+
const sortKey = Object.keys($sort)[0]
|
|
89
|
+
const direction = ($sort as any)[sortKey]
|
|
90
|
+
results.sort((a, b) => {
|
|
91
|
+
if (a[sortKey] < b[sortKey]) return direction === -1 ? 1 : -1
|
|
92
|
+
if (a[sortKey] > b[sortKey]) return direction === -1 ? -1 : 1
|
|
93
|
+
return 0
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ($skip) results = results.slice($skip)
|
|
98
|
+
if ($limit) results = results.slice(0, $limit)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -175,8 +175,13 @@ export function TableProvider<TSchema extends TableSchema>({children, ...props}:
|
|
|
175
175
|
)
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
export function useTable<TSchema extends TableSchema>() {
|
|
179
|
-
|
|
178
|
+
export function useTable<TSchema extends TableSchema>(table: string, schema: TSchema) {
|
|
179
|
+
const methods = useTableInterface(table, schema)
|
|
180
|
+
const index = useTableIndex(table, schema)
|
|
181
|
+
return {
|
|
182
|
+
...methods,
|
|
183
|
+
...index,
|
|
184
|
+
}
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
export function CreateForm<TSchema extends TableSchema>({ table, schema, defaults, onSuccess, children }: {
|