@object-ui/core 3.1.5 → 3.3.1
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/CHANGELOG.md +18 -0
- package/README.md +20 -1
- package/dist/actions/ActionRunner.d.ts +9 -0
- package/dist/actions/ActionRunner.js +41 -4
- package/dist/adapters/ValueDataSource.d.ts +5 -1
- package/dist/adapters/ValueDataSource.js +30 -1
- package/dist/errors/index.js +2 -3
- package/dist/evaluator/ExpressionCache.d.ts +9 -10
- package/dist/evaluator/ExpressionCache.js +29 -8
- package/dist/evaluator/SafeExpressionParser.d.ts +131 -0
- package/dist/evaluator/SafeExpressionParser.js +851 -0
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/protocols/DndProtocol.js +2 -14
- package/dist/protocols/KeyboardProtocol.js +1 -4
- package/dist/protocols/NotificationProtocol.js +3 -13
- package/dist/utils/debug.js +2 -1
- package/dist/utils/filter-converter.js +25 -5
- package/package.json +33 -9
- package/.turbo/turbo-build.log +0 -4
- package/src/__benchmarks__/core.bench.ts +0 -64
- package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
- package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
- package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
- package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
- package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
- package/src/actions/ActionEngine.ts +0 -268
- package/src/actions/ActionRunner.ts +0 -717
- package/src/actions/TransactionManager.ts +0 -521
- package/src/actions/UndoManager.ts +0 -215
- package/src/actions/__tests__/ActionEngine.test.ts +0 -206
- package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
- package/src/actions/__tests__/ActionRunner.test.ts +0 -711
- package/src/actions/__tests__/TransactionManager.test.ts +0 -447
- package/src/actions/__tests__/UndoManager.test.ts +0 -320
- package/src/actions/index.ts +0 -12
- package/src/adapters/ApiDataSource.ts +0 -376
- package/src/adapters/README.md +0 -180
- package/src/adapters/ValueDataSource.ts +0 -438
- package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
- package/src/adapters/__tests__/ValueDataSource.test.ts +0 -472
- package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
- package/src/adapters/index.ts +0 -15
- package/src/adapters/resolveDataSource.ts +0 -79
- package/src/builder/__tests__/schema-builder.test.ts +0 -235
- package/src/builder/schema-builder.ts +0 -584
- package/src/data-scope/DataScopeManager.ts +0 -269
- package/src/data-scope/ViewDataProvider.ts +0 -282
- package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
- package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
- package/src/data-scope/index.ts +0 -24
- package/src/errors/__tests__/errors.test.ts +0 -292
- package/src/errors/index.ts +0 -270
- package/src/evaluator/ExpressionCache.ts +0 -192
- package/src/evaluator/ExpressionContext.ts +0 -118
- package/src/evaluator/ExpressionEvaluator.ts +0 -315
- package/src/evaluator/FormulaFunctions.ts +0 -398
- package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
- package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
- package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -131
- package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
- package/src/evaluator/index.ts +0 -12
- package/src/index.ts +0 -38
- package/src/protocols/DndProtocol.ts +0 -184
- package/src/protocols/KeyboardProtocol.ts +0 -185
- package/src/protocols/NotificationProtocol.ts +0 -159
- package/src/protocols/ResponsiveProtocol.ts +0 -210
- package/src/protocols/SharingProtocol.ts +0 -185
- package/src/protocols/index.ts +0 -13
- package/src/query/__tests__/query-ast.test.ts +0 -211
- package/src/query/__tests__/window-functions.test.ts +0 -275
- package/src/query/index.ts +0 -7
- package/src/query/query-ast.ts +0 -341
- package/src/registry/PluginScopeImpl.ts +0 -259
- package/src/registry/PluginSystem.ts +0 -206
- package/src/registry/Registry.ts +0 -219
- package/src/registry/WidgetRegistry.ts +0 -316
- package/src/registry/__tests__/PluginSystem.test.ts +0 -309
- package/src/registry/__tests__/Registry.test.ts +0 -293
- package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
- package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
- package/src/theme/ThemeEngine.ts +0 -530
- package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
- package/src/theme/index.ts +0 -24
- package/src/types/index.ts +0 -21
- package/src/utils/__tests__/debug-collector.test.ts +0 -102
- package/src/utils/__tests__/debug.test.ts +0 -134
- package/src/utils/__tests__/expand-fields.test.ts +0 -120
- package/src/utils/__tests__/extract-records.test.ts +0 -50
- package/src/utils/__tests__/filter-converter.test.ts +0 -118
- package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
- package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
- package/src/utils/debug-collector.ts +0 -100
- package/src/utils/debug.ts +0 -147
- package/src/utils/expand-fields.ts +0 -76
- package/src/utils/extract-records.ts +0 -33
- package/src/utils/filter-converter.ts +0 -133
- package/src/utils/merge-views-into-objects.ts +0 -36
- package/src/utils/normalize-quick-filter.ts +0 -78
- package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
- package/src/validation/__tests__/schema-validator.test.ts +0 -118
- package/src/validation/__tests__/validation-engine.test.ts +0 -102
- package/src/validation/index.ts +0 -10
- package/src/validation/schema-validator.ts +0 -344
- package/src/validation/validation-engine.ts +0 -528
- package/src/validation/validators/index.ts +0 -25
- package/src/validation/validators/object-validation-engine.ts +0 -722
- package/tsconfig.json +0 -15
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -2
package/src/adapters/README.md
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
# Data Source Adapters
|
|
2
|
-
|
|
3
|
-
This directory contains data source adapters that bridge various backend protocols with the ObjectUI DataSource interface.
|
|
4
|
-
|
|
5
|
-
## ObjectStack Adapter
|
|
6
|
-
|
|
7
|
-
The `ObjectStackAdapter` provides seamless integration with ObjectStack Protocol servers.
|
|
8
|
-
|
|
9
|
-
### Features
|
|
10
|
-
|
|
11
|
-
- ✅ Full CRUD operations (find, findOne, create, update, delete)
|
|
12
|
-
- ✅ Bulk operations (createMany, updateMany, deleteMany)
|
|
13
|
-
- ✅ Auto-discovery of server capabilities
|
|
14
|
-
- ✅ Query parameter translation (OData-style → ObjectStack)
|
|
15
|
-
- ✅ Proper error handling
|
|
16
|
-
- ✅ TypeScript types
|
|
17
|
-
|
|
18
|
-
### Usage
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
import { createObjectStackAdapter } from '@object-ui/core';
|
|
22
|
-
|
|
23
|
-
// Create the adapter
|
|
24
|
-
const dataSource = createObjectStackAdapter({
|
|
25
|
-
baseUrl: 'https://api.example.com',
|
|
26
|
-
token: 'your-auth-token', // Optional
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Use it with ObjectUI components
|
|
30
|
-
const schema = {
|
|
31
|
-
type: 'data-table',
|
|
32
|
-
dataSource,
|
|
33
|
-
resource: 'users',
|
|
34
|
-
columns: [
|
|
35
|
-
{ header: 'Name', accessorKey: 'name' },
|
|
36
|
-
{ header: 'Email', accessorKey: 'email' },
|
|
37
|
-
]
|
|
38
|
-
};
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Advanced Usage
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import { ObjectStackAdapter } from '@object-ui/core';
|
|
45
|
-
|
|
46
|
-
const adapter = new ObjectStackAdapter({
|
|
47
|
-
baseUrl: 'https://api.example.com',
|
|
48
|
-
token: process.env.API_TOKEN,
|
|
49
|
-
fetch: customFetch // Optional: use custom fetch (e.g., Next.js fetch)
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Manually connect (optional, auto-connects on first request)
|
|
53
|
-
await adapter.connect();
|
|
54
|
-
|
|
55
|
-
// Query with filters (MongoDB-like operators)
|
|
56
|
-
const result = await adapter.find('tasks', {
|
|
57
|
-
$filter: {
|
|
58
|
-
status: 'active',
|
|
59
|
-
priority: { $gte: 2 }
|
|
60
|
-
},
|
|
61
|
-
$orderby: { createdAt: 'desc' },
|
|
62
|
-
$top: 20,
|
|
63
|
-
$skip: 0
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Access the underlying client for advanced operations
|
|
67
|
-
const client = adapter.getClient();
|
|
68
|
-
const metadata = await client.meta.getObject('task');
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Filter Conversion
|
|
72
|
-
|
|
73
|
-
The adapter automatically converts MongoDB-like filter operators to **ObjectStack FilterNode AST format**. This ensures compatibility with the latest ObjectStack Protocol (v0.1.2+).
|
|
74
|
-
|
|
75
|
-
#### Supported Filter Operators
|
|
76
|
-
|
|
77
|
-
| MongoDB Operator | ObjectStack Operator | Example |
|
|
78
|
-
|------------------|---------------------|---------|
|
|
79
|
-
| `$eq` or simple value | `=` | `{ status: 'active' }` → `['status', '=', 'active']` |
|
|
80
|
-
| `$ne` | `!=` | `{ status: { $ne: 'archived' } }` → `['status', '!=', 'archived']` |
|
|
81
|
-
| `$gt` | `>` | `{ age: { $gt: 18 } }` → `['age', '>', 18]` |
|
|
82
|
-
| `$gte` | `>=` | `{ age: { $gte: 18 } }` → `['age', '>=', 18]` |
|
|
83
|
-
| `$lt` | `<` | `{ age: { $lt: 65 } }` → `['age', '<', 65]` |
|
|
84
|
-
| `$lte` | `<=` | `{ age: { $lte: 65 } }` → `['age', '<=', 65]` |
|
|
85
|
-
| `$in` | `in` | `{ status: { $in: ['active', 'pending'] } }` → `['status', 'in', ['active', 'pending']]` |
|
|
86
|
-
| `$nin` / `$notin` | `notin` | `{ status: { $nin: ['archived'] } }` → `['status', 'notin', ['archived']]` |
|
|
87
|
-
| `$contains` / `$regex` | `contains` | `{ name: { $contains: 'John' } }` → `['name', 'contains', 'John']` |
|
|
88
|
-
| `$startswith` | `startswith` | `{ email: { $startswith: 'admin' } }` → `['email', 'startswith', 'admin']` |
|
|
89
|
-
| `$between` | `between` | `{ age: { $between: [18, 65] } }` → `['age', 'between', [18, 65]]` |
|
|
90
|
-
|
|
91
|
-
#### Complex Filter Examples
|
|
92
|
-
|
|
93
|
-
**Multiple conditions** are combined with `'and'`:
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
// Input
|
|
97
|
-
$filter: {
|
|
98
|
-
age: { $gte: 18, $lte: 65 },
|
|
99
|
-
status: 'active'
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Converted to AST
|
|
103
|
-
['and',
|
|
104
|
-
['age', '>=', 18],
|
|
105
|
-
['age', '<=', 65],
|
|
106
|
-
['status', '=', 'active']
|
|
107
|
-
]
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Query Parameter Mapping
|
|
111
|
-
|
|
112
|
-
The adapter automatically converts ObjectUI query parameters (OData-style) to ObjectStack protocol:
|
|
113
|
-
|
|
114
|
-
| ObjectUI ($) | ObjectStack | Description |
|
|
115
|
-
|--------------|-------------|-------------|
|
|
116
|
-
| `$select` | `select` | Field selection |
|
|
117
|
-
| `$filter` | `filters` (AST) | Filter conditions (converted to FilterNode AST) |
|
|
118
|
-
| `$orderby` | `sort` | Sort order |
|
|
119
|
-
| `$skip` | `skip` | Pagination offset |
|
|
120
|
-
| `$top` | `top` | Limit records |
|
|
121
|
-
|
|
122
|
-
### Example with Sorting
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// OData-style
|
|
126
|
-
await dataSource.find('users', {
|
|
127
|
-
$orderby: {
|
|
128
|
-
createdAt: 'desc',
|
|
129
|
-
name: 'asc'
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Converted to ObjectStack: ['-createdAt', 'name']
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
## Creating Custom Adapters
|
|
137
|
-
|
|
138
|
-
To create a custom adapter, implement the `DataSource<T>` interface:
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
import type { DataSource, QueryParams, QueryResult } from '@object-ui/types';
|
|
142
|
-
|
|
143
|
-
export class MyCustomAdapter<T = any> implements DataSource<T> {
|
|
144
|
-
async find(resource: string, params?: QueryParams): Promise<QueryResult<T>> {
|
|
145
|
-
// Your implementation
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async findOne(resource: string, id: string | number): Promise<T | null> {
|
|
149
|
-
// Your implementation
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async create(resource: string, data: Partial<T>): Promise<T> {
|
|
153
|
-
// Your implementation
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async update(resource: string, id: string | number, data: Partial<T>): Promise<T> {
|
|
157
|
-
// Your implementation
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async delete(resource: string, id: string | number): Promise<boolean> {
|
|
161
|
-
// Your implementation
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Optional: bulk operations
|
|
165
|
-
async bulk?(resource: string, operation: string, data: Partial<T>[]): Promise<T[]> {
|
|
166
|
-
// Your implementation
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
## Available Adapters
|
|
172
|
-
|
|
173
|
-
- **ObjectStackAdapter** - For ObjectStack Protocol servers
|
|
174
|
-
- More adapters coming soon (REST, GraphQL, Supabase, Firebase, etc.)
|
|
175
|
-
|
|
176
|
-
## Related Packages
|
|
177
|
-
|
|
178
|
-
- `@objectstack/client` - ObjectStack Client SDK
|
|
179
|
-
- `@objectstack/spec` - ObjectStack Protocol Specification
|
|
180
|
-
- `@object-ui/types` - ObjectUI Type Definitions
|
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI — ValueDataSource
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*
|
|
8
|
-
* A DataSource adapter for the `provider: 'value'` ViewData mode.
|
|
9
|
-
* Operates entirely on an in-memory array — no network requests.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type {
|
|
13
|
-
DataSource,
|
|
14
|
-
QueryParams,
|
|
15
|
-
QueryResult,
|
|
16
|
-
AggregateParams,
|
|
17
|
-
AggregateResult,
|
|
18
|
-
} from '@object-ui/types';
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Configuration
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
export interface ValueDataSourceConfig<T = any> {
|
|
25
|
-
/** The static data array */
|
|
26
|
-
items: T[];
|
|
27
|
-
/** Optional ID field name for findOne/update/delete (defaults to 'id' then '_id') */
|
|
28
|
-
idField?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Helpers
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
/** Resolve the ID of a record given possible field names */
|
|
36
|
-
function getRecordId(record: any, idField?: string): string | number | undefined {
|
|
37
|
-
if (idField) return record[idField];
|
|
38
|
-
return record.id ?? record._id;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Evaluate an AST-format filter node against a record.
|
|
43
|
-
* Supports conditions like ['field', 'op', value] and logical
|
|
44
|
-
* combinations like ['and', ...conditions] or ['or', ...conditions].
|
|
45
|
-
*/
|
|
46
|
-
function matchesASTFilter(record: any, filterNode: any[]): boolean {
|
|
47
|
-
if (!filterNode || filterNode.length === 0) return true;
|
|
48
|
-
|
|
49
|
-
const head = filterNode[0];
|
|
50
|
-
|
|
51
|
-
// Logical operators: ['and', ...conditions] or ['or', ...conditions]
|
|
52
|
-
if (head === 'and') {
|
|
53
|
-
return filterNode.slice(1).every((sub: any) => matchesASTFilter(record, sub));
|
|
54
|
-
}
|
|
55
|
-
if (head === 'or') {
|
|
56
|
-
return filterNode.slice(1).some((sub: any) => matchesASTFilter(record, sub));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Condition node: [field, operator, value]
|
|
60
|
-
if (filterNode.length === 3 && typeof head === 'string') {
|
|
61
|
-
const [field, operator, target] = filterNode;
|
|
62
|
-
const value = record[field];
|
|
63
|
-
|
|
64
|
-
switch (operator) {
|
|
65
|
-
case '=':
|
|
66
|
-
return value === target;
|
|
67
|
-
case '!=':
|
|
68
|
-
return value !== target;
|
|
69
|
-
case '>':
|
|
70
|
-
return value > target;
|
|
71
|
-
case '>=':
|
|
72
|
-
return value >= target;
|
|
73
|
-
case '<':
|
|
74
|
-
return value < target;
|
|
75
|
-
case '<=':
|
|
76
|
-
return value <= target;
|
|
77
|
-
case 'in':
|
|
78
|
-
return Array.isArray(target) && target.includes(value);
|
|
79
|
-
case 'not in':
|
|
80
|
-
case 'notin': // alias used by convertFiltersToAST
|
|
81
|
-
return Array.isArray(target) && !target.includes(value);
|
|
82
|
-
case 'contains': {
|
|
83
|
-
const lv = typeof value === 'string' ? value.toLowerCase() : '';
|
|
84
|
-
return typeof value === 'string' && lv.includes(String(target).toLowerCase());
|
|
85
|
-
}
|
|
86
|
-
case 'notcontains': {
|
|
87
|
-
const lv = typeof value === 'string' ? value.toLowerCase() : '';
|
|
88
|
-
return typeof value === 'string' && !lv.includes(String(target).toLowerCase());
|
|
89
|
-
}
|
|
90
|
-
case 'startswith': {
|
|
91
|
-
const lv = typeof value === 'string' ? value.toLowerCase() : '';
|
|
92
|
-
return typeof value === 'string' && lv.startsWith(String(target).toLowerCase());
|
|
93
|
-
}
|
|
94
|
-
case 'between':
|
|
95
|
-
return Array.isArray(target) && target.length === 2 && value >= target[0] && value <= target[1];
|
|
96
|
-
default:
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Simple in-memory filter evaluation.
|
|
106
|
-
* Supports flat key-value equality and basic operators ($gt, $gte, $lt, $lte, $ne, $in).
|
|
107
|
-
*/
|
|
108
|
-
function matchesFilter(record: any, filter: Record<string, any>): boolean {
|
|
109
|
-
for (const [key, condition] of Object.entries(filter)) {
|
|
110
|
-
const value = record[key];
|
|
111
|
-
|
|
112
|
-
if (condition && typeof condition === 'object' && !Array.isArray(condition)) {
|
|
113
|
-
// Operator-based filter
|
|
114
|
-
for (const [op, target] of Object.entries(condition)) {
|
|
115
|
-
switch (op) {
|
|
116
|
-
case '$gt':
|
|
117
|
-
if (!(value > (target as any))) return false;
|
|
118
|
-
break;
|
|
119
|
-
case '$gte':
|
|
120
|
-
if (!(value >= (target as any))) return false;
|
|
121
|
-
break;
|
|
122
|
-
case '$lt':
|
|
123
|
-
if (!(value < (target as any))) return false;
|
|
124
|
-
break;
|
|
125
|
-
case '$lte':
|
|
126
|
-
if (!(value <= (target as any))) return false;
|
|
127
|
-
break;
|
|
128
|
-
case '$ne':
|
|
129
|
-
if (value === target) return false;
|
|
130
|
-
break;
|
|
131
|
-
case '$in':
|
|
132
|
-
if (!Array.isArray(target) || !target.includes(value)) return false;
|
|
133
|
-
break;
|
|
134
|
-
case '$contains':
|
|
135
|
-
if (typeof value !== 'string' || !value.toLowerCase().includes(String(target).toLowerCase())) return false;
|
|
136
|
-
break;
|
|
137
|
-
default:
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
// Simple equality
|
|
143
|
-
if (value !== condition) return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Apply sort ordering to an array (returns a new sorted array) */
|
|
150
|
-
function applySort<T>(
|
|
151
|
-
data: T[],
|
|
152
|
-
orderby?: QueryParams['$orderby'],
|
|
153
|
-
): T[] {
|
|
154
|
-
if (!orderby) return data;
|
|
155
|
-
|
|
156
|
-
// Normalize to array of { field, order }
|
|
157
|
-
let sorts: Array<{ field: string; order: 'asc' | 'desc' }> = [];
|
|
158
|
-
|
|
159
|
-
if (Array.isArray(orderby)) {
|
|
160
|
-
sorts = orderby.map((item) => {
|
|
161
|
-
if (typeof item === 'string') {
|
|
162
|
-
if (item.startsWith('-')) {
|
|
163
|
-
return { field: item.slice(1), order: 'desc' as const };
|
|
164
|
-
}
|
|
165
|
-
return { field: item, order: 'asc' as const };
|
|
166
|
-
}
|
|
167
|
-
return { field: item.field, order: (item.order ?? 'asc') as 'asc' | 'desc' };
|
|
168
|
-
});
|
|
169
|
-
} else if (typeof orderby === 'object') {
|
|
170
|
-
sorts = Object.entries(orderby).map(([field, order]) => ({
|
|
171
|
-
field,
|
|
172
|
-
order: order as 'asc' | 'desc',
|
|
173
|
-
}));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (sorts.length === 0) return data;
|
|
177
|
-
|
|
178
|
-
return [...data].sort((a: any, b: any) => {
|
|
179
|
-
for (const { field, order } of sorts) {
|
|
180
|
-
const av = a[field];
|
|
181
|
-
const bv = b[field];
|
|
182
|
-
if (av === bv) continue;
|
|
183
|
-
if (av == null) return order === 'asc' ? -1 : 1;
|
|
184
|
-
if (bv == null) return order === 'asc' ? 1 : -1;
|
|
185
|
-
const cmp = av < bv ? -1 : 1;
|
|
186
|
-
return order === 'asc' ? cmp : -cmp;
|
|
187
|
-
}
|
|
188
|
-
return 0;
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Pick specific fields from a record */
|
|
193
|
-
function selectFields<T>(record: T, fields?: string[]): T {
|
|
194
|
-
if (!fields || fields.length === 0) return record;
|
|
195
|
-
const out: any = {};
|
|
196
|
-
for (const f of fields) {
|
|
197
|
-
if (f in (record as any)) {
|
|
198
|
-
out[f] = (record as any)[f];
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return out as T;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ---------------------------------------------------------------------------
|
|
205
|
-
// ValueDataSource
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* ValueDataSource — an in-memory DataSource backed by a static array.
|
|
210
|
-
*
|
|
211
|
-
* Used when `ViewData.provider === 'value'`. All operations are synchronous
|
|
212
|
-
* (but wrapped in Promises to satisfy the DataSource interface). Supports
|
|
213
|
-
* basic filter, sort, pagination, and CRUD operations.
|
|
214
|
-
*
|
|
215
|
-
* @example
|
|
216
|
-
* ```ts
|
|
217
|
-
* const ds = new ValueDataSource({
|
|
218
|
-
* items: [
|
|
219
|
-
* { id: '1', name: 'Alice', age: 30 },
|
|
220
|
-
* { id: '2', name: 'Bob', age: 25 },
|
|
221
|
-
* ],
|
|
222
|
-
* });
|
|
223
|
-
*
|
|
224
|
-
* const result = await ds.find('users', { $filter: { age: { $gt: 26 } } });
|
|
225
|
-
* // result.data === [{ id: '1', name: 'Alice', age: 30 }]
|
|
226
|
-
* ```
|
|
227
|
-
*/
|
|
228
|
-
export class ValueDataSource<T = any> implements DataSource<T> {
|
|
229
|
-
private items: T[];
|
|
230
|
-
private idField: string | undefined;
|
|
231
|
-
|
|
232
|
-
constructor(config: ValueDataSourceConfig<T>) {
|
|
233
|
-
// Deep clone to prevent external mutation
|
|
234
|
-
this.items = JSON.parse(JSON.stringify(config.items));
|
|
235
|
-
this.idField = config.idField;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// -----------------------------------------------------------------------
|
|
239
|
-
// DataSource interface
|
|
240
|
-
// -----------------------------------------------------------------------
|
|
241
|
-
|
|
242
|
-
async find(_resource: string, params?: QueryParams): Promise<QueryResult<T>> {
|
|
243
|
-
let result = [...this.items];
|
|
244
|
-
|
|
245
|
-
// Filter — support both MongoDB-style objects and AST-format arrays
|
|
246
|
-
if (params?.$filter) {
|
|
247
|
-
if (Array.isArray(params.$filter) && params.$filter.length > 0) {
|
|
248
|
-
result = result.filter((r) => matchesASTFilter(r, params.$filter as any[]));
|
|
249
|
-
} else if (!Array.isArray(params.$filter) && Object.keys(params.$filter).length > 0) {
|
|
250
|
-
result = result.filter((r) => matchesFilter(r, params.$filter!));
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Search (simple text search across all string fields)
|
|
255
|
-
if (params?.$search) {
|
|
256
|
-
const q = params.$search.toLowerCase();
|
|
257
|
-
result = result.filter((r) =>
|
|
258
|
-
Object.values(r as any).some(
|
|
259
|
-
(v) => typeof v === 'string' && v.toLowerCase().includes(q),
|
|
260
|
-
),
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const totalCount = result.length;
|
|
265
|
-
|
|
266
|
-
// Sort
|
|
267
|
-
result = applySort(result, params?.$orderby);
|
|
268
|
-
|
|
269
|
-
// Pagination
|
|
270
|
-
const skip = params?.$skip ?? 0;
|
|
271
|
-
const top = params?.$top;
|
|
272
|
-
if (skip > 0) result = result.slice(skip);
|
|
273
|
-
if (top !== undefined) result = result.slice(0, top);
|
|
274
|
-
|
|
275
|
-
// Select
|
|
276
|
-
if (params?.$select?.length) {
|
|
277
|
-
result = result.map((r) => selectFields(r, params.$select));
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
data: result,
|
|
282
|
-
total: totalCount,
|
|
283
|
-
hasMore: skip + (top ?? result.length) < totalCount,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async findOne(
|
|
288
|
-
_resource: string,
|
|
289
|
-
id: string | number,
|
|
290
|
-
params?: QueryParams,
|
|
291
|
-
): Promise<T | null> {
|
|
292
|
-
const record = this.items.find(
|
|
293
|
-
(r) => String(getRecordId(r, this.idField)) === String(id),
|
|
294
|
-
);
|
|
295
|
-
if (!record) return null;
|
|
296
|
-
|
|
297
|
-
if (params?.$select?.length) {
|
|
298
|
-
return selectFields(record, params.$select);
|
|
299
|
-
}
|
|
300
|
-
return { ...record };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async create(_resource: string, data: Partial<T>): Promise<T> {
|
|
304
|
-
const record = { ...data } as T;
|
|
305
|
-
// Auto-generate an ID if missing
|
|
306
|
-
if (!getRecordId(record, this.idField)) {
|
|
307
|
-
const field = this.idField ?? 'id';
|
|
308
|
-
(record as any)[field] = `auto_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
309
|
-
}
|
|
310
|
-
this.items.push(record);
|
|
311
|
-
return { ...record };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async update(
|
|
315
|
-
_resource: string,
|
|
316
|
-
id: string | number,
|
|
317
|
-
data: Partial<T>,
|
|
318
|
-
): Promise<T> {
|
|
319
|
-
const index = this.items.findIndex(
|
|
320
|
-
(r) => String(getRecordId(r, this.idField)) === String(id),
|
|
321
|
-
);
|
|
322
|
-
if (index === -1) {
|
|
323
|
-
throw new Error(`ValueDataSource: Record with id "${id}" not found`);
|
|
324
|
-
}
|
|
325
|
-
this.items[index] = { ...this.items[index], ...data };
|
|
326
|
-
return { ...this.items[index] };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async delete(_resource: string, id: string | number): Promise<boolean> {
|
|
330
|
-
const index = this.items.findIndex(
|
|
331
|
-
(r) => String(getRecordId(r, this.idField)) === String(id),
|
|
332
|
-
);
|
|
333
|
-
if (index === -1) return false;
|
|
334
|
-
this.items.splice(index, 1);
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async bulk(
|
|
339
|
-
_resource: string,
|
|
340
|
-
operation: 'create' | 'update' | 'delete',
|
|
341
|
-
data: Partial<T>[],
|
|
342
|
-
): Promise<T[]> {
|
|
343
|
-
const results: T[] = [];
|
|
344
|
-
for (const item of data) {
|
|
345
|
-
switch (operation) {
|
|
346
|
-
case 'create':
|
|
347
|
-
results.push(await this.create(_resource, item));
|
|
348
|
-
break;
|
|
349
|
-
case 'update': {
|
|
350
|
-
const id = getRecordId(item, this.idField);
|
|
351
|
-
if (id !== undefined) {
|
|
352
|
-
results.push(await this.update(_resource, id, item));
|
|
353
|
-
}
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
case 'delete': {
|
|
357
|
-
const id = getRecordId(item, this.idField);
|
|
358
|
-
if (id !== undefined) {
|
|
359
|
-
await this.delete(_resource, id);
|
|
360
|
-
}
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
return results;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async getObjectSchema(_objectName: string): Promise<any> {
|
|
369
|
-
// Infer a minimal schema from the first item
|
|
370
|
-
if (this.items.length === 0) return { name: _objectName, fields: {} };
|
|
371
|
-
|
|
372
|
-
const sample = this.items[0];
|
|
373
|
-
const fields: Record<string, any> = {};
|
|
374
|
-
for (const [key, value] of Object.entries(sample as any)) {
|
|
375
|
-
fields[key] = { type: typeof value };
|
|
376
|
-
}
|
|
377
|
-
return { name: _objectName, fields };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async getView(_objectName: string, _viewId: string): Promise<any | null> {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
async getApp(_appId: string): Promise<any | null> {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
async aggregate(_resource: string, params: AggregateParams): Promise<AggregateResult[]> {
|
|
389
|
-
const { field, function: aggFn, groupBy } = params;
|
|
390
|
-
const groups: Record<string, any[]> = {};
|
|
391
|
-
|
|
392
|
-
for (const record of this.items as any[]) {
|
|
393
|
-
const key = String(record[groupBy] ?? 'Unknown');
|
|
394
|
-
if (!groups[key]) groups[key] = [];
|
|
395
|
-
groups[key].push(record);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return Object.entries(groups).map(([key, group]) => {
|
|
399
|
-
const values = group.map(r => Number(r[field]) || 0);
|
|
400
|
-
let result: number;
|
|
401
|
-
|
|
402
|
-
switch (aggFn) {
|
|
403
|
-
case 'count':
|
|
404
|
-
result = group.length;
|
|
405
|
-
break;
|
|
406
|
-
case 'avg':
|
|
407
|
-
result = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
|
408
|
-
break;
|
|
409
|
-
case 'min':
|
|
410
|
-
result = values.length > 0 ? Math.min(...values) : 0;
|
|
411
|
-
break;
|
|
412
|
-
case 'max':
|
|
413
|
-
result = values.length > 0 ? Math.max(...values) : 0;
|
|
414
|
-
break;
|
|
415
|
-
case 'sum':
|
|
416
|
-
default:
|
|
417
|
-
result = values.reduce((a, b) => a + b, 0);
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return { [groupBy]: key, [field]: result };
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// -----------------------------------------------------------------------
|
|
426
|
-
// Extra utilities
|
|
427
|
-
// -----------------------------------------------------------------------
|
|
428
|
-
|
|
429
|
-
/** Get the current number of items */
|
|
430
|
-
get count(): number {
|
|
431
|
-
return this.items.length;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/** Get a snapshot of all items (cloned) */
|
|
435
|
-
getAll(): T[] {
|
|
436
|
-
return JSON.parse(JSON.stringify(this.items));
|
|
437
|
-
}
|
|
438
|
-
}
|