@object-ui/data-objectstack 2.0.0 → 3.0.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/dist/index.cjs +429 -6
- package/dist/index.d.cts +496 -1
- package/dist/index.d.ts +496 -1
- package/dist/index.js +420 -5
- package/package.json +6 -6
- package/src/cache/MetadataCache.ts +22 -0
- package/src/cloud.ts +109 -0
- package/src/contracts.ts +115 -0
- package/src/index.ts +73 -5
- package/src/integration.ts +192 -0
- package/src/security.ts +230 -0
- package/src/studio.ts +152 -0
- package/src/v3-compat.test.ts +240 -0
package/src/studio.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
* Studio module integration for @objectstack/spec v3.0.0
|
|
11
|
+
* Provides visual designer schema improvements: canvas, property editors, theme builder.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface StudioCanvasConfig {
|
|
15
|
+
/** Canvas width */
|
|
16
|
+
width: number;
|
|
17
|
+
/** Canvas height */
|
|
18
|
+
height: number;
|
|
19
|
+
/** Background type */
|
|
20
|
+
background: 'grid' | 'dots' | 'lines' | 'none';
|
|
21
|
+
/** Grid size in pixels */
|
|
22
|
+
gridSize: number;
|
|
23
|
+
/** Snap to grid */
|
|
24
|
+
snapToGrid: boolean;
|
|
25
|
+
/** Zoom range */
|
|
26
|
+
zoom: {
|
|
27
|
+
min: number;
|
|
28
|
+
max: number;
|
|
29
|
+
step: number;
|
|
30
|
+
current: number;
|
|
31
|
+
};
|
|
32
|
+
/** Pan offset */
|
|
33
|
+
panOffset: { x: number; y: number };
|
|
34
|
+
/** Show minimap */
|
|
35
|
+
showMinimap: boolean;
|
|
36
|
+
/** Minimap position */
|
|
37
|
+
minimapPosition: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface StudioPropertyEditor {
|
|
41
|
+
/** Property name */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Display label */
|
|
44
|
+
label: string;
|
|
45
|
+
/** Editor type */
|
|
46
|
+
type: 'text' | 'number' | 'boolean' | 'select' | 'color' | 'code' | 'expression';
|
|
47
|
+
/** Default value */
|
|
48
|
+
defaultValue?: unknown;
|
|
49
|
+
/** Options for select type */
|
|
50
|
+
options?: Array<{ label: string; value: string }>;
|
|
51
|
+
/** Group/category */
|
|
52
|
+
group?: string;
|
|
53
|
+
/** Description */
|
|
54
|
+
description?: string;
|
|
55
|
+
/** Whether the property supports live preview */
|
|
56
|
+
livePreview?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface StudioThemeBuilderConfig {
|
|
60
|
+
/** Available color palettes */
|
|
61
|
+
palettes: StudioColorPalette[];
|
|
62
|
+
/** Typography presets */
|
|
63
|
+
typography: StudioTypographyPreset[];
|
|
64
|
+
/** Spacing scale */
|
|
65
|
+
spacing: number[];
|
|
66
|
+
/** Border radius options */
|
|
67
|
+
borderRadius: number[];
|
|
68
|
+
/** Shadow presets */
|
|
69
|
+
shadows: StudioShadowPreset[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface StudioColorPalette {
|
|
73
|
+
name: string;
|
|
74
|
+
colors: Record<string, string>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface StudioTypographyPreset {
|
|
78
|
+
name: string;
|
|
79
|
+
fontFamily: string;
|
|
80
|
+
fontSize: Record<string, string>;
|
|
81
|
+
fontWeight: Record<string, number>;
|
|
82
|
+
lineHeight: Record<string, string>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface StudioShadowPreset {
|
|
86
|
+
name: string;
|
|
87
|
+
value: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Default canvas configuration for designers.
|
|
92
|
+
*/
|
|
93
|
+
export function createDefaultCanvasConfig(overrides?: Partial<StudioCanvasConfig>): StudioCanvasConfig {
|
|
94
|
+
return {
|
|
95
|
+
width: 1200,
|
|
96
|
+
height: 800,
|
|
97
|
+
background: 'grid',
|
|
98
|
+
gridSize: 8,
|
|
99
|
+
snapToGrid: true,
|
|
100
|
+
zoom: {
|
|
101
|
+
min: 0.25,
|
|
102
|
+
max: 3,
|
|
103
|
+
step: 0.1,
|
|
104
|
+
current: 1,
|
|
105
|
+
},
|
|
106
|
+
panOffset: { x: 0, y: 0 },
|
|
107
|
+
showMinimap: false,
|
|
108
|
+
minimapPosition: 'bottom-right',
|
|
109
|
+
...overrides,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Snap a position to the grid.
|
|
115
|
+
*/
|
|
116
|
+
export function snapToGrid(x: number, y: number, gridSize: number): { x: number; y: number } {
|
|
117
|
+
return {
|
|
118
|
+
x: Math.round(x / gridSize) * gridSize,
|
|
119
|
+
y: Math.round(y / gridSize) * gridSize,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Calculate auto-layout positions for a set of items.
|
|
125
|
+
* Uses a simple grid-based layout algorithm.
|
|
126
|
+
*/
|
|
127
|
+
export function calculateAutoLayout(
|
|
128
|
+
items: Array<{ id: string; width: number; height: number }>,
|
|
129
|
+
canvasWidth: number,
|
|
130
|
+
padding: number = 40,
|
|
131
|
+
gap: number = 40,
|
|
132
|
+
): Array<{ id: string; x: number; y: number }> {
|
|
133
|
+
const positions: Array<{ id: string; x: number; y: number }> = [];
|
|
134
|
+
let currentX = padding;
|
|
135
|
+
let currentY = padding;
|
|
136
|
+
let rowMaxHeight = 0;
|
|
137
|
+
|
|
138
|
+
for (const item of items) {
|
|
139
|
+
// Wrap to next row if exceeds canvas width
|
|
140
|
+
if (currentX + item.width + padding > canvasWidth) {
|
|
141
|
+
currentX = padding;
|
|
142
|
+
currentY += rowMaxHeight + gap;
|
|
143
|
+
rowMaxHeight = 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
positions.push({ id: item.id, x: currentX, y: currentY });
|
|
147
|
+
currentX += item.width + gap;
|
|
148
|
+
rowMaxHeight = Math.max(rowMaxHeight, item.height);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return positions;
|
|
152
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3.0.0 compatibility tests for @objectstack dependencies.
|
|
3
|
+
* Validates that all integration modules work correctly with the spec.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { validatePluginContract, generateContractManifest } from './contracts';
|
|
7
|
+
import { IntegrationManager } from './integration';
|
|
8
|
+
import { SecurityManager } from './security';
|
|
9
|
+
import { CloudOperations } from './cloud';
|
|
10
|
+
import { createDefaultCanvasConfig, snapToGrid, calculateAutoLayout } from './studio';
|
|
11
|
+
|
|
12
|
+
describe('v3.0.0 Compatibility', () => {
|
|
13
|
+
describe('Cloud namespace (replacing Hub)', () => {
|
|
14
|
+
it('should create cloud operations instance', () => {
|
|
15
|
+
const ops = new CloudOperations(() => ({}));
|
|
16
|
+
expect(ops).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle deploy when client has no cloud support', async () => {
|
|
20
|
+
const ops = new CloudOperations(() => ({}));
|
|
21
|
+
const result = await ops.deploy('app-1', { environment: 'production' });
|
|
22
|
+
expect(result).toHaveProperty('deploymentId');
|
|
23
|
+
expect(result).toHaveProperty('status');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should handle marketplace search when client has no cloud support', async () => {
|
|
27
|
+
const ops = new CloudOperations(() => ({}));
|
|
28
|
+
const results = await ops.searchMarketplace('grid');
|
|
29
|
+
expect(Array.isArray(results)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('Contracts module', () => {
|
|
34
|
+
it('should validate a valid plugin contract', () => {
|
|
35
|
+
const contract = {
|
|
36
|
+
name: 'my-plugin',
|
|
37
|
+
version: '1.0.0',
|
|
38
|
+
exports: [{ name: 'MyComponent', type: 'component' as const }],
|
|
39
|
+
permissions: ['data.read'],
|
|
40
|
+
};
|
|
41
|
+
const result = validatePluginContract(contract);
|
|
42
|
+
expect(result.valid).toBe(true);
|
|
43
|
+
expect(result.errors).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should reject invalid plugin contract', () => {
|
|
47
|
+
const contract = {
|
|
48
|
+
name: '',
|
|
49
|
+
version: 'invalid',
|
|
50
|
+
exports: [],
|
|
51
|
+
};
|
|
52
|
+
const result = validatePluginContract(contract);
|
|
53
|
+
expect(result.valid).toBe(false);
|
|
54
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should generate contract manifest', () => {
|
|
58
|
+
const contract = {
|
|
59
|
+
name: 'test-plugin',
|
|
60
|
+
version: '2.0.0',
|
|
61
|
+
exports: [{ name: 'GridView', type: 'component' as const, description: 'A grid view' }],
|
|
62
|
+
permissions: ['data.read', 'data.write'],
|
|
63
|
+
};
|
|
64
|
+
const manifest = generateContractManifest(contract);
|
|
65
|
+
expect(manifest.$schema).toBeDefined();
|
|
66
|
+
expect(manifest.name).toBe('test-plugin');
|
|
67
|
+
expect(manifest.version).toBe('2.0.0');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('Integration module', () => {
|
|
72
|
+
it('should register and retrieve integrations', () => {
|
|
73
|
+
const manager = new IntegrationManager();
|
|
74
|
+
manager.register('slack-1', {
|
|
75
|
+
provider: 'slack',
|
|
76
|
+
enabled: true,
|
|
77
|
+
config: { webhookUrl: 'https://hooks.slack.com/test' },
|
|
78
|
+
triggers: [{ event: 'record.created' }],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const all = manager.getAll();
|
|
82
|
+
expect(all.size).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should filter integrations by event', () => {
|
|
86
|
+
const manager = new IntegrationManager();
|
|
87
|
+
manager.register('webhook-1', {
|
|
88
|
+
provider: 'webhook',
|
|
89
|
+
enabled: true,
|
|
90
|
+
config: { url: 'https://example.com/hook', method: 'POST' },
|
|
91
|
+
triggers: [{ event: 'record.created' }],
|
|
92
|
+
});
|
|
93
|
+
manager.register('webhook-2', {
|
|
94
|
+
provider: 'webhook',
|
|
95
|
+
enabled: true,
|
|
96
|
+
config: { url: 'https://example.com/hook2', method: 'POST' },
|
|
97
|
+
triggers: [{ event: 'record.deleted' }],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const createMatches = manager.getForEvent('record.created');
|
|
101
|
+
expect(createMatches).toHaveLength(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should unregister integrations', () => {
|
|
105
|
+
const manager = new IntegrationManager();
|
|
106
|
+
manager.register('test', {
|
|
107
|
+
provider: 'webhook',
|
|
108
|
+
enabled: true,
|
|
109
|
+
config: { url: 'https://example.com', method: 'POST' },
|
|
110
|
+
});
|
|
111
|
+
manager.unregister('test');
|
|
112
|
+
expect(manager.getAll().size).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('Security module', () => {
|
|
117
|
+
it('should generate CSP header', () => {
|
|
118
|
+
const manager = new SecurityManager({
|
|
119
|
+
csp: {
|
|
120
|
+
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
121
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
122
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
123
|
+
connectSrc: ["'self'", 'https://api.example.com'],
|
|
124
|
+
fontSrc: ["'self'"],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const header = manager.generateCSPHeader();
|
|
129
|
+
expect(header).toContain("script-src 'self' 'unsafe-inline'");
|
|
130
|
+
expect(header).toContain("connect-src 'self' https://api.example.com");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should record and filter audit logs', () => {
|
|
134
|
+
const manager = new SecurityManager({
|
|
135
|
+
auditLog: {
|
|
136
|
+
enabled: true,
|
|
137
|
+
events: ['data.create', 'data.read', 'auth.login'],
|
|
138
|
+
destination: 'console',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
manager.recordAudit({ event: 'data.create', userId: 'user-1', resource: 'accounts' });
|
|
143
|
+
manager.recordAudit({ event: 'auth.login', userId: 'user-2' });
|
|
144
|
+
manager.recordAudit({ event: 'data.create', userId: 'user-1', resource: 'contacts' });
|
|
145
|
+
|
|
146
|
+
const allLogs = manager.getAuditLog();
|
|
147
|
+
expect(allLogs).toHaveLength(3);
|
|
148
|
+
|
|
149
|
+
const userLogs = manager.getAuditLog({ userId: 'user-1' });
|
|
150
|
+
expect(userLogs).toHaveLength(2);
|
|
151
|
+
|
|
152
|
+
const loginLogs = manager.getAuditLog({ event: 'auth.login' });
|
|
153
|
+
expect(loginLogs).toHaveLength(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should mask record data', () => {
|
|
157
|
+
const manager = new SecurityManager({
|
|
158
|
+
dataMasking: {
|
|
159
|
+
rules: [
|
|
160
|
+
{ field: 'ssn', strategy: 'partial', visibleChars: 4 },
|
|
161
|
+
{ field: 'password', strategy: 'redact' },
|
|
162
|
+
{ field: 'email', strategy: 'full' },
|
|
163
|
+
{ field: 'phone', strategy: 'partial', visibleChars: 3, exemptRoles: ['admin'] },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const record = {
|
|
169
|
+
ssn: '123-45-6789',
|
|
170
|
+
password: 'secret123',
|
|
171
|
+
email: 'john@example.com',
|
|
172
|
+
phone: '555-1234',
|
|
173
|
+
name: 'John',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const masked = manager.maskRecord(record);
|
|
177
|
+
expect(masked.ssn).toBe('123-*******');
|
|
178
|
+
expect(masked.password).toBe('[REDACTED]');
|
|
179
|
+
expect(masked.email).toBe('****************');
|
|
180
|
+
expect(masked.name).toBe('John'); // Not masked
|
|
181
|
+
|
|
182
|
+
// Admin sees phone unmasked
|
|
183
|
+
const adminMasked = manager.maskRecord(record, ['admin']);
|
|
184
|
+
expect(adminMasked.phone).toBe('555-1234');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('Studio module', () => {
|
|
189
|
+
it('should create default canvas config', () => {
|
|
190
|
+
const config = createDefaultCanvasConfig();
|
|
191
|
+
expect(config.width).toBe(1200);
|
|
192
|
+
expect(config.height).toBe(800);
|
|
193
|
+
expect(config.snapToGrid).toBe(true);
|
|
194
|
+
expect(config.zoom.min).toBe(0.25);
|
|
195
|
+
expect(config.zoom.max).toBe(3);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should create canvas config with overrides', () => {
|
|
199
|
+
const config = createDefaultCanvasConfig({ width: 1600, showMinimap: true });
|
|
200
|
+
expect(config.width).toBe(1600);
|
|
201
|
+
expect(config.showMinimap).toBe(true);
|
|
202
|
+
expect(config.height).toBe(800); // Default
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should snap positions to grid', () => {
|
|
206
|
+
expect(snapToGrid(13, 27, 8)).toEqual({ x: 16, y: 24 });
|
|
207
|
+
expect(snapToGrid(0, 0, 8)).toEqual({ x: 0, y: 0 });
|
|
208
|
+
expect(snapToGrid(4, 4, 8)).toEqual({ x: 8, y: 8 });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should calculate auto-layout positions', () => {
|
|
212
|
+
const items = [
|
|
213
|
+
{ id: '1', width: 200, height: 150 },
|
|
214
|
+
{ id: '2', width: 200, height: 100 },
|
|
215
|
+
{ id: '3', width: 200, height: 200 },
|
|
216
|
+
];
|
|
217
|
+
const positions = calculateAutoLayout(items, 1200);
|
|
218
|
+
expect(positions).toHaveLength(3);
|
|
219
|
+
expect(positions[0].x).toBe(40); // padding
|
|
220
|
+
expect(positions[0].y).toBe(40); // padding
|
|
221
|
+
expect(positions[1].x).toBeGreaterThan(positions[0].x);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('PaginatedResult API (records/total/hasMore)', () => {
|
|
226
|
+
it('should support v3.0.0 PaginatedResult fields', () => {
|
|
227
|
+
// Verify the QueryResult type supports records/total/hasMore
|
|
228
|
+
const result = {
|
|
229
|
+
data: [{ id: '1' }],
|
|
230
|
+
total: 10,
|
|
231
|
+
page: 1,
|
|
232
|
+
pageSize: 5,
|
|
233
|
+
hasMore: true,
|
|
234
|
+
};
|
|
235
|
+
expect(result.data).toHaveLength(1);
|
|
236
|
+
expect(result.total).toBe(10);
|
|
237
|
+
expect(result.hasMore).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|