@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,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Collection Implementation
|
|
3
|
+
*
|
|
4
|
+
* In-memory collection with Zod validation, reactive subscriptions, filtering,
|
|
5
|
+
* sorting, and pagination support. Provides full CRUD operations with type safety.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ZodSchema } from 'zod'
|
|
9
|
+
import type { Collection, FindManyOptions, WhereClause, OrderByClause, FieldFilter } from './types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configuration for creating a collection
|
|
13
|
+
*
|
|
14
|
+
* @template T - The document type stored in this collection
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const config: CreateCollectionConfig<User> = {
|
|
19
|
+
* name: 'users',
|
|
20
|
+
* schema: UserSchema,
|
|
21
|
+
* primaryKey: 'id',
|
|
22
|
+
* indexes: ['email', 'role']
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export interface CreateCollectionConfig<T> {
|
|
27
|
+
/** Collection name - used to reference in queries and mutations */
|
|
28
|
+
name: string
|
|
29
|
+
/** Zod schema for automatic validation on all insert/update operations */
|
|
30
|
+
schema: ZodSchema<T>
|
|
31
|
+
/** Primary key field (defaults to 'id'). Used for duplicate detection */
|
|
32
|
+
primaryKey?: string
|
|
33
|
+
/** Fields to index for faster queries (metadata only for future optimization) */
|
|
34
|
+
indexes?: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if a value matches a field filter
|
|
39
|
+
*/
|
|
40
|
+
function matchesFilter<T>(value: T, filter: FieldFilter<T>): boolean {
|
|
41
|
+
// Direct equality
|
|
42
|
+
if (filter === null || filter === undefined || typeof filter !== 'object') {
|
|
43
|
+
return value === filter
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Comparison operators
|
|
47
|
+
const ops = filter as Record<string, any>
|
|
48
|
+
|
|
49
|
+
if ('$eq' in ops && value !== ops.$eq) return false
|
|
50
|
+
if ('$ne' in ops && value === ops.$ne) return false
|
|
51
|
+
if ('$gt' in ops && !(value > ops.$gt)) return false
|
|
52
|
+
if ('$gte' in ops && !(value >= ops.$gte)) return false
|
|
53
|
+
if ('$lt' in ops && !(value < ops.$lt)) return false
|
|
54
|
+
if ('$lte' in ops && !(value <= ops.$lte)) return false
|
|
55
|
+
if ('$in' in ops && !ops.$in.includes(value)) return false
|
|
56
|
+
if ('$nin' in ops && ops.$nin.includes(value)) return false
|
|
57
|
+
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if a document matches a where clause
|
|
63
|
+
*/
|
|
64
|
+
function matchesWhere<T extends Record<string, any>>(doc: T, where: WhereClause<T>): boolean {
|
|
65
|
+
// Handle $or operator
|
|
66
|
+
if ('$or' in where && where.$or) {
|
|
67
|
+
const orClauses = where.$or as WhereClause<T>[]
|
|
68
|
+
const matchesOr = orClauses.some(clause => matchesWhere(doc, clause))
|
|
69
|
+
if (!matchesOr) return false
|
|
70
|
+
|
|
71
|
+
// Check remaining fields (AND with the $or result)
|
|
72
|
+
const { $or, ...rest } = where
|
|
73
|
+
if (Object.keys(rest).length === 0) return true
|
|
74
|
+
return matchesWhere(doc, rest as WhereClause<T>)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check all field conditions (AND)
|
|
78
|
+
for (const [key, filter] of Object.entries(where)) {
|
|
79
|
+
if (key === '$or') continue
|
|
80
|
+
const value = doc[key]
|
|
81
|
+
if (!matchesFilter(value, filter as FieldFilter<any>)) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sort documents by orderBy clause
|
|
91
|
+
*/
|
|
92
|
+
function sortDocuments<T extends Record<string, any>>(
|
|
93
|
+
docs: T[],
|
|
94
|
+
orderBy: OrderByClause<T> | OrderByClause<T>[]
|
|
95
|
+
): T[] {
|
|
96
|
+
const orderClauses = Array.isArray(orderBy) ? orderBy : [orderBy]
|
|
97
|
+
|
|
98
|
+
return [...docs].sort((a, b) => {
|
|
99
|
+
for (const clause of orderClauses) {
|
|
100
|
+
const [field, direction] = Object.entries(clause)[0]
|
|
101
|
+
const aVal = a[field]
|
|
102
|
+
const bVal = b[field]
|
|
103
|
+
|
|
104
|
+
// Handle null/undefined - put them last
|
|
105
|
+
if (aVal === null || aVal === undefined) {
|
|
106
|
+
if (bVal !== null && bVal !== undefined) return 1
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if (bVal === null || bVal === undefined) return -1
|
|
110
|
+
|
|
111
|
+
// Compare values
|
|
112
|
+
let comparison = 0
|
|
113
|
+
if (typeof aVal === 'string') {
|
|
114
|
+
comparison = aVal.localeCompare(bVal)
|
|
115
|
+
} else if (typeof aVal === 'number') {
|
|
116
|
+
comparison = aVal - bVal
|
|
117
|
+
} else if (aVal instanceof Date) {
|
|
118
|
+
comparison = aVal.getTime() - bVal.getTime()
|
|
119
|
+
} else {
|
|
120
|
+
comparison = String(aVal).localeCompare(String(bVal))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (comparison !== 0) {
|
|
124
|
+
return direction === 'desc' ? -comparison : comparison
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return 0
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a typed in-memory collection with Zod validation
|
|
133
|
+
*
|
|
134
|
+
* @template T - The document type (must be a Record for proper typing)
|
|
135
|
+
*
|
|
136
|
+
* @param config - Collection configuration (name, schema, primaryKey, indexes)
|
|
137
|
+
* @returns A fully typed collection instance with CRUD operations and subscriptions
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* - All documents are validated against the provided Zod schema on insert/update
|
|
141
|
+
* - Primary key defaults to 'id' if not specified
|
|
142
|
+
* - Provides reactive subscriptions for real-time updates
|
|
143
|
+
* - Supports filtering with where clauses and comparison operators
|
|
144
|
+
* - Supports sorting with orderBy and multiple sort fields
|
|
145
|
+
* - Supports pagination with limit and offset
|
|
146
|
+
* - Thread-safe for concurrent operations (in-memory only)
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* import { z } from 'zod'
|
|
151
|
+
* import { createCollection } from '@mdxui/terminal'
|
|
152
|
+
*
|
|
153
|
+
* // Define schema
|
|
154
|
+
* const UserSchema = z.object({
|
|
155
|
+
* id: z.string(),
|
|
156
|
+
* name: z.string(),
|
|
157
|
+
* email: z.string().email(),
|
|
158
|
+
* age: z.number().optional(),
|
|
159
|
+
* role: z.enum(['admin', 'user', 'guest'])
|
|
160
|
+
* })
|
|
161
|
+
*
|
|
162
|
+
* type User = z.infer<typeof UserSchema>
|
|
163
|
+
*
|
|
164
|
+
* // Create collection
|
|
165
|
+
* const users = createCollection<User>({
|
|
166
|
+
* name: 'users',
|
|
167
|
+
* schema: UserSchema,
|
|
168
|
+
* primaryKey: 'id',
|
|
169
|
+
* indexes: ['email', 'role']
|
|
170
|
+
* })
|
|
171
|
+
*
|
|
172
|
+
* // CRUD Operations
|
|
173
|
+
* const user = await users.insert({
|
|
174
|
+
* id: '1',
|
|
175
|
+
* name: 'Alice',
|
|
176
|
+
* email: 'alice@example.com',
|
|
177
|
+
* role: 'admin'
|
|
178
|
+
* })
|
|
179
|
+
*
|
|
180
|
+
* const updated = await users.update(
|
|
181
|
+
* { id: '1' },
|
|
182
|
+
* { name: 'Alicia' }
|
|
183
|
+
* )
|
|
184
|
+
*
|
|
185
|
+
* const found = await users.findOne({ id: '1' })
|
|
186
|
+
*
|
|
187
|
+
* const admins = await users.findMany({
|
|
188
|
+
* where: { role: 'admin' },
|
|
189
|
+
* orderBy: { name: 'asc' }
|
|
190
|
+
* })
|
|
191
|
+
*
|
|
192
|
+
* await users.delete({ id: '1' })
|
|
193
|
+
*
|
|
194
|
+
* // Subscribe to changes
|
|
195
|
+
* const unsubscribe = users.subscribe((allUsers) => {
|
|
196
|
+
* console.log('Users updated:', allUsers)
|
|
197
|
+
* })
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export function createCollection<T extends Record<string, any>>(
|
|
201
|
+
config: CreateCollectionConfig<T>
|
|
202
|
+
): Collection<T> {
|
|
203
|
+
const { name, schema, primaryKey = 'id', indexes = [] } = config
|
|
204
|
+
|
|
205
|
+
// In-memory storage
|
|
206
|
+
const data = new Map<string, T>()
|
|
207
|
+
const subscribers = new Set<(data: T[]) => void>()
|
|
208
|
+
|
|
209
|
+
const collection: Collection<T> = {
|
|
210
|
+
name,
|
|
211
|
+
schema,
|
|
212
|
+
primaryKey: primaryKey as string,
|
|
213
|
+
indexes: indexes as string[],
|
|
214
|
+
|
|
215
|
+
async insert(doc: T): Promise<T> {
|
|
216
|
+
// Validate against schema
|
|
217
|
+
const validated = schema.parse(doc)
|
|
218
|
+
const id = validated[primaryKey as keyof T]
|
|
219
|
+
|
|
220
|
+
// Check for duplicates
|
|
221
|
+
if (data.has(String(id))) {
|
|
222
|
+
throw new Error(`Duplicate ${primaryKey}: ${id}`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
data.set(String(id), validated)
|
|
226
|
+
collection._notify()
|
|
227
|
+
return validated
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
async update(filter: Partial<T>, updates: Partial<T>): Promise<T[]> {
|
|
231
|
+
const updated: T[] = []
|
|
232
|
+
|
|
233
|
+
for (const [key, doc] of data.entries()) {
|
|
234
|
+
if (matchesWhere(doc, filter as WhereClause<T>)) {
|
|
235
|
+
const newDoc = { ...doc, ...updates }
|
|
236
|
+
// Validate updated document
|
|
237
|
+
const validated = schema.parse(newDoc)
|
|
238
|
+
data.set(key, validated)
|
|
239
|
+
updated.push(validated)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (updated.length > 0) {
|
|
244
|
+
collection._notify()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return updated
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
async delete(filter: Partial<T>): Promise<void> {
|
|
251
|
+
let deleted = false
|
|
252
|
+
|
|
253
|
+
for (const [key, doc] of data.entries()) {
|
|
254
|
+
if (matchesWhere(doc, filter as WhereClause<T>)) {
|
|
255
|
+
data.delete(key)
|
|
256
|
+
deleted = true
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (deleted) {
|
|
261
|
+
collection._notify()
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
async findOne(filter: Partial<T>): Promise<T | null> {
|
|
266
|
+
for (const doc of data.values()) {
|
|
267
|
+
if (matchesWhere(doc, filter as WhereClause<T>)) {
|
|
268
|
+
return doc
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return null
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
async findMany(options?: FindManyOptions<T>): Promise<T[]> {
|
|
275
|
+
let results = Array.from(data.values())
|
|
276
|
+
|
|
277
|
+
// Apply where filter
|
|
278
|
+
if (options?.where) {
|
|
279
|
+
results = results.filter(doc => matchesWhere(doc, options.where!))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Apply ordering
|
|
283
|
+
if (options?.orderBy) {
|
|
284
|
+
results = sortDocuments(results, options.orderBy)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Apply offset
|
|
288
|
+
if (options?.offset !== undefined) {
|
|
289
|
+
results = results.slice(options.offset)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Apply limit
|
|
293
|
+
if (options?.limit !== undefined) {
|
|
294
|
+
results = results.slice(0, options.limit)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return results
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
subscribe(callback: (data: T[]) => void): () => void {
|
|
301
|
+
subscribers.add(callback)
|
|
302
|
+
return () => {
|
|
303
|
+
subscribers.delete(callback)
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
_notify(): void {
|
|
308
|
+
const allData = Array.from(data.values())
|
|
309
|
+
for (const callback of subscribers) {
|
|
310
|
+
callback(allData)
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return collection
|
|
316
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal SaaS Collections
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: This is a stub file that will cause tests to fail with meaningful errors.
|
|
5
|
+
* The GREEN phase implementation will replace this with actual schemas and collections.
|
|
6
|
+
*
|
|
7
|
+
* Pre-built Zod schemas and collections for common SaaS primitives:
|
|
8
|
+
* - Users: Authentication and user management
|
|
9
|
+
* - APIKeys: API key management with permissions
|
|
10
|
+
* - Webhooks: Webhook configuration and event subscriptions
|
|
11
|
+
* - Teams: Team/organization management with members and plans
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { createCollection } from './collection'
|
|
16
|
+
import type { Collection } from './types'
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// RED PHASE STUBS - These will fail validation tests
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
// Placeholder schemas that will fail the tests
|
|
23
|
+
// GREEN phase will implement proper schemas
|
|
24
|
+
|
|
25
|
+
export const UserSchema = z.object({})
|
|
26
|
+
export const APIKeySchema = z.object({})
|
|
27
|
+
export const WebhookSchema = z.object({})
|
|
28
|
+
export const TeamSchema = z.object({})
|
|
29
|
+
|
|
30
|
+
// Type exports
|
|
31
|
+
export type User = z.infer<typeof UserSchema>
|
|
32
|
+
export type APIKey = z.infer<typeof APIKeySchema>
|
|
33
|
+
export type Webhook = z.infer<typeof WebhookSchema>
|
|
34
|
+
export type Team = z.infer<typeof TeamSchema>
|
|
35
|
+
|
|
36
|
+
// Placeholder collections that will fail
|
|
37
|
+
export const usersCollection = undefined as unknown as Collection<User>
|
|
38
|
+
export const apiKeysCollection = undefined as unknown as Collection<APIKey>
|
|
39
|
+
export const webhooksCollection = undefined as unknown as Collection<Webhook>
|
|
40
|
+
export const teamsCollection = undefined as unknown as Collection<Team>
|
|
41
|
+
|
|
42
|
+
// Factory function placeholder
|
|
43
|
+
export function createSaaSCollections() {
|
|
44
|
+
return {
|
|
45
|
+
users: undefined as unknown as Collection<User>,
|
|
46
|
+
apiKeys: undefined as unknown as Collection<APIKey>,
|
|
47
|
+
webhooks: undefined as unknown as Collection<Webhook>,
|
|
48
|
+
teams: undefined as unknown as Collection<Team>,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Database Context
|
|
3
|
+
*
|
|
4
|
+
* React Context API provider for database access throughout component tree.
|
|
5
|
+
* Enables hooks like `useQuery` and `useMutation` to access the database
|
|
6
|
+
* without prop drilling.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as React from 'react'
|
|
10
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
11
|
+
import type { DB } from './types'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* React Context for database instance
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Created as undefined by default - must be provided by DBProvider wrapper.
|
|
18
|
+
* Used internally by useQuery and useMutation hooks.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
const DBContext = createContext<DB | undefined>(undefined)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Props for DBProvider component
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const props: DBProviderProps = {
|
|
30
|
+
* db: myDatabase,
|
|
31
|
+
* children: <App />
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export interface DBProviderProps {
|
|
36
|
+
/** Database instance to provide to all children */
|
|
37
|
+
db: DB
|
|
38
|
+
/** React components that will have access to the database */
|
|
39
|
+
children: ReactNode
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Database provider component - wraps your app to provide database context
|
|
44
|
+
*
|
|
45
|
+
* Provides database access to all child components via React Context.
|
|
46
|
+
* Must wrap any component using `useQuery` or `useMutation` hooks.
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* - Use at the top level of your app (e.g., around `<App />`)
|
|
50
|
+
* - Can have multiple DBProviders with different database instances
|
|
51
|
+
* - All useQuery and useMutation hooks must be within a DBProvider
|
|
52
|
+
* - Throws error if hooks are used outside DBProvider
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* import { z } from 'zod'
|
|
57
|
+
* import { createDB, createCollection, DBProvider } from '@mdxui/terminal'
|
|
58
|
+
*
|
|
59
|
+
* // Create database
|
|
60
|
+
* const db = createDB({
|
|
61
|
+
* collections: [usersCollection, todosCollection]
|
|
62
|
+
* })
|
|
63
|
+
*
|
|
64
|
+
* // Wrap app with provider
|
|
65
|
+
* export function App() {
|
|
66
|
+
* return (
|
|
67
|
+
* <DBProvider db={db}>
|
|
68
|
+
* <UserList />
|
|
69
|
+
* <TodoForm />
|
|
70
|
+
* </DBProvider>
|
|
71
|
+
* )
|
|
72
|
+
* }
|
|
73
|
+
*
|
|
74
|
+
* // Now these hooks work:
|
|
75
|
+
* function UserList() {
|
|
76
|
+
* const { data, isLoading } = useQuery({
|
|
77
|
+
* from: 'users',
|
|
78
|
+
* where: { role: 'admin' }
|
|
79
|
+
* })
|
|
80
|
+
*
|
|
81
|
+
* if (isLoading) return <div>Loading...</div>
|
|
82
|
+
* return (
|
|
83
|
+
* <ul>
|
|
84
|
+
* {data?.map(user => (
|
|
85
|
+
* <li key={user.id}>{user.name}</li>
|
|
86
|
+
* ))}
|
|
87
|
+
* </ul>
|
|
88
|
+
* )
|
|
89
|
+
* }
|
|
90
|
+
*
|
|
91
|
+
* function TodoForm() {
|
|
92
|
+
* const { mutate, isPending } = useMutation({
|
|
93
|
+
* collection: 'todos',
|
|
94
|
+
* operation: 'insert'
|
|
95
|
+
* })
|
|
96
|
+
*
|
|
97
|
+
* return (
|
|
98
|
+
* <form onSubmit={(e) => {
|
|
99
|
+
* e.preventDefault()
|
|
100
|
+
* mutate({ id: '1', title: 'New', completed: false })
|
|
101
|
+
* }}>
|
|
102
|
+
* <button disabled={isPending}>Add Todo</button>
|
|
103
|
+
* </form>
|
|
104
|
+
* )
|
|
105
|
+
* }
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function DBProvider({ db, children }: DBProviderProps): React.ReactElement {
|
|
109
|
+
return React.createElement(DBContext.Provider, { value: db }, children)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Hook to access the database instance from context
|
|
114
|
+
*
|
|
115
|
+
* @returns The database instance from the nearest DBProvider
|
|
116
|
+
* @throws Error if used outside a DBProvider component
|
|
117
|
+
*
|
|
118
|
+
* @remarks
|
|
119
|
+
* - Must be called within a component tree wrapped by DBProvider
|
|
120
|
+
* - Allows direct access to collections (advanced use case)
|
|
121
|
+
* - Most apps use useQuery/useMutation instead
|
|
122
|
+
* - Useful for manual collection operations outside hooks
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```tsx
|
|
126
|
+
* // Direct database access (advanced)
|
|
127
|
+
* function MyComponent() {
|
|
128
|
+
* const db = useDBContext()
|
|
129
|
+
*
|
|
130
|
+
* // Manually query
|
|
131
|
+
* const handleClick = async () => {
|
|
132
|
+
* const admins = await db.collections.users.findMany({
|
|
133
|
+
* where: { role: 'admin' }
|
|
134
|
+
* })
|
|
135
|
+
* console.log('Admins:', admins)
|
|
136
|
+
* }
|
|
137
|
+
*
|
|
138
|
+
* return <button onClick={handleClick}>Get Admins</button>
|
|
139
|
+
* }
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```tsx
|
|
144
|
+
* // Subscribe to collection changes (advanced)
|
|
145
|
+
* function CollectionMonitor() {
|
|
146
|
+
* const db = useDBContext()
|
|
147
|
+
*
|
|
148
|
+
* React.useEffect(() => {
|
|
149
|
+
* const unsubscribe = db.collections.users.subscribe((users) => {
|
|
150
|
+
* console.log('Users changed:', users)
|
|
151
|
+
* })
|
|
152
|
+
* return unsubscribe
|
|
153
|
+
* }, [db])
|
|
154
|
+
*
|
|
155
|
+
* return <div>Monitoring changes...</div>
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export function useDBContext(): DB {
|
|
160
|
+
const db = useContext(DBContext)
|
|
161
|
+
if (!db) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'useDBContext must be used within a DBProvider. ' +
|
|
164
|
+
'Wrap your component with <DBProvider db={...}> at a higher level in the component tree.'
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
return db
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Internal: Export context for use by other hooks
|
|
172
|
+
* @internal
|
|
173
|
+
*/
|
|
174
|
+
export { DBContext }
|
package/src/data/db.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Database Implementation
|
|
3
|
+
*
|
|
4
|
+
* In-memory database with collection management, sync adapter support,
|
|
5
|
+
* and centralized data access for React applications.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DB, DBConfig, SyncAdapter, Collection } from './types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a database instance with collections and optional sync adapter
|
|
12
|
+
*
|
|
13
|
+
* @param config - Database configuration with collections and sync adapter
|
|
14
|
+
* @returns A fully typed database instance for accessing collections
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* - Collections are accessed via `db.collections[name]`
|
|
18
|
+
* - All data is stored in-memory (cleared on process exit)
|
|
19
|
+
* - Optional sync adapter can push/pull changes to a remote backend
|
|
20
|
+
* - Use within `DBProvider` for React applications
|
|
21
|
+
* - Thread-safe for concurrent operations
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { z } from 'zod'
|
|
26
|
+
* import { createDB, createCollection } from '@mdxui/terminal'
|
|
27
|
+
*
|
|
28
|
+
* // Define schemas
|
|
29
|
+
* const UserSchema = z.object({
|
|
30
|
+
* id: z.string(),
|
|
31
|
+
* name: z.string(),
|
|
32
|
+
* email: z.string().email(),
|
|
33
|
+
* role: z.enum(['admin', 'user', 'guest'])
|
|
34
|
+
* })
|
|
35
|
+
*
|
|
36
|
+
* // Create collections
|
|
37
|
+
* const usersCollection = createCollection({
|
|
38
|
+
* name: 'users',
|
|
39
|
+
* schema: UserSchema,
|
|
40
|
+
* primaryKey: 'id'
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* // Create database with collections
|
|
44
|
+
* const db = createDB({
|
|
45
|
+
* collections: [usersCollection],
|
|
46
|
+
* sync: customSyncAdapter // optional
|
|
47
|
+
* })
|
|
48
|
+
*
|
|
49
|
+
* // Access collections
|
|
50
|
+
* const user = await db.collections.users.insert({
|
|
51
|
+
* id: '1',
|
|
52
|
+
* name: 'Alice',
|
|
53
|
+
* email: 'alice@example.com',
|
|
54
|
+
* role: 'admin'
|
|
55
|
+
* })
|
|
56
|
+
*
|
|
57
|
+
* const all = await db.collections.users.findMany()
|
|
58
|
+
* const admins = await db.collections.users.findMany({
|
|
59
|
+
* where: { role: 'admin' }
|
|
60
|
+
* })
|
|
61
|
+
*
|
|
62
|
+
* // Clean up
|
|
63
|
+
* await db.clear()
|
|
64
|
+
* db.close()
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* // With sync adapter for remote sync
|
|
70
|
+
* const syncAdapter = {
|
|
71
|
+
* async push(changes) {
|
|
72
|
+
* await fetch('/api/sync', {
|
|
73
|
+
* method: 'POST',
|
|
74
|
+
* body: JSON.stringify(changes)
|
|
75
|
+
* })
|
|
76
|
+
* },
|
|
77
|
+
* async pull() {
|
|
78
|
+
* const res = await fetch('/api/sync')
|
|
79
|
+
* return res.json()
|
|
80
|
+
* },
|
|
81
|
+
* subscribe(callback) {
|
|
82
|
+
* const ws = new WebSocket('wss://...')
|
|
83
|
+
* ws.onmessage = (e) => callback(e.data)
|
|
84
|
+
* return () => ws.close()
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* const db = createDB({
|
|
89
|
+
* collections: [usersCollection],
|
|
90
|
+
* sync: syncAdapter
|
|
91
|
+
* })
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function createDB(config?: DBConfig): DB {
|
|
95
|
+
const collections: Record<string, Collection<any>> = {}
|
|
96
|
+
|
|
97
|
+
// Register collections
|
|
98
|
+
if (config?.collections) {
|
|
99
|
+
for (const collection of config.collections) {
|
|
100
|
+
collections[collection.name] = collection
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const db: DB = {
|
|
105
|
+
collections,
|
|
106
|
+
sync: config?.sync,
|
|
107
|
+
|
|
108
|
+
close(): void {
|
|
109
|
+
// Clean up resources
|
|
110
|
+
// In a real implementation, this would close connections
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async clear(): Promise<void> {
|
|
114
|
+
// Clear all data in all collections
|
|
115
|
+
for (const collection of Object.values(collections)) {
|
|
116
|
+
// Get all documents and delete them
|
|
117
|
+
const docs = await collection.findMany()
|
|
118
|
+
for (const doc of docs) {
|
|
119
|
+
const primaryKey = collection.primaryKey || 'id'
|
|
120
|
+
await collection.delete({ [primaryKey]: doc[primaryKey] } as any)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return db
|
|
127
|
+
}
|