@mdxui/terminal 2.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 +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- package/src/types.ts +103 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Data Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for reactive queries and mutations with optimistic updates.
|
|
5
|
+
* Provides type-safe hooks for querying and mutating database collections
|
|
6
|
+
* within a DBProvider context.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
10
|
+
import { useDBContext } from './context'
|
|
11
|
+
import type {
|
|
12
|
+
QueryOptions,
|
|
13
|
+
QueryResult,
|
|
14
|
+
MutationOptions,
|
|
15
|
+
MutationResult,
|
|
16
|
+
WhereClause,
|
|
17
|
+
OrderByClause,
|
|
18
|
+
UpdateMutationData,
|
|
19
|
+
DeleteMutationData,
|
|
20
|
+
} from './types'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a value matches a field filter
|
|
24
|
+
*/
|
|
25
|
+
function matchesFilter<T>(value: T, filter: any): boolean {
|
|
26
|
+
if (filter === null || filter === undefined || typeof filter !== 'object') {
|
|
27
|
+
return value === filter
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ops = filter as Record<string, any>
|
|
31
|
+
|
|
32
|
+
if ('$eq' in ops && value !== ops.$eq) return false
|
|
33
|
+
if ('$ne' in ops && value === ops.$ne) return false
|
|
34
|
+
if ('$gt' in ops && !(value > ops.$gt)) return false
|
|
35
|
+
if ('$gte' in ops && !(value >= ops.$gte)) return false
|
|
36
|
+
if ('$lt' in ops && !(value < ops.$lt)) return false
|
|
37
|
+
if ('$lte' in ops && !(value <= ops.$lte)) return false
|
|
38
|
+
if ('$in' in ops && !ops.$in.includes(value)) return false
|
|
39
|
+
if ('$nin' in ops && ops.$nin.includes(value)) return false
|
|
40
|
+
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a document matches a where clause
|
|
46
|
+
*/
|
|
47
|
+
function matchesWhere<T extends Record<string, any>>(doc: T, where: WhereClause<T>): boolean {
|
|
48
|
+
if ('$or' in where && where.$or) {
|
|
49
|
+
const orClauses = where.$or as WhereClause<T>[]
|
|
50
|
+
const matchesOr = orClauses.some(clause => matchesWhere(doc, clause))
|
|
51
|
+
if (!matchesOr) return false
|
|
52
|
+
|
|
53
|
+
const { $or, ...rest } = where
|
|
54
|
+
if (Object.keys(rest).length === 0) return true
|
|
55
|
+
return matchesWhere(doc, rest as WhereClause<T>)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const [key, filter] of Object.entries(where)) {
|
|
59
|
+
if (key === '$or') continue
|
|
60
|
+
const value = doc[key]
|
|
61
|
+
if (!matchesFilter(value, filter)) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Sort documents by orderBy clause
|
|
71
|
+
*/
|
|
72
|
+
function sortDocuments<T extends Record<string, any>>(
|
|
73
|
+
docs: T[],
|
|
74
|
+
orderBy: OrderByClause<T> | OrderByClause<T>[]
|
|
75
|
+
): T[] {
|
|
76
|
+
const orderClauses = Array.isArray(orderBy) ? orderBy : [orderBy]
|
|
77
|
+
|
|
78
|
+
return [...docs].sort((a, b) => {
|
|
79
|
+
for (const clause of orderClauses) {
|
|
80
|
+
const [field, direction] = Object.entries(clause)[0]
|
|
81
|
+
const aVal = a[field]
|
|
82
|
+
const bVal = b[field]
|
|
83
|
+
|
|
84
|
+
if (aVal === null || aVal === undefined) {
|
|
85
|
+
if (bVal !== null && bVal !== undefined) return 1
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
if (bVal === null || bVal === undefined) return -1
|
|
89
|
+
|
|
90
|
+
let comparison = 0
|
|
91
|
+
if (typeof aVal === 'string') {
|
|
92
|
+
comparison = aVal.localeCompare(bVal)
|
|
93
|
+
} else if (typeof aVal === 'number') {
|
|
94
|
+
comparison = aVal - bVal
|
|
95
|
+
} else if (aVal instanceof Date) {
|
|
96
|
+
comparison = aVal.getTime() - bVal.getTime()
|
|
97
|
+
} else {
|
|
98
|
+
comparison = String(aVal).localeCompare(String(bVal))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (comparison !== 0) {
|
|
102
|
+
return direction === 'desc' ? -comparison : comparison
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return 0
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* React hook for reactive data queries with filtering, sorting, and pagination
|
|
111
|
+
*
|
|
112
|
+
* @template T - The document type being queried
|
|
113
|
+
*
|
|
114
|
+
* @param options - Query options (collection name, filters, sorting, pagination)
|
|
115
|
+
* @returns Query result with reactive data, loading/error states, and refetch control
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* - Must be used within a DBProvider component
|
|
119
|
+
* - Automatically fetches initial data on mount
|
|
120
|
+
* - Reactively updates when collection changes (subscriptions)
|
|
121
|
+
* - Filters, sorting, and pagination are re-applied when data changes
|
|
122
|
+
* - `data` is undefined while loading, array when loaded
|
|
123
|
+
* - `isLoading` is true for the initial fetch only
|
|
124
|
+
* - Manual refetch available via `refetch()` function
|
|
125
|
+
* - Error state persists until query succeeds
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```tsx
|
|
129
|
+
* // Basic query - get all documents
|
|
130
|
+
* function AllUsers() {
|
|
131
|
+
* const { data, isLoading, error } = useQuery({
|
|
132
|
+
* from: 'users'
|
|
133
|
+
* })
|
|
134
|
+
*
|
|
135
|
+
* if (isLoading) return <div>Loading...</div>
|
|
136
|
+
* if (error) return <div>Error: {error.message}</div>
|
|
137
|
+
*
|
|
138
|
+
* return (
|
|
139
|
+
* <ul>
|
|
140
|
+
* {data?.map(user => (
|
|
141
|
+
* <li key={user.id}>{user.name}</li>
|
|
142
|
+
* ))}
|
|
143
|
+
* </ul>
|
|
144
|
+
* )
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```tsx
|
|
150
|
+
* // With filtering
|
|
151
|
+
* function AdminUsers() {
|
|
152
|
+
* const { data } = useQuery({
|
|
153
|
+
* from: 'users',
|
|
154
|
+
* where: { role: 'admin' }
|
|
155
|
+
* })
|
|
156
|
+
*
|
|
157
|
+
* return data?.map(user => <AdminCard key={user.id} user={user} />)
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* // With sorting and pagination
|
|
164
|
+
* function UsersPaginated() {
|
|
165
|
+
* const { data, refetch } = useQuery({
|
|
166
|
+
* from: 'users',
|
|
167
|
+
* where: { status: 'active' },
|
|
168
|
+
* orderBy: { createdAt: 'desc' },
|
|
169
|
+
* limit: 20,
|
|
170
|
+
* offset: 0
|
|
171
|
+
* })
|
|
172
|
+
*
|
|
173
|
+
* return (
|
|
174
|
+
* <div>
|
|
175
|
+
* <Users data={data} />
|
|
176
|
+
* <button onClick={refetch}>Refresh</button>
|
|
177
|
+
* </div>
|
|
178
|
+
* )
|
|
179
|
+
* }
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```tsx
|
|
184
|
+
* // With comparison operators
|
|
185
|
+
* function AdultUsers() {
|
|
186
|
+
* const { data } = useQuery({
|
|
187
|
+
* from: 'users',
|
|
188
|
+
* where: { age: { $gte: 18 } }
|
|
189
|
+
* })
|
|
190
|
+
*
|
|
191
|
+
* return data?.map(user => <Card key={user.id} user={user} />)
|
|
192
|
+
* }
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```tsx
|
|
197
|
+
* // With OR conditions
|
|
198
|
+
* function AdminsOrModerators() {
|
|
199
|
+
* const { data } = useQuery({
|
|
200
|
+
* from: 'users',
|
|
201
|
+
* where: {
|
|
202
|
+
* $or: [
|
|
203
|
+
* { role: 'admin' },
|
|
204
|
+
* { role: 'moderator' }
|
|
205
|
+
* ]
|
|
206
|
+
* }
|
|
207
|
+
* })
|
|
208
|
+
*
|
|
209
|
+
* return data?.map(user => <Card key={user.id} user={user} />)
|
|
210
|
+
* }
|
|
211
|
+
* ```
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```tsx
|
|
215
|
+
* // With multiple sort fields
|
|
216
|
+
* function UsersSorted() {
|
|
217
|
+
* const { data } = useQuery({
|
|
218
|
+
* from: 'users',
|
|
219
|
+
* orderBy: [
|
|
220
|
+
* { role: 'asc' },
|
|
221
|
+
* { name: 'asc' }
|
|
222
|
+
* ]
|
|
223
|
+
* })
|
|
224
|
+
*
|
|
225
|
+
* return data?.map(user => <Card key={user.id} user={user} />)
|
|
226
|
+
* }
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export function useQuery<T extends Record<string, any>>(
|
|
230
|
+
options: QueryOptions<T>
|
|
231
|
+
): QueryResult<T> {
|
|
232
|
+
const db = useDBContext()
|
|
233
|
+
const [data, setData] = useState<T[] | undefined>(undefined)
|
|
234
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
235
|
+
const [error, setError] = useState<Error | undefined>(undefined)
|
|
236
|
+
|
|
237
|
+
const { from, where, orderBy, limit, offset } = options
|
|
238
|
+
|
|
239
|
+
// Get collection
|
|
240
|
+
const collection = db.collections[from]
|
|
241
|
+
|
|
242
|
+
const executeQuery = useCallback(async () => {
|
|
243
|
+
if (!collection) {
|
|
244
|
+
setError(new Error(`Collection '${from}' not found`))
|
|
245
|
+
setIsLoading(false)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const result = await collection.findMany({ where, orderBy, limit, offset })
|
|
251
|
+
setData(result)
|
|
252
|
+
setError(undefined)
|
|
253
|
+
} catch (err) {
|
|
254
|
+
setError(err instanceof Error ? err : new Error(String(err)))
|
|
255
|
+
} finally {
|
|
256
|
+
setIsLoading(false)
|
|
257
|
+
}
|
|
258
|
+
}, [collection, from, JSON.stringify(where), JSON.stringify(orderBy), limit, offset])
|
|
259
|
+
|
|
260
|
+
// Initial fetch
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
executeQuery()
|
|
263
|
+
}, [executeQuery])
|
|
264
|
+
|
|
265
|
+
// Subscribe to collection changes
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (!collection) return
|
|
268
|
+
|
|
269
|
+
const unsubscribe = collection.subscribe((allData: T[]) => {
|
|
270
|
+
// Re-apply filters when data changes
|
|
271
|
+
let result = allData
|
|
272
|
+
|
|
273
|
+
if (where) {
|
|
274
|
+
result = result.filter(doc => matchesWhere(doc, where))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (orderBy) {
|
|
278
|
+
result = sortDocuments(result, orderBy)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (offset !== undefined) {
|
|
282
|
+
result = result.slice(offset)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (limit !== undefined) {
|
|
286
|
+
result = result.slice(0, limit)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
setData(result)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
return unsubscribe
|
|
293
|
+
}, [collection, JSON.stringify(where), JSON.stringify(orderBy), limit, offset])
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
data,
|
|
297
|
+
isLoading,
|
|
298
|
+
error,
|
|
299
|
+
refetch: executeQuery,
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* React hook for data mutations (insert, update, delete) with optimistic updates
|
|
305
|
+
*
|
|
306
|
+
* @template T - The document type being mutated
|
|
307
|
+
*
|
|
308
|
+
* @param options - Mutation options (collection, operation, optimistic mode)
|
|
309
|
+
* @returns Mutation result with mutate function, pending/error states, and reset
|
|
310
|
+
*
|
|
311
|
+
* @remarks
|
|
312
|
+
* - Must be used within a DBProvider component
|
|
313
|
+
* - Supports three operations: insert, update, delete
|
|
314
|
+
* - Optimistic updates: UI updates immediately, rolls back on error
|
|
315
|
+
* - Manual updates: UI updates only after server confirmation
|
|
316
|
+
* - `isPending` is true while mutation is executing
|
|
317
|
+
* - Error state persists until mutation succeeds or `reset()` is called
|
|
318
|
+
* - Data format depends on operation type:
|
|
319
|
+
* - `insert`: T (full document)
|
|
320
|
+
* - `update`: UpdateMutationData<T> (where + data)
|
|
321
|
+
* - `delete`: DeleteMutationData<T> (id)
|
|
322
|
+
* - Sync adapter is called when optimistic=true and sync is configured
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```tsx
|
|
326
|
+
* // Insert with optimistic update
|
|
327
|
+
* function CreateUserForm() {
|
|
328
|
+
* const { mutate, isPending, error, reset } = useMutation({
|
|
329
|
+
* collection: 'users',
|
|
330
|
+
* operation: 'insert',
|
|
331
|
+
* optimistic: true
|
|
332
|
+
* })
|
|
333
|
+
*
|
|
334
|
+
* const handleSubmit = async (formData) => {
|
|
335
|
+
* try {
|
|
336
|
+
* await mutate({
|
|
337
|
+
* id: crypto.randomUUID(),
|
|
338
|
+
* name: formData.name,
|
|
339
|
+
* email: formData.email,
|
|
340
|
+
* role: 'user'
|
|
341
|
+
* })
|
|
342
|
+
* console.log('User created!')
|
|
343
|
+
* } catch (err) {
|
|
344
|
+
* console.error('Failed to create user')
|
|
345
|
+
* }
|
|
346
|
+
* }
|
|
347
|
+
*
|
|
348
|
+
* return (
|
|
349
|
+
* <form onSubmit={async (e) => {
|
|
350
|
+
* e.preventDefault()
|
|
351
|
+
* await handleSubmit(Object.fromEntries(new FormData(e.currentTarget)))
|
|
352
|
+
* }}>
|
|
353
|
+
* <input name="name" required />
|
|
354
|
+
* <input name="email" type="email" required />
|
|
355
|
+
* <button disabled={isPending}>
|
|
356
|
+
* {isPending ? 'Creating...' : 'Create'}
|
|
357
|
+
* </button>
|
|
358
|
+
* {error && (
|
|
359
|
+
* <div>
|
|
360
|
+
* <p>Error: {error.message}</p>
|
|
361
|
+
* <button onClick={reset}>Dismiss</button>
|
|
362
|
+
* </div>
|
|
363
|
+
* )}
|
|
364
|
+
* </form>
|
|
365
|
+
* )
|
|
366
|
+
* }
|
|
367
|
+
* ```
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* ```tsx
|
|
371
|
+
* // Update mutation
|
|
372
|
+
* function EditUserForm({ userId }) {
|
|
373
|
+
* const { mutate, isPending } = useMutation({
|
|
374
|
+
* collection: 'users',
|
|
375
|
+
* operation: 'update'
|
|
376
|
+
* })
|
|
377
|
+
*
|
|
378
|
+
* const handleSave = async (updates) => {
|
|
379
|
+
* await mutate({
|
|
380
|
+
* where: { id: userId },
|
|
381
|
+
* data: updates
|
|
382
|
+
* })
|
|
383
|
+
* }
|
|
384
|
+
*
|
|
385
|
+
* return <Form onSave={handleSave} disabled={isPending} />
|
|
386
|
+
* }
|
|
387
|
+
* ```
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```tsx
|
|
391
|
+
* // Delete mutation
|
|
392
|
+
* function DeleteUserButton({ userId }) {
|
|
393
|
+
* const { mutate, isPending } = useMutation({
|
|
394
|
+
* collection: 'users',
|
|
395
|
+
* operation: 'delete'
|
|
396
|
+
* })
|
|
397
|
+
*
|
|
398
|
+
* const handleDelete = async () => {
|
|
399
|
+
* if (confirm('Really delete?')) {
|
|
400
|
+
* await mutate({ id: userId })
|
|
401
|
+
* }
|
|
402
|
+
* }
|
|
403
|
+
*
|
|
404
|
+
* return (
|
|
405
|
+
* <button onClick={handleDelete} disabled={isPending}>
|
|
406
|
+
* {isPending ? 'Deleting...' : 'Delete'}
|
|
407
|
+
* </button>
|
|
408
|
+
* )
|
|
409
|
+
* }
|
|
410
|
+
* ```
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```tsx
|
|
414
|
+
* // Optimistic vs non-optimistic
|
|
415
|
+
* function UserActions() {
|
|
416
|
+
* // UI updates immediately, auto-rollback on error
|
|
417
|
+
* const { mutate: optimisticInsert } = useMutation({
|
|
418
|
+
* collection: 'users',
|
|
419
|
+
* operation: 'insert',
|
|
420
|
+
* optimistic: true // Default UI update
|
|
421
|
+
* })
|
|
422
|
+
*
|
|
423
|
+
* // UI updates only after server confirms
|
|
424
|
+
* const { mutate: carefullInsert } = useMutation({
|
|
425
|
+
* collection: 'users',
|
|
426
|
+
* operation: 'insert',
|
|
427
|
+
* optimistic: false // Wait for server
|
|
428
|
+
* })
|
|
429
|
+
*
|
|
430
|
+
* return (
|
|
431
|
+
* <div>
|
|
432
|
+
* <button onClick={() => optimisticInsert(newUser)}>
|
|
433
|
+
* Quick Add (optimistic)
|
|
434
|
+
* </button>
|
|
435
|
+
* <button onClick={() => carefullInsert(newUser)}>
|
|
436
|
+
* Safe Add (wait for server)
|
|
437
|
+
* </button>
|
|
438
|
+
* </div>
|
|
439
|
+
* )
|
|
440
|
+
* }
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
export function useMutation<T extends Record<string, any>>(
|
|
444
|
+
options: MutationOptions
|
|
445
|
+
): MutationResult<T> {
|
|
446
|
+
const db = useDBContext()
|
|
447
|
+
const [isPending, setIsPending] = useState(false)
|
|
448
|
+
const [error, setError] = useState<Error | undefined>(undefined)
|
|
449
|
+
|
|
450
|
+
const { collection: collectionName, operation, optimistic = false } = options
|
|
451
|
+
|
|
452
|
+
// Get collection
|
|
453
|
+
const collection = db.collections[collectionName]
|
|
454
|
+
|
|
455
|
+
// Store for rollback
|
|
456
|
+
const rollbackRef = useRef<(() => void) | null>(null)
|
|
457
|
+
|
|
458
|
+
const mutate = useCallback(
|
|
459
|
+
async (mutationData: T | UpdateMutationData<T> | DeleteMutationData<T>) => {
|
|
460
|
+
if (!collection) {
|
|
461
|
+
throw new Error(`Collection '${collectionName}' not found`)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
setIsPending(true)
|
|
465
|
+
setError(undefined)
|
|
466
|
+
|
|
467
|
+
// Optimistic update storage
|
|
468
|
+
let rollbackFn: (() => void) | null = null
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
if (operation === 'insert') {
|
|
472
|
+
const insertData = mutationData as T
|
|
473
|
+
|
|
474
|
+
if (optimistic) {
|
|
475
|
+
// Optimistically add to the local store immediately
|
|
476
|
+
// This triggers the subscription update
|
|
477
|
+
await collection.insert(insertData)
|
|
478
|
+
|
|
479
|
+
// If sync fails, we need to rollback
|
|
480
|
+
if (db.sync) {
|
|
481
|
+
try {
|
|
482
|
+
await db.sync.push([{ type: 'insert', collection: collectionName, data: insertData }])
|
|
483
|
+
} catch (syncError) {
|
|
484
|
+
// Rollback: delete the optimistically inserted data
|
|
485
|
+
const primaryKey = collection.primaryKey || 'id'
|
|
486
|
+
await collection.delete({ [primaryKey]: (insertData as any)[primaryKey] } as any)
|
|
487
|
+
throw syncError
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
await collection.insert(insertData)
|
|
492
|
+
if (db.sync) {
|
|
493
|
+
await db.sync.push([{ type: 'insert', collection: collectionName, data: insertData }])
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} else if (operation === 'update') {
|
|
497
|
+
const updateData = mutationData as UpdateMutationData<T>
|
|
498
|
+
await collection.update(updateData.where, updateData.data)
|
|
499
|
+
if (db.sync) {
|
|
500
|
+
await db.sync.push([{ type: 'update', collection: collectionName, data: updateData }])
|
|
501
|
+
}
|
|
502
|
+
} else if (operation === 'delete') {
|
|
503
|
+
const deleteData = mutationData as DeleteMutationData<T>
|
|
504
|
+
await collection.delete({ id: deleteData.id } as any)
|
|
505
|
+
if (db.sync) {
|
|
506
|
+
await db.sync.push([{ type: 'delete', collection: collectionName, data: deleteData }])
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} catch (err) {
|
|
510
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
511
|
+
setError(error)
|
|
512
|
+
throw error
|
|
513
|
+
} finally {
|
|
514
|
+
setIsPending(false)
|
|
515
|
+
rollbackRef.current = null
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
[collection, collectionName, operation, optimistic, db.sync]
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
const reset = useCallback(() => {
|
|
522
|
+
setIsPending(false)
|
|
523
|
+
setError(undefined)
|
|
524
|
+
}, [])
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
mutate,
|
|
528
|
+
isPending,
|
|
529
|
+
error,
|
|
530
|
+
reset,
|
|
531
|
+
}
|
|
532
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Data Layer
|
|
3
|
+
*
|
|
4
|
+
* TanStack DB-like integration for reactive data management.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import {
|
|
9
|
+
* createDB,
|
|
10
|
+
* createCollection,
|
|
11
|
+
* DBProvider,
|
|
12
|
+
* useQuery,
|
|
13
|
+
* useMutation,
|
|
14
|
+
* } from '@mdxui/terminal'
|
|
15
|
+
* import { z } from 'zod'
|
|
16
|
+
*
|
|
17
|
+
* // Define schema
|
|
18
|
+
* const UserSchema = z.object({
|
|
19
|
+
* id: z.string(),
|
|
20
|
+
* name: z.string(),
|
|
21
|
+
* email: z.string().email(),
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* // Create collection
|
|
25
|
+
* const usersCollection = createCollection({
|
|
26
|
+
* name: 'users',
|
|
27
|
+
* schema: UserSchema,
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* // Create database
|
|
31
|
+
* const db = createDB({
|
|
32
|
+
* collections: [usersCollection],
|
|
33
|
+
* })
|
|
34
|
+
*
|
|
35
|
+
* // Use in React
|
|
36
|
+
* function App() {
|
|
37
|
+
* return (
|
|
38
|
+
* <DBProvider db={db}>
|
|
39
|
+
* <UserList />
|
|
40
|
+
* </DBProvider>
|
|
41
|
+
* )
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* function UserList() {
|
|
45
|
+
* const { data, isLoading } = useQuery({
|
|
46
|
+
* from: 'users',
|
|
47
|
+
* where: { role: 'admin' },
|
|
48
|
+
* })
|
|
49
|
+
*
|
|
50
|
+
* const { mutate } = useMutation({
|
|
51
|
+
* collection: 'users',
|
|
52
|
+
* operation: 'insert',
|
|
53
|
+
* })
|
|
54
|
+
*
|
|
55
|
+
* // ...
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
// Core functions
|
|
61
|
+
export { createDB } from './db'
|
|
62
|
+
export { createCollection } from './collection'
|
|
63
|
+
export { createDOSync } from './sync'
|
|
64
|
+
export type { DOSyncConfig, DOSyncAdapter, ReconnectOptions, ConflictResolution } from './sync'
|
|
65
|
+
|
|
66
|
+
// React context
|
|
67
|
+
export { DBProvider, useDBContext } from './context'
|
|
68
|
+
export type { DBProviderProps } from './context'
|
|
69
|
+
|
|
70
|
+
// React hooks
|
|
71
|
+
export { useQuery, useMutation } from './hooks'
|
|
72
|
+
|
|
73
|
+
// Reactive data hooks for data components (Table, List, Card, Metrics)
|
|
74
|
+
export {
|
|
75
|
+
useReactiveData,
|
|
76
|
+
useReactiveTable,
|
|
77
|
+
useReactiveList,
|
|
78
|
+
useReactiveMetrics,
|
|
79
|
+
useReactiveCard,
|
|
80
|
+
} from './reactive'
|
|
81
|
+
export type {
|
|
82
|
+
SortState,
|
|
83
|
+
SelectionState,
|
|
84
|
+
PaginationState,
|
|
85
|
+
ReactiveDataOptions,
|
|
86
|
+
ReactiveDataResult,
|
|
87
|
+
ReactiveTableOptions,
|
|
88
|
+
ReactiveListOptions,
|
|
89
|
+
ReactiveMetricsOptions,
|
|
90
|
+
MetricValue,
|
|
91
|
+
ReactiveMetricsResult,
|
|
92
|
+
ReactiveCardOptions,
|
|
93
|
+
ReactiveCardResult,
|
|
94
|
+
} from './reactive'
|
|
95
|
+
|
|
96
|
+
// SaaS Collections
|
|
97
|
+
export {
|
|
98
|
+
// Factory functions
|
|
99
|
+
UsersCollection,
|
|
100
|
+
APIKeysCollection,
|
|
101
|
+
WebhooksCollection,
|
|
102
|
+
TeamsCollection,
|
|
103
|
+
UsageCollection,
|
|
104
|
+
// Schemas
|
|
105
|
+
UserSchema,
|
|
106
|
+
UserRoleSchema,
|
|
107
|
+
APIKeySchema,
|
|
108
|
+
WebhookSchema,
|
|
109
|
+
TeamSchema,
|
|
110
|
+
UsageSchema,
|
|
111
|
+
} from './saas-collections'
|
|
112
|
+
export type {
|
|
113
|
+
User,
|
|
114
|
+
UserRole,
|
|
115
|
+
APIKey,
|
|
116
|
+
Webhook,
|
|
117
|
+
Team,
|
|
118
|
+
Usage,
|
|
119
|
+
} from './saas-collections'
|
|
120
|
+
|
|
121
|
+
// Types
|
|
122
|
+
export type {
|
|
123
|
+
DB,
|
|
124
|
+
DBConfig,
|
|
125
|
+
Collection,
|
|
126
|
+
SyncAdapter,
|
|
127
|
+
FindManyOptions,
|
|
128
|
+
QueryOptions,
|
|
129
|
+
QueryResult,
|
|
130
|
+
MutationOptions,
|
|
131
|
+
MutationResult,
|
|
132
|
+
WhereClause,
|
|
133
|
+
OrderByClause,
|
|
134
|
+
ComparisonOperators,
|
|
135
|
+
FieldFilter,
|
|
136
|
+
OrderDirection,
|
|
137
|
+
MutationOperation,
|
|
138
|
+
} from './types'
|