@object-ui/core 3.1.1 → 3.1.3
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 +12 -0
- package/dist/adapters/ValueDataSource.d.ts +1 -1
- package/dist/adapters/ValueDataSource.js +2 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/registry/PluginSystem.d.ts +21 -1
- package/dist/registry/PluginSystem.js +40 -0
- package/dist/utils/merge-views-into-objects.d.ts +19 -0
- package/dist/utils/merge-views-into-objects.js +39 -0
- package/package.json +3 -3
- package/src/adapters/ValueDataSource.ts +3 -3
- package/src/adapters/__tests__/ValueDataSource.test.ts +19 -19
- package/src/index.ts +9 -0
- package/src/registry/PluginSystem.ts +46 -1
- package/src/registry/__tests__/PluginSystem.test.ts +83 -0
- package/src/utils/__tests__/merge-views-into-objects.test.ts +110 -0
- package/src/utils/merge-views-into-objects.ts +36 -0
- package/tsconfig.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -12,7 +12,7 @@ import type { DataSource, QueryParams, QueryResult, AggregateParams, AggregateRe
|
|
|
12
12
|
export interface ValueDataSourceConfig<T = any> {
|
|
13
13
|
/** The static data array */
|
|
14
14
|
items: T[];
|
|
15
|
-
/** Optional ID field name for findOne/update/delete (defaults to '
|
|
15
|
+
/** Optional ID field name for findOne/update/delete (defaults to 'id' then '_id') */
|
|
16
16
|
idField?: string;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
function getRecordId(record, idField) {
|
|
16
16
|
if (idField)
|
|
17
17
|
return record[idField];
|
|
18
|
-
return record.
|
|
18
|
+
return record.id ?? record._id;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Evaluate an AST-format filter node against a record.
|
|
@@ -273,7 +273,7 @@ export class ValueDataSource {
|
|
|
273
273
|
const record = { ...data };
|
|
274
274
|
// Auto-generate an ID if missing
|
|
275
275
|
if (!getRecordId(record, this.idField)) {
|
|
276
|
-
const field = this.idField ?? '
|
|
276
|
+
const field = this.idField ?? 'id';
|
|
277
277
|
record[field] = `auto_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
278
278
|
}
|
|
279
279
|
this.items.push(record);
|
package/dist/index.d.ts
CHANGED
|
@@ -25,4 +25,12 @@ export * from './data-scope/index.js';
|
|
|
25
25
|
export * from './errors/index.js';
|
|
26
26
|
export * from './utils/debug.js';
|
|
27
27
|
export * from './utils/debug-collector.js';
|
|
28
|
+
export * from './utils/merge-views-into-objects.js';
|
|
28
29
|
export * from './protocols/index.js';
|
|
30
|
+
/**
|
|
31
|
+
* @deprecated Import `composeStacks` from `@objectstack/spec` instead.
|
|
32
|
+
*
|
|
33
|
+
* This re-export is kept only for backward compatibility and will be removed
|
|
34
|
+
* in the next major version of `@object-ui/core`.
|
|
35
|
+
*/
|
|
36
|
+
export { composeStacks } from '@objectstack/spec';
|
package/dist/index.js
CHANGED
|
@@ -24,4 +24,12 @@ export * from './data-scope/index.js';
|
|
|
24
24
|
export * from './errors/index.js';
|
|
25
25
|
export * from './utils/debug.js';
|
|
26
26
|
export * from './utils/debug-collector.js';
|
|
27
|
+
export * from './utils/merge-views-into-objects.js';
|
|
27
28
|
export * from './protocols/index.js';
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated Import `composeStacks` from `@objectstack/spec` instead.
|
|
31
|
+
*
|
|
32
|
+
* This re-export is kept only for backward compatibility and will be removed
|
|
33
|
+
* in the next major version of `@object-ui/core`.
|
|
34
|
+
*/
|
|
35
|
+
export { composeStacks } from '@objectstack/spec';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import type { Registry } from './Registry.js';
|
|
9
|
-
import type { PluginScope, PluginScopeConfig } from '@object-ui/types';
|
|
9
|
+
import type { PluginScope, PluginScopeConfig, AppMetadataPlugin, AppPluginContext } from '@object-ui/types';
|
|
10
10
|
export interface PluginDefinition {
|
|
11
11
|
name: string;
|
|
12
12
|
version: string;
|
|
@@ -63,4 +63,24 @@ export declare class PluginSystem {
|
|
|
63
63
|
* @returns Array of all plugin definitions
|
|
64
64
|
*/
|
|
65
65
|
getAllPlugins(): PluginDefinition[];
|
|
66
|
+
/**
|
|
67
|
+
* Install an AppMetadataPlugin at runtime.
|
|
68
|
+
*
|
|
69
|
+
* Wraps the plugin as a PluginDefinition, calls its `init()` and `start()`
|
|
70
|
+
* lifecycle hooks, and loads it into the system.
|
|
71
|
+
*
|
|
72
|
+
* @param plugin - An AppMetadataPlugin instance
|
|
73
|
+
* @param registry - The component registry
|
|
74
|
+
* @param ctx - Optional context passed to the plugin's start() hook
|
|
75
|
+
*/
|
|
76
|
+
install(plugin: AppMetadataPlugin, registry: Registry, ctx?: AppPluginContext): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Uninstall an AppMetadataPlugin at runtime.
|
|
79
|
+
*
|
|
80
|
+
* Calls the plugin's `stop()` lifecycle hook (via onUnload) and
|
|
81
|
+
* removes it from the system.
|
|
82
|
+
*
|
|
83
|
+
* @param pluginName - Name of the plugin to uninstall
|
|
84
|
+
*/
|
|
85
|
+
uninstall(pluginName: string): Promise<void>;
|
|
66
86
|
}
|
|
@@ -139,4 +139,44 @@ export class PluginSystem {
|
|
|
139
139
|
getAllPlugins() {
|
|
140
140
|
return Array.from(this.plugins.values());
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Install an AppMetadataPlugin at runtime.
|
|
144
|
+
*
|
|
145
|
+
* Wraps the plugin as a PluginDefinition, calls its `init()` and `start()`
|
|
146
|
+
* lifecycle hooks, and loads it into the system.
|
|
147
|
+
*
|
|
148
|
+
* @param plugin - An AppMetadataPlugin instance
|
|
149
|
+
* @param registry - The component registry
|
|
150
|
+
* @param ctx - Optional context passed to the plugin's start() hook
|
|
151
|
+
*/
|
|
152
|
+
async install(plugin, registry, ctx) {
|
|
153
|
+
if (this.loaded.has(plugin.name)) {
|
|
154
|
+
console.warn(`Plugin "${plugin.name}" is already installed. Skipping.`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await plugin.init();
|
|
158
|
+
const definition = {
|
|
159
|
+
name: plugin.name,
|
|
160
|
+
version: plugin.version,
|
|
161
|
+
register: () => { },
|
|
162
|
+
onLoad: async () => {
|
|
163
|
+
await plugin.start(ctx ?? { logger: console });
|
|
164
|
+
},
|
|
165
|
+
onUnload: async () => {
|
|
166
|
+
await plugin.stop();
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
await this.loadPlugin(definition, registry);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Uninstall an AppMetadataPlugin at runtime.
|
|
173
|
+
*
|
|
174
|
+
* Calls the plugin's `stop()` lifecycle hook (via onUnload) and
|
|
175
|
+
* removes it from the system.
|
|
176
|
+
*
|
|
177
|
+
* @param pluginName - Name of the plugin to uninstall
|
|
178
|
+
*/
|
|
179
|
+
async uninstall(pluginName) {
|
|
180
|
+
await this.unloadPlugin(pluginName);
|
|
181
|
+
}
|
|
142
182
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
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
|
+
/**
|
|
9
|
+
* Adapter: merge stack-level views into object definitions.
|
|
10
|
+
*
|
|
11
|
+
* Views are defined at the stack level (views[].listViews) but the runtime
|
|
12
|
+
* expects listViews on each object definition. This bridges the gap until
|
|
13
|
+
* the runtime/provider layer handles it natively.
|
|
14
|
+
*
|
|
15
|
+
* @param objects - Object definitions from composed stack
|
|
16
|
+
* @param views - View definitions containing listViews keyed by object name
|
|
17
|
+
* @returns Objects with listViews merged in from matching views
|
|
18
|
+
*/
|
|
19
|
+
export declare function mergeViewsIntoObjects(objects: any[], views: any[]): any[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
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
|
+
/**
|
|
9
|
+
* Adapter: merge stack-level views into object definitions.
|
|
10
|
+
*
|
|
11
|
+
* Views are defined at the stack level (views[].listViews) but the runtime
|
|
12
|
+
* expects listViews on each object definition. This bridges the gap until
|
|
13
|
+
* the runtime/provider layer handles it natively.
|
|
14
|
+
*
|
|
15
|
+
* @param objects - Object definitions from composed stack
|
|
16
|
+
* @param views - View definitions containing listViews keyed by object name
|
|
17
|
+
* @returns Objects with listViews merged in from matching views
|
|
18
|
+
*/
|
|
19
|
+
export function mergeViewsIntoObjects(objects, views) {
|
|
20
|
+
const viewsByObject = {};
|
|
21
|
+
for (const view of views) {
|
|
22
|
+
if (!view.listViews)
|
|
23
|
+
continue;
|
|
24
|
+
for (const [viewName, listView] of Object.entries(view.listViews)) {
|
|
25
|
+
const objectName = listView?.data?.object;
|
|
26
|
+
if (!objectName)
|
|
27
|
+
continue;
|
|
28
|
+
if (!viewsByObject[objectName])
|
|
29
|
+
viewsByObject[objectName] = {};
|
|
30
|
+
viewsByObject[objectName][viewName] = listView;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return objects.map((obj) => {
|
|
34
|
+
const v = viewsByObject[obj.name];
|
|
35
|
+
if (!v)
|
|
36
|
+
return obj;
|
|
37
|
+
return { ...obj, listViews: { ...(obj.listViews || {}), ...v } };
|
|
38
|
+
});
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@objectstack/spec": "^3.2.
|
|
28
|
+
"@objectstack/spec": "^3.2.6",
|
|
29
29
|
"lodash": "^4.17.23",
|
|
30
30
|
"zod": "^4.3.6",
|
|
31
|
-
"@object-ui/types": "3.1.
|
|
31
|
+
"@object-ui/types": "3.1.3"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"typescript": "^5.9.3",
|
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
export interface ValueDataSourceConfig<T = any> {
|
|
25
25
|
/** The static data array */
|
|
26
26
|
items: T[];
|
|
27
|
-
/** Optional ID field name for findOne/update/delete (defaults to '
|
|
27
|
+
/** Optional ID field name for findOne/update/delete (defaults to 'id' then '_id') */
|
|
28
28
|
idField?: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -35,7 +35,7 @@ export interface ValueDataSourceConfig<T = any> {
|
|
|
35
35
|
/** Resolve the ID of a record given possible field names */
|
|
36
36
|
function getRecordId(record: any, idField?: string): string | number | undefined {
|
|
37
37
|
if (idField) return record[idField];
|
|
38
|
-
return record.
|
|
38
|
+
return record.id ?? record._id;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -304,7 +304,7 @@ export class ValueDataSource<T = any> implements DataSource<T> {
|
|
|
304
304
|
const record = { ...data } as T;
|
|
305
305
|
// Auto-generate an ID if missing
|
|
306
306
|
if (!getRecordId(record, this.idField)) {
|
|
307
|
-
const field = this.idField ?? '
|
|
307
|
+
const field = this.idField ?? 'id';
|
|
308
308
|
(record as any)[field] = `auto_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
309
309
|
}
|
|
310
310
|
this.items.push(record);
|
|
@@ -10,11 +10,11 @@ import { describe, it, expect } from 'vitest';
|
|
|
10
10
|
import { ValueDataSource } from '../ValueDataSource';
|
|
11
11
|
|
|
12
12
|
const sampleData = [
|
|
13
|
-
{
|
|
14
|
-
{
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
13
|
+
{ id: '1', name: 'Alice', age: 30, role: 'admin' },
|
|
14
|
+
{ id: '2', name: 'Bob', age: 25, role: 'user' },
|
|
15
|
+
{ id: '3', name: 'Charlie', age: 35, role: 'admin' },
|
|
16
|
+
{ id: '4', name: 'Diana', age: 28, role: 'user' },
|
|
17
|
+
{ id: '5', name: 'Eve', age: 22, role: 'guest' },
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
function createDS() {
|
|
@@ -132,7 +132,7 @@ describe('ValueDataSource — find', () => {
|
|
|
132
132
|
const first = result.data[0] as any;
|
|
133
133
|
expect(first.name).toBeDefined();
|
|
134
134
|
expect(first.age).toBeDefined();
|
|
135
|
-
expect(first.
|
|
135
|
+
expect(first.id).toBeUndefined();
|
|
136
136
|
expect(first.role).toBeUndefined();
|
|
137
137
|
});
|
|
138
138
|
|
|
@@ -252,7 +252,7 @@ describe('ValueDataSource — AST filter', () => {
|
|
|
252
252
|
// ---------------------------------------------------------------------------
|
|
253
253
|
|
|
254
254
|
describe('ValueDataSource — findOne', () => {
|
|
255
|
-
it('should find a record by
|
|
255
|
+
it('should find a record by id', async () => {
|
|
256
256
|
const ds = createDS();
|
|
257
257
|
const record = await ds.findOne('users', '3');
|
|
258
258
|
expect(record).not.toBeNull();
|
|
@@ -299,14 +299,14 @@ describe('ValueDataSource — create', () => {
|
|
|
299
299
|
it('should auto-generate an ID if missing', async () => {
|
|
300
300
|
const ds = createDS();
|
|
301
301
|
const created = await ds.create('users', { name: 'Grace' });
|
|
302
|
-
expect((created as any).
|
|
303
|
-
expect(String((created as any).
|
|
302
|
+
expect((created as any).id).toBeDefined();
|
|
303
|
+
expect(String((created as any).id).startsWith('auto_')).toBe(true);
|
|
304
304
|
});
|
|
305
305
|
|
|
306
306
|
it('should preserve existing ID', async () => {
|
|
307
307
|
const ds = createDS();
|
|
308
|
-
const created = await ds.create('users', {
|
|
309
|
-
expect((created as any).
|
|
308
|
+
const created = await ds.create('users', { id: 'custom-id', name: 'Heidi' });
|
|
309
|
+
expect((created as any).id).toBe('custom-id');
|
|
310
310
|
});
|
|
311
311
|
});
|
|
312
312
|
|
|
@@ -374,7 +374,7 @@ describe('ValueDataSource — bulk', () => {
|
|
|
374
374
|
|
|
375
375
|
it('should bulk delete records', async () => {
|
|
376
376
|
const ds = createDS();
|
|
377
|
-
await ds.bulk!('users', 'delete', [{
|
|
377
|
+
await ds.bulk!('users', 'delete', [{ id: '1' }, { id: '2' }]);
|
|
378
378
|
expect(ds.count).toBe(3);
|
|
379
379
|
});
|
|
380
380
|
});
|
|
@@ -388,7 +388,7 @@ describe('ValueDataSource — getObjectSchema', () => {
|
|
|
388
388
|
const ds = createDS();
|
|
389
389
|
const schema = await ds.getObjectSchema('users');
|
|
390
390
|
expect(schema.name).toBe('users');
|
|
391
|
-
expect(schema.fields.
|
|
391
|
+
expect(schema.fields.id).toBeDefined();
|
|
392
392
|
expect(schema.fields.name.type).toBe('string');
|
|
393
393
|
expect(schema.fields.age.type).toBe('number');
|
|
394
394
|
});
|
|
@@ -406,7 +406,7 @@ describe('ValueDataSource — getObjectSchema', () => {
|
|
|
406
406
|
|
|
407
407
|
describe('ValueDataSource — isolation', () => {
|
|
408
408
|
it('should not mutate the original items array', async () => {
|
|
409
|
-
const original = [{
|
|
409
|
+
const original = [{ id: '1', name: 'Test' }];
|
|
410
410
|
const ds = new ValueDataSource({ items: original });
|
|
411
411
|
await ds.create('x', { name: 'New' });
|
|
412
412
|
expect(original).toHaveLength(1); // original untouched
|
|
@@ -420,11 +420,11 @@ describe('ValueDataSource — isolation', () => {
|
|
|
420
420
|
|
|
421
421
|
describe('ValueDataSource — aggregate', () => {
|
|
422
422
|
const aggData = [
|
|
423
|
-
{
|
|
424
|
-
{
|
|
425
|
-
{
|
|
426
|
-
{
|
|
427
|
-
{
|
|
423
|
+
{ id: '1', category: 'A', amount: 10 },
|
|
424
|
+
{ id: '2', category: 'A', amount: 20 },
|
|
425
|
+
{ id: '3', category: 'B', amount: 30 },
|
|
426
|
+
{ id: '4', category: 'B', amount: 40 },
|
|
427
|
+
{ id: '5', category: 'B', amount: 50 },
|
|
428
428
|
];
|
|
429
429
|
|
|
430
430
|
function createAggDS() {
|
package/src/index.ts
CHANGED
|
@@ -26,4 +26,13 @@ export * from './data-scope/index.js';
|
|
|
26
26
|
export * from './errors/index.js';
|
|
27
27
|
export * from './utils/debug.js';
|
|
28
28
|
export * from './utils/debug-collector.js';
|
|
29
|
+
export * from './utils/merge-views-into-objects.js';
|
|
29
30
|
export * from './protocols/index.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @deprecated Import `composeStacks` from `@objectstack/spec` instead.
|
|
34
|
+
*
|
|
35
|
+
* This re-export is kept only for backward compatibility and will be removed
|
|
36
|
+
* in the next major version of `@object-ui/core`.
|
|
37
|
+
*/
|
|
38
|
+
export { composeStacks } from '@objectstack/spec';
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Registry } from './Registry.js';
|
|
10
|
-
import type { PluginScope, PluginScopeConfig } from '@object-ui/types';
|
|
10
|
+
import type { PluginScope, PluginScopeConfig, AppMetadataPlugin, AppPluginContext } from '@object-ui/types';
|
|
11
11
|
import { PluginScopeImpl } from './PluginScopeImpl.js';
|
|
12
12
|
|
|
13
13
|
export interface PluginDefinition {
|
|
@@ -158,4 +158,49 @@ export class PluginSystem {
|
|
|
158
158
|
getAllPlugins(): PluginDefinition[] {
|
|
159
159
|
return Array.from(this.plugins.values());
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Install an AppMetadataPlugin at runtime.
|
|
164
|
+
*
|
|
165
|
+
* Wraps the plugin as a PluginDefinition, calls its `init()` and `start()`
|
|
166
|
+
* lifecycle hooks, and loads it into the system.
|
|
167
|
+
*
|
|
168
|
+
* @param plugin - An AppMetadataPlugin instance
|
|
169
|
+
* @param registry - The component registry
|
|
170
|
+
* @param ctx - Optional context passed to the plugin's start() hook
|
|
171
|
+
*/
|
|
172
|
+
async install(plugin: AppMetadataPlugin, registry: Registry, ctx?: AppPluginContext): Promise<void> {
|
|
173
|
+
if (this.loaded.has(plugin.name)) {
|
|
174
|
+
console.warn(`Plugin "${plugin.name}" is already installed. Skipping.`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await plugin.init();
|
|
179
|
+
|
|
180
|
+
const definition: PluginDefinition = {
|
|
181
|
+
name: plugin.name,
|
|
182
|
+
version: plugin.version,
|
|
183
|
+
register: () => {},
|
|
184
|
+
onLoad: async () => {
|
|
185
|
+
await plugin.start(ctx ?? { logger: console });
|
|
186
|
+
},
|
|
187
|
+
onUnload: async () => {
|
|
188
|
+
await plugin.stop();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
await this.loadPlugin(definition, registry);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Uninstall an AppMetadataPlugin at runtime.
|
|
197
|
+
*
|
|
198
|
+
* Calls the plugin's `stop()` lifecycle hook (via onUnload) and
|
|
199
|
+
* removes it from the system.
|
|
200
|
+
*
|
|
201
|
+
* @param pluginName - Name of the plugin to uninstall
|
|
202
|
+
*/
|
|
203
|
+
async uninstall(pluginName: string): Promise<void> {
|
|
204
|
+
await this.unloadPlugin(pluginName);
|
|
205
|
+
}
|
|
161
206
|
}
|
|
@@ -223,4 +223,87 @@ describe('PluginSystem', () => {
|
|
|
223
223
|
expect(pluginSystem.isLoaded('failing-plugin')).toBe(false);
|
|
224
224
|
expect(pluginSystem.getPlugin('failing-plugin')).toBeUndefined();
|
|
225
225
|
});
|
|
226
|
+
|
|
227
|
+
describe('install / uninstall (AppMetadataPlugin)', () => {
|
|
228
|
+
it('should install an AppMetadataPlugin and call init + start', async () => {
|
|
229
|
+
const initFn = vi.fn();
|
|
230
|
+
const startFn = vi.fn();
|
|
231
|
+
const stopFn = vi.fn();
|
|
232
|
+
|
|
233
|
+
const appPlugin = {
|
|
234
|
+
name: '@test/my-plugin',
|
|
235
|
+
version: '1.0.0',
|
|
236
|
+
type: 'app-metadata' as const,
|
|
237
|
+
description: 'Test metadata plugin',
|
|
238
|
+
init: initFn,
|
|
239
|
+
start: startFn,
|
|
240
|
+
stop: stopFn,
|
|
241
|
+
getConfig: () => ({ objects: [] }),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
await pluginSystem.install(appPlugin, registry, { logger: console });
|
|
245
|
+
|
|
246
|
+
expect(initFn).toHaveBeenCalledTimes(1);
|
|
247
|
+
expect(startFn).toHaveBeenCalledTimes(1);
|
|
248
|
+
expect(startFn).toHaveBeenCalledWith({ logger: console });
|
|
249
|
+
expect(pluginSystem.isLoaded('@test/my-plugin')).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should not install the same plugin twice', async () => {
|
|
253
|
+
const appPlugin = {
|
|
254
|
+
name: '@test/dup-plugin',
|
|
255
|
+
version: '1.0.0',
|
|
256
|
+
type: 'app-metadata' as const,
|
|
257
|
+
init: vi.fn(),
|
|
258
|
+
start: vi.fn(),
|
|
259
|
+
stop: vi.fn(),
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
await pluginSystem.install(appPlugin, registry);
|
|
263
|
+
await pluginSystem.install(appPlugin, registry);
|
|
264
|
+
|
|
265
|
+
expect(appPlugin.init).toHaveBeenCalledTimes(1);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should uninstall a plugin and call stop', async () => {
|
|
269
|
+
const stopFn = vi.fn();
|
|
270
|
+
const appPlugin = {
|
|
271
|
+
name: '@test/removable',
|
|
272
|
+
version: '1.0.0',
|
|
273
|
+
type: 'app-metadata' as const,
|
|
274
|
+
init: vi.fn(),
|
|
275
|
+
start: vi.fn(),
|
|
276
|
+
stop: stopFn,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
await pluginSystem.install(appPlugin, registry);
|
|
280
|
+
expect(pluginSystem.isLoaded('@test/removable')).toBe(true);
|
|
281
|
+
|
|
282
|
+
await pluginSystem.uninstall('@test/removable');
|
|
283
|
+
expect(pluginSystem.isLoaded('@test/removable')).toBe(false);
|
|
284
|
+
expect(stopFn).toHaveBeenCalledTimes(1);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should throw when uninstalling a plugin that is not installed', async () => {
|
|
288
|
+
await expect(pluginSystem.uninstall('@test/nonexistent')).rejects.toThrow(
|
|
289
|
+
'Plugin "@test/nonexistent" is not loaded'
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should provide default context when none is given', async () => {
|
|
294
|
+
const startFn = vi.fn();
|
|
295
|
+
const appPlugin = {
|
|
296
|
+
name: '@test/default-ctx',
|
|
297
|
+
version: '1.0.0',
|
|
298
|
+
type: 'app-metadata' as const,
|
|
299
|
+
init: vi.fn(),
|
|
300
|
+
start: startFn,
|
|
301
|
+
stop: vi.fn(),
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
await pluginSystem.install(appPlugin, registry);
|
|
305
|
+
|
|
306
|
+
expect(startFn).toHaveBeenCalledWith({ logger: console });
|
|
307
|
+
});
|
|
308
|
+
});
|
|
226
309
|
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
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
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import { mergeViewsIntoObjects } from '../merge-views-into-objects';
|
|
11
|
+
|
|
12
|
+
describe('mergeViewsIntoObjects', () => {
|
|
13
|
+
it('should merge listViews from views into corresponding objects', () => {
|
|
14
|
+
const objects = [{ name: 'account', label: 'Account', fields: {} }];
|
|
15
|
+
const views = [
|
|
16
|
+
{
|
|
17
|
+
listViews: {
|
|
18
|
+
all_accounts: {
|
|
19
|
+
name: 'all_accounts',
|
|
20
|
+
label: 'All Accounts',
|
|
21
|
+
type: 'grid',
|
|
22
|
+
data: { provider: 'object', object: 'account' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const result = mergeViewsIntoObjects(objects, views);
|
|
29
|
+
expect(result[0].listViews.all_accounts).toBeDefined();
|
|
30
|
+
expect(result[0].listViews.all_accounts.label).toBe('All Accounts');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should preserve existing listViews on objects', () => {
|
|
34
|
+
const objects = [
|
|
35
|
+
{
|
|
36
|
+
name: 'todo_task',
|
|
37
|
+
label: 'Task',
|
|
38
|
+
fields: {},
|
|
39
|
+
listViews: {
|
|
40
|
+
existing: { name: 'existing', label: 'Existing View', type: 'grid', data: { provider: 'object', object: 'todo_task' } },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
const views = [
|
|
45
|
+
{
|
|
46
|
+
listViews: {
|
|
47
|
+
new_view: {
|
|
48
|
+
name: 'new_view',
|
|
49
|
+
label: 'New View',
|
|
50
|
+
type: 'grid',
|
|
51
|
+
data: { provider: 'object', object: 'todo_task' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const result = mergeViewsIntoObjects(objects, views);
|
|
58
|
+
const task = result[0];
|
|
59
|
+
expect(task.listViews.existing).toBeDefined();
|
|
60
|
+
expect(task.listViews.new_view).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should ignore listViews without data.object', () => {
|
|
64
|
+
const objects = [{ name: 'account', label: 'Account', fields: {} }];
|
|
65
|
+
const views = [
|
|
66
|
+
{
|
|
67
|
+
listViews: {
|
|
68
|
+
orphan: { name: 'orphan', label: 'Orphan View', type: 'grid' },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const result = mergeViewsIntoObjects(objects, views);
|
|
74
|
+
expect(result[0].listViews).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return objects unchanged when views is empty', () => {
|
|
78
|
+
const objects = [{ name: 'account', label: 'Account', fields: {} }];
|
|
79
|
+
|
|
80
|
+
const result = mergeViewsIntoObjects(objects, []);
|
|
81
|
+
expect(result).toEqual(objects);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should ignore views without listViews property', () => {
|
|
85
|
+
const objects = [{ name: 'account', label: 'Account', fields: {} }];
|
|
86
|
+
const views = [{ someOtherProp: true }];
|
|
87
|
+
|
|
88
|
+
const result = mergeViewsIntoObjects(objects, views);
|
|
89
|
+
expect(result).toEqual(objects);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should merge views from multiple view entries into different objects', () => {
|
|
93
|
+
const objects = [
|
|
94
|
+
{ name: 'account', label: 'Account', fields: {} },
|
|
95
|
+
{ name: 'contact', label: 'Contact', fields: {} },
|
|
96
|
+
];
|
|
97
|
+
const views = [
|
|
98
|
+
{
|
|
99
|
+
listViews: {
|
|
100
|
+
all_accounts: { name: 'all_accounts', label: 'All Accounts', type: 'grid', data: { provider: 'object', object: 'account' } },
|
|
101
|
+
all_contacts: { name: 'all_contacts', label: 'All Contacts', type: 'grid', data: { provider: 'object', object: 'contact' } },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const result = mergeViewsIntoObjects(objects, views);
|
|
107
|
+
expect(result[0].listViews.all_accounts).toBeDefined();
|
|
108
|
+
expect(result[1].listViews.all_contacts).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
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
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Adapter: merge stack-level views into object definitions.
|
|
11
|
+
*
|
|
12
|
+
* Views are defined at the stack level (views[].listViews) but the runtime
|
|
13
|
+
* expects listViews on each object definition. This bridges the gap until
|
|
14
|
+
* the runtime/provider layer handles it natively.
|
|
15
|
+
*
|
|
16
|
+
* @param objects - Object definitions from composed stack
|
|
17
|
+
* @param views - View definitions containing listViews keyed by object name
|
|
18
|
+
* @returns Objects with listViews merged in from matching views
|
|
19
|
+
*/
|
|
20
|
+
export function mergeViewsIntoObjects(objects: any[], views: any[]): any[] {
|
|
21
|
+
const viewsByObject: Record<string, Record<string, any>> = {};
|
|
22
|
+
for (const view of views) {
|
|
23
|
+
if (!view.listViews) continue;
|
|
24
|
+
for (const [viewName, listView] of Object.entries(view.listViews as Record<string, any>)) {
|
|
25
|
+
const objectName = listView?.data?.object;
|
|
26
|
+
if (!objectName) continue;
|
|
27
|
+
if (!viewsByObject[objectName]) viewsByObject[objectName] = {};
|
|
28
|
+
viewsByObject[objectName][viewName] = listView;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return objects.map((obj: any) => {
|
|
32
|
+
const v = viewsByObject[obj.name];
|
|
33
|
+
if (!v) return obj;
|
|
34
|
+
return { ...obj, listViews: { ...(obj.listViews || {}), ...v } };
|
|
35
|
+
});
|
|
36
|
+
}
|