@object-ui/core 0.5.0 → 3.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +28 -0
- package/dist/__benchmarks__/core.bench.d.ts +8 -0
- package/dist/__benchmarks__/core.bench.js +53 -0
- package/dist/actions/ActionRunner.d.ts +228 -4
- package/dist/actions/ActionRunner.js +397 -45
- package/dist/actions/TransactionManager.d.ts +193 -0
- package/dist/actions/TransactionManager.js +410 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/adapters/ApiDataSource.d.ts +69 -0
- package/dist/adapters/ApiDataSource.js +293 -0
- package/dist/adapters/ValueDataSource.d.ts +55 -0
- package/dist/adapters/ValueDataSource.js +287 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +5 -2
- package/dist/adapters/resolveDataSource.d.ts +40 -0
- package/dist/adapters/resolveDataSource.js +59 -0
- package/dist/data-scope/DataScopeManager.d.ts +127 -0
- package/dist/data-scope/DataScopeManager.js +229 -0
- package/dist/data-scope/index.d.ts +10 -0
- package/dist/data-scope/index.js +10 -0
- package/dist/errors/index.d.ts +75 -0
- package/dist/errors/index.js +224 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +11 -1
- package/dist/evaluator/ExpressionEvaluator.js +32 -8
- package/dist/evaluator/FormulaFunctions.d.ts +58 -0
- package/dist/evaluator/FormulaFunctions.js +350 -0
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -2
- package/dist/query/query-ast.d.ts +2 -2
- package/dist/query/query-ast.js +3 -3
- package/dist/registry/Registry.d.ts +10 -0
- package/dist/registry/Registry.js +9 -2
- package/dist/registry/WidgetRegistry.d.ts +120 -0
- package/dist/registry/WidgetRegistry.js +275 -0
- package/dist/theme/ThemeEngine.d.ts +105 -0
- package/dist/theme/ThemeEngine.js +469 -0
- package/dist/theme/index.d.ts +8 -0
- package/dist/theme/index.js +8 -0
- package/dist/utils/debug.d.ts +31 -0
- package/dist/utils/debug.js +62 -0
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/validation-engine.d.ts +19 -1
- package/dist/validation/validation-engine.js +74 -3
- package/dist/validation/validators/index.d.ts +1 -1
- package/dist/validation/validators/index.js +1 -1
- package/dist/validation/validators/object-validation-engine.d.ts +2 -2
- package/dist/validation/validators/object-validation-engine.js +1 -1
- package/package.json +4 -3
- package/src/__benchmarks__/core.bench.ts +64 -0
- package/src/actions/ActionRunner.ts +577 -55
- package/src/actions/TransactionManager.ts +521 -0
- package/src/actions/__tests__/ActionRunner.params.test.ts +134 -0
- package/src/actions/__tests__/ActionRunner.test.ts +711 -0
- package/src/actions/__tests__/TransactionManager.test.ts +447 -0
- package/src/actions/index.ts +1 -0
- package/src/adapters/ApiDataSource.ts +349 -0
- package/src/adapters/ValueDataSource.ts +332 -0
- package/src/adapters/__tests__/ApiDataSource.test.ts +418 -0
- package/src/adapters/__tests__/ValueDataSource.test.ts +325 -0
- package/src/adapters/__tests__/resolveDataSource.test.ts +144 -0
- package/src/adapters/index.ts +6 -1
- package/src/adapters/resolveDataSource.ts +79 -0
- package/src/builder/__tests__/schema-builder.test.ts +235 -0
- package/src/data-scope/DataScopeManager.ts +269 -0
- package/src/data-scope/__tests__/DataScopeManager.test.ts +211 -0
- package/src/data-scope/index.ts +16 -0
- package/src/errors/__tests__/errors.test.ts +292 -0
- package/src/errors/index.ts +270 -0
- package/src/evaluator/ExpressionEvaluator.ts +34 -8
- package/src/evaluator/FormulaFunctions.ts +398 -0
- package/src/evaluator/__tests__/ExpressionContext.test.ts +110 -0
- package/src/evaluator/__tests__/FormulaFunctions.test.ts +447 -0
- package/src/evaluator/index.ts +1 -0
- package/src/index.ts +6 -3
- package/src/query/__tests__/window-functions.test.ts +1 -1
- package/src/query/query-ast.ts +3 -3
- package/src/registry/Registry.ts +19 -2
- package/src/registry/WidgetRegistry.ts +316 -0
- package/src/registry/__tests__/WidgetRegistry.test.ts +321 -0
- package/src/theme/ThemeEngine.ts +530 -0
- package/src/theme/__tests__/ThemeEngine.test.ts +668 -0
- package/src/theme/index.ts +24 -0
- package/src/utils/__tests__/debug.test.ts +83 -0
- package/src/utils/debug.ts +66 -0
- package/src/validation/__tests__/object-validation-engine.test.ts +1 -1
- package/src/validation/__tests__/schema-validator.test.ts +118 -0
- package/src/validation/index.ts +1 -1
- package/src/validation/validation-engine.ts +70 -3
- package/src/validation/validators/index.ts +1 -1
- package/src/validation/validators/object-validation-engine.ts +2 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — ApiDataSource
|
|
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: 'api'` ViewData mode.
|
|
9
|
+
* Makes raw HTTP requests using the HttpRequest configs from ViewData.
|
|
10
|
+
*/
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/** Build a full URL with query params */
|
|
15
|
+
function buildUrl(base, pathSuffix, queryParams) {
|
|
16
|
+
let url = base;
|
|
17
|
+
if (pathSuffix) {
|
|
18
|
+
url = url.replace(/\/+$/, '') + '/' + pathSuffix.replace(/^\/+/, '');
|
|
19
|
+
}
|
|
20
|
+
if (queryParams && Object.keys(queryParams).length > 0) {
|
|
21
|
+
const search = new URLSearchParams();
|
|
22
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
23
|
+
if (value !== undefined && value !== null) {
|
|
24
|
+
search.set(key, String(value));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const qs = search.toString();
|
|
28
|
+
if (qs) {
|
|
29
|
+
url += (url.includes('?') ? '&' : '?') + qs;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return url;
|
|
33
|
+
}
|
|
34
|
+
/** Convert QueryParams to flat query string params */
|
|
35
|
+
function queryParamsToRecord(params) {
|
|
36
|
+
if (!params)
|
|
37
|
+
return {};
|
|
38
|
+
const out = {};
|
|
39
|
+
if (params.$select?.length) {
|
|
40
|
+
out.$select = params.$select.join(',');
|
|
41
|
+
}
|
|
42
|
+
if (params.$filter && Object.keys(params.$filter).length > 0) {
|
|
43
|
+
out.$filter = JSON.stringify(params.$filter);
|
|
44
|
+
}
|
|
45
|
+
if (params.$orderby) {
|
|
46
|
+
if (Array.isArray(params.$orderby)) {
|
|
47
|
+
if (typeof params.$orderby[0] === 'string') {
|
|
48
|
+
out.$orderby = params.$orderby.join(',');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
out.$orderby = params.$orderby
|
|
52
|
+
.map((s) => `${s.field} ${s.order || 'asc'}`)
|
|
53
|
+
.join(',');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
out.$orderby = Object.entries(params.$orderby)
|
|
58
|
+
.map(([field, order]) => `${field} ${order}`)
|
|
59
|
+
.join(',');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (params.$skip !== undefined)
|
|
63
|
+
out.$skip = params.$skip;
|
|
64
|
+
if (params.$top !== undefined)
|
|
65
|
+
out.$top = params.$top;
|
|
66
|
+
if (params.$expand?.length)
|
|
67
|
+
out.$expand = params.$expand.join(',');
|
|
68
|
+
if (params.$search)
|
|
69
|
+
out.$search = params.$search;
|
|
70
|
+
if (params.$count)
|
|
71
|
+
out.$count = 'true';
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** Merge two header objects, giving priority to the second */
|
|
75
|
+
function mergeHeaders(base, override) {
|
|
76
|
+
return { ...base, ...override };
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// ApiDataSource
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
/**
|
|
82
|
+
* ApiDataSource — a DataSource adapter for raw HTTP APIs.
|
|
83
|
+
*
|
|
84
|
+
* Used when `ViewData.provider === 'api'`. The read and write HttpRequest
|
|
85
|
+
* configs define the endpoints; all CRUD methods map onto HTTP verbs.
|
|
86
|
+
*
|
|
87
|
+
* Read operations use the `read` config, write operations use the `write` config.
|
|
88
|
+
* Both fall back to each other when one is not provided.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const ds = new ApiDataSource({
|
|
93
|
+
* read: { url: '/api/contacts', method: 'GET' },
|
|
94
|
+
* write: { url: '/api/contacts', method: 'POST' },
|
|
95
|
+
* });
|
|
96
|
+
*
|
|
97
|
+
* const result = await ds.find('contacts', { $top: 10 });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export class ApiDataSource {
|
|
101
|
+
constructor(config) {
|
|
102
|
+
Object.defineProperty(this, "readConfig", {
|
|
103
|
+
enumerable: true,
|
|
104
|
+
configurable: true,
|
|
105
|
+
writable: true,
|
|
106
|
+
value: void 0
|
|
107
|
+
});
|
|
108
|
+
Object.defineProperty(this, "writeConfig", {
|
|
109
|
+
enumerable: true,
|
|
110
|
+
configurable: true,
|
|
111
|
+
writable: true,
|
|
112
|
+
value: void 0
|
|
113
|
+
});
|
|
114
|
+
Object.defineProperty(this, "fetchFn", {
|
|
115
|
+
enumerable: true,
|
|
116
|
+
configurable: true,
|
|
117
|
+
writable: true,
|
|
118
|
+
value: void 0
|
|
119
|
+
});
|
|
120
|
+
Object.defineProperty(this, "defaultHeaders", {
|
|
121
|
+
enumerable: true,
|
|
122
|
+
configurable: true,
|
|
123
|
+
writable: true,
|
|
124
|
+
value: void 0
|
|
125
|
+
});
|
|
126
|
+
this.readConfig = config.read;
|
|
127
|
+
this.writeConfig = config.write;
|
|
128
|
+
this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
129
|
+
this.defaultHeaders = config.defaultHeaders ?? {};
|
|
130
|
+
}
|
|
131
|
+
// -----------------------------------------------------------------------
|
|
132
|
+
// Internal request executor
|
|
133
|
+
// -----------------------------------------------------------------------
|
|
134
|
+
async request(base, options = {}) {
|
|
135
|
+
if (!base) {
|
|
136
|
+
throw new Error('ApiDataSource: No HTTP configuration provided for this operation. ' +
|
|
137
|
+
'Ensure the ViewData has read/write configs.');
|
|
138
|
+
}
|
|
139
|
+
const method = options.method ?? base.method ?? 'GET';
|
|
140
|
+
// Merge query params: base.params + extra queryParams
|
|
141
|
+
const allQuery = {
|
|
142
|
+
...base.params,
|
|
143
|
+
...options.queryParams,
|
|
144
|
+
};
|
|
145
|
+
const url = buildUrl(base.url, options.pathSuffix, allQuery);
|
|
146
|
+
const headers = mergeHeaders(mergeHeaders(this.defaultHeaders, base.headers), options.headers);
|
|
147
|
+
const init = {
|
|
148
|
+
method,
|
|
149
|
+
headers,
|
|
150
|
+
};
|
|
151
|
+
// Attach body for non-GET methods
|
|
152
|
+
if (options.body !== undefined && method !== 'GET') {
|
|
153
|
+
if (options.body instanceof FormData ||
|
|
154
|
+
options.body instanceof Blob) {
|
|
155
|
+
init.body = options.body;
|
|
156
|
+
}
|
|
157
|
+
else if (typeof options.body === 'string') {
|
|
158
|
+
init.body = options.body;
|
|
159
|
+
if (!headers['Content-Type']) {
|
|
160
|
+
headers['Content-Type'] = 'text/plain';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
init.body = JSON.stringify(options.body);
|
|
165
|
+
if (!headers['Content-Type']) {
|
|
166
|
+
headers['Content-Type'] = 'application/json';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const response = await this.fetchFn(url, init);
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
const text = await response.text().catch(() => '');
|
|
173
|
+
throw new Error(`ApiDataSource: HTTP ${response.status} ${response.statusText} — ${text}`);
|
|
174
|
+
}
|
|
175
|
+
// Try to parse as JSON; fall back to empty object
|
|
176
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
177
|
+
if (contentType.includes('application/json')) {
|
|
178
|
+
return response.json();
|
|
179
|
+
}
|
|
180
|
+
// Non-JSON response — return text wrapped in an object
|
|
181
|
+
const text = await response.text();
|
|
182
|
+
return text;
|
|
183
|
+
}
|
|
184
|
+
// -----------------------------------------------------------------------
|
|
185
|
+
// DataSource interface
|
|
186
|
+
// -----------------------------------------------------------------------
|
|
187
|
+
async find(_resource, params) {
|
|
188
|
+
const queryParams = queryParamsToRecord(params);
|
|
189
|
+
const raw = await this.request(this.readConfig, {
|
|
190
|
+
method: 'GET',
|
|
191
|
+
queryParams,
|
|
192
|
+
});
|
|
193
|
+
// Normalize: the API might return an array, an object with `data`, or a QueryResult
|
|
194
|
+
return this.normalizeQueryResult(raw);
|
|
195
|
+
}
|
|
196
|
+
async findOne(_resource, id, params) {
|
|
197
|
+
try {
|
|
198
|
+
const queryParams = queryParamsToRecord(params);
|
|
199
|
+
const raw = await this.request(this.readConfig, {
|
|
200
|
+
pathSuffix: String(id),
|
|
201
|
+
method: 'GET',
|
|
202
|
+
queryParams,
|
|
203
|
+
});
|
|
204
|
+
return raw ?? null;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async create(_resource, data) {
|
|
211
|
+
const config = this.writeConfig ?? this.readConfig;
|
|
212
|
+
return this.request(config, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
body: data,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async update(_resource, id, data) {
|
|
218
|
+
const config = this.writeConfig ?? this.readConfig;
|
|
219
|
+
return this.request(config, {
|
|
220
|
+
pathSuffix: String(id),
|
|
221
|
+
method: 'PATCH',
|
|
222
|
+
body: data,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
async delete(_resource, id) {
|
|
226
|
+
const config = this.writeConfig ?? this.readConfig;
|
|
227
|
+
try {
|
|
228
|
+
await this.request(config, {
|
|
229
|
+
pathSuffix: String(id),
|
|
230
|
+
method: 'DELETE',
|
|
231
|
+
});
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async getObjectSchema(_objectName) {
|
|
239
|
+
// Generic API endpoints typically don't expose schema metadata.
|
|
240
|
+
// Return a minimal stub so schema-dependent components don't crash.
|
|
241
|
+
return { name: _objectName, fields: {} };
|
|
242
|
+
}
|
|
243
|
+
async getView(_objectName, _viewId) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
async getApp(_appId) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
// -----------------------------------------------------------------------
|
|
250
|
+
// Helpers
|
|
251
|
+
// -----------------------------------------------------------------------
|
|
252
|
+
/**
|
|
253
|
+
* Normalize various API response shapes into a QueryResult.
|
|
254
|
+
*
|
|
255
|
+
* Supported shapes:
|
|
256
|
+
* - `T[]` → wrap in QueryResult
|
|
257
|
+
* - `{ data: T[] }` → extract data
|
|
258
|
+
* - `{ items: T[] }` → extract items
|
|
259
|
+
* - `{ results: T[] }` → extract results
|
|
260
|
+
* - `{ records: T[] }` → extract records (Salesforce-style)
|
|
261
|
+
* - `{ value: T[] }` → extract value (OData-style)
|
|
262
|
+
* - Full QueryResult (has data + totalCount) → return as-is
|
|
263
|
+
*/
|
|
264
|
+
normalizeQueryResult(raw) {
|
|
265
|
+
if (Array.isArray(raw)) {
|
|
266
|
+
return { data: raw, total: raw.length };
|
|
267
|
+
}
|
|
268
|
+
if (raw && typeof raw === 'object') {
|
|
269
|
+
// Already a QueryResult
|
|
270
|
+
if (Array.isArray(raw.data) && ('total' in raw || 'totalCount' in raw)) {
|
|
271
|
+
return {
|
|
272
|
+
data: raw.data,
|
|
273
|
+
total: raw.total ?? raw.totalCount ?? raw.data.length,
|
|
274
|
+
hasMore: raw.hasMore,
|
|
275
|
+
cursor: raw.cursor,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// Common envelope patterns
|
|
279
|
+
for (const key of ['data', 'items', 'results', 'records', 'value']) {
|
|
280
|
+
if (Array.isArray(raw[key])) {
|
|
281
|
+
return {
|
|
282
|
+
data: raw[key],
|
|
283
|
+
total: raw.total ?? raw.totalCount ?? raw.count ?? raw[key].length,
|
|
284
|
+
hasMore: raw.hasMore ?? raw.hasNextPage,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Single-object response — wrap as array
|
|
289
|
+
return { data: [raw], total: 1 };
|
|
290
|
+
}
|
|
291
|
+
return { data: [], total: 0 };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
import type { DataSource, QueryParams, QueryResult } from '@object-ui/types';
|
|
12
|
+
export interface ValueDataSourceConfig<T = any> {
|
|
13
|
+
/** The static data array */
|
|
14
|
+
items: T[];
|
|
15
|
+
/** Optional ID field name for findOne/update/delete (defaults to '_id' then 'id') */
|
|
16
|
+
idField?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* ValueDataSource — an in-memory DataSource backed by a static array.
|
|
20
|
+
*
|
|
21
|
+
* Used when `ViewData.provider === 'value'`. All operations are synchronous
|
|
22
|
+
* (but wrapped in Promises to satisfy the DataSource interface). Supports
|
|
23
|
+
* basic filter, sort, pagination, and CRUD operations.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const ds = new ValueDataSource({
|
|
28
|
+
* items: [
|
|
29
|
+
* { id: '1', name: 'Alice', age: 30 },
|
|
30
|
+
* { id: '2', name: 'Bob', age: 25 },
|
|
31
|
+
* ],
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* const result = await ds.find('users', { $filter: { age: { $gt: 26 } } });
|
|
35
|
+
* // result.data === [{ id: '1', name: 'Alice', age: 30 }]
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare class ValueDataSource<T = any> implements DataSource<T> {
|
|
39
|
+
private items;
|
|
40
|
+
private idField;
|
|
41
|
+
constructor(config: ValueDataSourceConfig<T>);
|
|
42
|
+
find(_resource: string, params?: QueryParams): Promise<QueryResult<T>>;
|
|
43
|
+
findOne(_resource: string, id: string | number, params?: QueryParams): Promise<T | null>;
|
|
44
|
+
create(_resource: string, data: Partial<T>): Promise<T>;
|
|
45
|
+
update(_resource: string, id: string | number, data: Partial<T>): Promise<T>;
|
|
46
|
+
delete(_resource: string, id: string | number): Promise<boolean>;
|
|
47
|
+
bulk(_resource: string, operation: 'create' | 'update' | 'delete', data: Partial<T>[]): Promise<T[]>;
|
|
48
|
+
getObjectSchema(_objectName: string): Promise<any>;
|
|
49
|
+
getView(_objectName: string, _viewId: string): Promise<any | null>;
|
|
50
|
+
getApp(_appId: string): Promise<any | null>;
|
|
51
|
+
/** Get the current number of items */
|
|
52
|
+
get count(): number;
|
|
53
|
+
/** Get a snapshot of all items (cloned) */
|
|
54
|
+
getAll(): T[];
|
|
55
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/** Resolve the ID of a record given possible field names */
|
|
15
|
+
function getRecordId(record, idField) {
|
|
16
|
+
if (idField)
|
|
17
|
+
return record[idField];
|
|
18
|
+
return record._id ?? record.id;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Simple in-memory filter evaluation.
|
|
22
|
+
* Supports flat key-value equality and basic operators ($gt, $gte, $lt, $lte, $ne, $in).
|
|
23
|
+
*/
|
|
24
|
+
function matchesFilter(record, filter) {
|
|
25
|
+
for (const [key, condition] of Object.entries(filter)) {
|
|
26
|
+
const value = record[key];
|
|
27
|
+
if (condition && typeof condition === 'object' && !Array.isArray(condition)) {
|
|
28
|
+
// Operator-based filter
|
|
29
|
+
for (const [op, target] of Object.entries(condition)) {
|
|
30
|
+
switch (op) {
|
|
31
|
+
case '$gt':
|
|
32
|
+
if (!(value > target))
|
|
33
|
+
return false;
|
|
34
|
+
break;
|
|
35
|
+
case '$gte':
|
|
36
|
+
if (!(value >= target))
|
|
37
|
+
return false;
|
|
38
|
+
break;
|
|
39
|
+
case '$lt':
|
|
40
|
+
if (!(value < target))
|
|
41
|
+
return false;
|
|
42
|
+
break;
|
|
43
|
+
case '$lte':
|
|
44
|
+
if (!(value <= target))
|
|
45
|
+
return false;
|
|
46
|
+
break;
|
|
47
|
+
case '$ne':
|
|
48
|
+
if (value === target)
|
|
49
|
+
return false;
|
|
50
|
+
break;
|
|
51
|
+
case '$in':
|
|
52
|
+
if (!Array.isArray(target) || !target.includes(value))
|
|
53
|
+
return false;
|
|
54
|
+
break;
|
|
55
|
+
case '$contains':
|
|
56
|
+
if (typeof value !== 'string' || !value.toLowerCase().includes(String(target).toLowerCase()))
|
|
57
|
+
return false;
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Simple equality
|
|
66
|
+
if (value !== condition)
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
/** Apply sort ordering to an array (returns a new sorted array) */
|
|
73
|
+
function applySort(data, orderby) {
|
|
74
|
+
if (!orderby)
|
|
75
|
+
return data;
|
|
76
|
+
// Normalize to array of { field, order }
|
|
77
|
+
let sorts = [];
|
|
78
|
+
if (Array.isArray(orderby)) {
|
|
79
|
+
sorts = orderby.map((item) => {
|
|
80
|
+
if (typeof item === 'string') {
|
|
81
|
+
if (item.startsWith('-')) {
|
|
82
|
+
return { field: item.slice(1), order: 'desc' };
|
|
83
|
+
}
|
|
84
|
+
return { field: item, order: 'asc' };
|
|
85
|
+
}
|
|
86
|
+
return { field: item.field, order: (item.order ?? 'asc') };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else if (typeof orderby === 'object') {
|
|
90
|
+
sorts = Object.entries(orderby).map(([field, order]) => ({
|
|
91
|
+
field,
|
|
92
|
+
order: order,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
if (sorts.length === 0)
|
|
96
|
+
return data;
|
|
97
|
+
return [...data].sort((a, b) => {
|
|
98
|
+
for (const { field, order } of sorts) {
|
|
99
|
+
const av = a[field];
|
|
100
|
+
const bv = b[field];
|
|
101
|
+
if (av === bv)
|
|
102
|
+
continue;
|
|
103
|
+
if (av == null)
|
|
104
|
+
return order === 'asc' ? -1 : 1;
|
|
105
|
+
if (bv == null)
|
|
106
|
+
return order === 'asc' ? 1 : -1;
|
|
107
|
+
const cmp = av < bv ? -1 : 1;
|
|
108
|
+
return order === 'asc' ? cmp : -cmp;
|
|
109
|
+
}
|
|
110
|
+
return 0;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Pick specific fields from a record */
|
|
114
|
+
function selectFields(record, fields) {
|
|
115
|
+
if (!fields || fields.length === 0)
|
|
116
|
+
return record;
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const f of fields) {
|
|
119
|
+
if (f in record) {
|
|
120
|
+
out[f] = record[f];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// ValueDataSource
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
/**
|
|
129
|
+
* ValueDataSource — an in-memory DataSource backed by a static array.
|
|
130
|
+
*
|
|
131
|
+
* Used when `ViewData.provider === 'value'`. All operations are synchronous
|
|
132
|
+
* (but wrapped in Promises to satisfy the DataSource interface). Supports
|
|
133
|
+
* basic filter, sort, pagination, and CRUD operations.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const ds = new ValueDataSource({
|
|
138
|
+
* items: [
|
|
139
|
+
* { id: '1', name: 'Alice', age: 30 },
|
|
140
|
+
* { id: '2', name: 'Bob', age: 25 },
|
|
141
|
+
* ],
|
|
142
|
+
* });
|
|
143
|
+
*
|
|
144
|
+
* const result = await ds.find('users', { $filter: { age: { $gt: 26 } } });
|
|
145
|
+
* // result.data === [{ id: '1', name: 'Alice', age: 30 }]
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export class ValueDataSource {
|
|
149
|
+
constructor(config) {
|
|
150
|
+
Object.defineProperty(this, "items", {
|
|
151
|
+
enumerable: true,
|
|
152
|
+
configurable: true,
|
|
153
|
+
writable: true,
|
|
154
|
+
value: void 0
|
|
155
|
+
});
|
|
156
|
+
Object.defineProperty(this, "idField", {
|
|
157
|
+
enumerable: true,
|
|
158
|
+
configurable: true,
|
|
159
|
+
writable: true,
|
|
160
|
+
value: void 0
|
|
161
|
+
});
|
|
162
|
+
// Deep clone to prevent external mutation
|
|
163
|
+
this.items = JSON.parse(JSON.stringify(config.items));
|
|
164
|
+
this.idField = config.idField;
|
|
165
|
+
}
|
|
166
|
+
// -----------------------------------------------------------------------
|
|
167
|
+
// DataSource interface
|
|
168
|
+
// -----------------------------------------------------------------------
|
|
169
|
+
async find(_resource, params) {
|
|
170
|
+
let result = [...this.items];
|
|
171
|
+
// Filter
|
|
172
|
+
if (params?.$filter && Object.keys(params.$filter).length > 0) {
|
|
173
|
+
result = result.filter((r) => matchesFilter(r, params.$filter));
|
|
174
|
+
}
|
|
175
|
+
// Search (simple text search across all string fields)
|
|
176
|
+
if (params?.$search) {
|
|
177
|
+
const q = params.$search.toLowerCase();
|
|
178
|
+
result = result.filter((r) => Object.values(r).some((v) => typeof v === 'string' && v.toLowerCase().includes(q)));
|
|
179
|
+
}
|
|
180
|
+
const totalCount = result.length;
|
|
181
|
+
// Sort
|
|
182
|
+
result = applySort(result, params?.$orderby);
|
|
183
|
+
// Pagination
|
|
184
|
+
const skip = params?.$skip ?? 0;
|
|
185
|
+
const top = params?.$top;
|
|
186
|
+
if (skip > 0)
|
|
187
|
+
result = result.slice(skip);
|
|
188
|
+
if (top !== undefined)
|
|
189
|
+
result = result.slice(0, top);
|
|
190
|
+
// Select
|
|
191
|
+
if (params?.$select?.length) {
|
|
192
|
+
result = result.map((r) => selectFields(r, params.$select));
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
data: result,
|
|
196
|
+
total: totalCount,
|
|
197
|
+
hasMore: skip + (top ?? result.length) < totalCount,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async findOne(_resource, id, params) {
|
|
201
|
+
const record = this.items.find((r) => String(getRecordId(r, this.idField)) === String(id));
|
|
202
|
+
if (!record)
|
|
203
|
+
return null;
|
|
204
|
+
if (params?.$select?.length) {
|
|
205
|
+
return selectFields(record, params.$select);
|
|
206
|
+
}
|
|
207
|
+
return { ...record };
|
|
208
|
+
}
|
|
209
|
+
async create(_resource, data) {
|
|
210
|
+
const record = { ...data };
|
|
211
|
+
// Auto-generate an ID if missing
|
|
212
|
+
if (!getRecordId(record, this.idField)) {
|
|
213
|
+
const field = this.idField ?? '_id';
|
|
214
|
+
record[field] = `auto_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
215
|
+
}
|
|
216
|
+
this.items.push(record);
|
|
217
|
+
return { ...record };
|
|
218
|
+
}
|
|
219
|
+
async update(_resource, id, data) {
|
|
220
|
+
const index = this.items.findIndex((r) => String(getRecordId(r, this.idField)) === String(id));
|
|
221
|
+
if (index === -1) {
|
|
222
|
+
throw new Error(`ValueDataSource: Record with id "${id}" not found`);
|
|
223
|
+
}
|
|
224
|
+
this.items[index] = { ...this.items[index], ...data };
|
|
225
|
+
return { ...this.items[index] };
|
|
226
|
+
}
|
|
227
|
+
async delete(_resource, id) {
|
|
228
|
+
const index = this.items.findIndex((r) => String(getRecordId(r, this.idField)) === String(id));
|
|
229
|
+
if (index === -1)
|
|
230
|
+
return false;
|
|
231
|
+
this.items.splice(index, 1);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
async bulk(_resource, operation, data) {
|
|
235
|
+
const results = [];
|
|
236
|
+
for (const item of data) {
|
|
237
|
+
switch (operation) {
|
|
238
|
+
case 'create':
|
|
239
|
+
results.push(await this.create(_resource, item));
|
|
240
|
+
break;
|
|
241
|
+
case 'update': {
|
|
242
|
+
const id = getRecordId(item, this.idField);
|
|
243
|
+
if (id !== undefined) {
|
|
244
|
+
results.push(await this.update(_resource, id, item));
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case 'delete': {
|
|
249
|
+
const id = getRecordId(item, this.idField);
|
|
250
|
+
if (id !== undefined) {
|
|
251
|
+
await this.delete(_resource, id);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return results;
|
|
258
|
+
}
|
|
259
|
+
async getObjectSchema(_objectName) {
|
|
260
|
+
// Infer a minimal schema from the first item
|
|
261
|
+
if (this.items.length === 0)
|
|
262
|
+
return { name: _objectName, fields: {} };
|
|
263
|
+
const sample = this.items[0];
|
|
264
|
+
const fields = {};
|
|
265
|
+
for (const [key, value] of Object.entries(sample)) {
|
|
266
|
+
fields[key] = { type: typeof value };
|
|
267
|
+
}
|
|
268
|
+
return { name: _objectName, fields };
|
|
269
|
+
}
|
|
270
|
+
async getView(_objectName, _viewId) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
async getApp(_appId) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
// -----------------------------------------------------------------------
|
|
277
|
+
// Extra utilities
|
|
278
|
+
// -----------------------------------------------------------------------
|
|
279
|
+
/** Get the current number of items */
|
|
280
|
+
get count() {
|
|
281
|
+
return this.items.length;
|
|
282
|
+
}
|
|
283
|
+
/** Get a snapshot of all items (cloned) */
|
|
284
|
+
getAll() {
|
|
285
|
+
return JSON.parse(JSON.stringify(this.items));
|
|
286
|
+
}
|
|
287
|
+
}
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -5,3 +5,6 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
export { ApiDataSource, type ApiDataSourceConfig } from './ApiDataSource.js';
|
|
9
|
+
export { ValueDataSource, type ValueDataSourceConfig } from './ValueDataSource.js';
|
|
10
|
+
export { resolveDataSource, type ResolveDataSourceOptions } from './resolveDataSource.js';
|
package/dist/adapters/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* ObjectUI
|
|
4
3
|
* Copyright (c) 2024-present ObjectStack Inc.
|
|
@@ -7,4 +6,8 @@
|
|
|
7
6
|
* LICENSE file in the root directory of this source tree.
|
|
8
7
|
*/
|
|
9
8
|
// export { ObjectStackAdapter, createObjectStackAdapter } from './objectstack-adapter';
|
|
10
|
-
//
|
|
9
|
+
// ObjectStack adapter has been moved to @object-ui/data-objectstack
|
|
10
|
+
// Generic data source adapters (no external SDK dependencies)
|
|
11
|
+
export { ApiDataSource } from './ApiDataSource.js';
|
|
12
|
+
export { ValueDataSource } from './ValueDataSource.js';
|
|
13
|
+
export { resolveDataSource } from './resolveDataSource.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — resolveDataSource
|
|
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
|
+
* Factory function to create the right DataSource from a ViewData config.
|
|
9
|
+
*/
|
|
10
|
+
import type { DataSource, ViewData } from '@object-ui/types';
|
|
11
|
+
export interface ResolveDataSourceOptions {
|
|
12
|
+
/** Custom fetch implementation passed to ApiDataSource */
|
|
13
|
+
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
14
|
+
/** Default headers for API requests */
|
|
15
|
+
defaultHeaders?: Record<string, string>;
|
|
16
|
+
/** Custom ID field for ValueDataSource */
|
|
17
|
+
idField?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a ViewData configuration into a concrete DataSource instance.
|
|
21
|
+
*
|
|
22
|
+
* - `provider: 'object'` → returns `fallback` (the context DataSource — typically ObjectStackAdapter)
|
|
23
|
+
* - `provider: 'api'` → returns a new `ApiDataSource`
|
|
24
|
+
* - `provider: 'value'` → returns a new `ValueDataSource`
|
|
25
|
+
*
|
|
26
|
+
* @param viewData - The ViewData configuration from the schema
|
|
27
|
+
* @param fallback - The default DataSource from context (for `provider: 'object'`)
|
|
28
|
+
* @param options - Additional options for adapter construction
|
|
29
|
+
* @returns A DataSource instance, or null if neither viewData nor fallback is available
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const ds = resolveDataSource(
|
|
34
|
+
* { provider: 'api', read: { url: '/api/users' } },
|
|
35
|
+
* contextDataSource,
|
|
36
|
+
* );
|
|
37
|
+
* const result = await ds.find('users');
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveDataSource<T = any>(viewData: ViewData | null | undefined, fallback?: DataSource<T> | null, options?: ResolveDataSourceOptions): DataSource<T> | null;
|