@viyv/agent-ui-schema 0.1.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.
Files changed (75) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +16 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/dist/__tests__/expression.test.d.ts +2 -0
  5. package/dist/__tests__/expression.test.d.ts.map +1 -0
  6. package/dist/__tests__/expression.test.js +77 -0
  7. package/dist/__tests__/expression.test.js.map +1 -0
  8. package/dist/__tests__/page-spec.test.d.ts +2 -0
  9. package/dist/__tests__/page-spec.test.d.ts.map +1 -0
  10. package/dist/__tests__/page-spec.test.js +223 -0
  11. package/dist/__tests__/page-spec.test.js.map +1 -0
  12. package/dist/__tests__/validator.test.d.ts +2 -0
  13. package/dist/__tests__/validator.test.d.ts.map +1 -0
  14. package/dist/__tests__/validator.test.js +177 -0
  15. package/dist/__tests__/validator.test.js.map +1 -0
  16. package/dist/action-def.d.ts +227 -0
  17. package/dist/action-def.d.ts.map +1 -0
  18. package/dist/action-def.js +54 -0
  19. package/dist/action-def.js.map +1 -0
  20. package/dist/catalog.d.ts +16 -0
  21. package/dist/catalog.d.ts.map +1 -0
  22. package/dist/catalog.js +8 -0
  23. package/dist/catalog.js.map +1 -0
  24. package/dist/data-source.d.ts +195 -0
  25. package/dist/data-source.d.ts.map +1 -0
  26. package/dist/data-source.js +28 -0
  27. package/dist/data-source.js.map +1 -0
  28. package/dist/element-def.d.ts +37 -0
  29. package/dist/element-def.d.ts.map +1 -0
  30. package/dist/element-def.js +13 -0
  31. package/dist/element-def.js.map +1 -0
  32. package/dist/expression.d.ts +28 -0
  33. package/dist/expression.d.ts.map +1 -0
  34. package/dist/expression.js +56 -0
  35. package/dist/expression.js.map +1 -0
  36. package/dist/hook-def.d.ts +456 -0
  37. package/dist/hook-def.d.ts.map +1 -0
  38. package/dist/hook-def.js +67 -0
  39. package/dist/hook-def.js.map +1 -0
  40. package/dist/index.d.ts +20 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +19 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/page-spec.d.ts +684 -0
  45. package/dist/page-spec.d.ts.map +1 -0
  46. package/dist/page-spec.js +34 -0
  47. package/dist/page-spec.js.map +1 -0
  48. package/dist/page-store.d.ts +20 -0
  49. package/dist/page-store.d.ts.map +1 -0
  50. package/dist/page-store.js +2 -0
  51. package/dist/page-store.js.map +1 -0
  52. package/dist/patch.d.ts +144 -0
  53. package/dist/patch.d.ts.map +1 -0
  54. package/dist/patch.js +23 -0
  55. package/dist/patch.js.map +1 -0
  56. package/dist/validator.d.ts +13 -0
  57. package/dist/validator.d.ts.map +1 -0
  58. package/dist/validator.js +286 -0
  59. package/dist/validator.js.map +1 -0
  60. package/package.json +27 -0
  61. package/src/__tests__/expression.test.ts +93 -0
  62. package/src/__tests__/page-spec.test.ts +242 -0
  63. package/src/__tests__/validator.test.ts +189 -0
  64. package/src/action-def.ts +63 -0
  65. package/src/catalog.ts +24 -0
  66. package/src/data-source.ts +46 -0
  67. package/src/element-def.ts +17 -0
  68. package/src/expression.ts +77 -0
  69. package/src/hook-def.ts +79 -0
  70. package/src/index.ts +72 -0
  71. package/src/page-spec.ts +42 -0
  72. package/src/page-store.ts +18 -0
  73. package/src/patch.ts +28 -0
  74. package/src/validator.ts +347 -0
  75. package/tsconfig.json +8 -0
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isExpression, parseExpression } from '../expression.js';
3
+
4
+ describe('isExpression', () => {
5
+ it('returns true for $-prefixed strings', () => {
6
+ expect(isExpression('$hook.sales')).toBe(true);
7
+ expect(isExpression('$state.filter')).toBe(true);
8
+ });
9
+
10
+ it('returns false for non-expressions', () => {
11
+ expect(isExpression('hello')).toBe(false);
12
+ expect(isExpression(42)).toBe(false);
13
+ expect(isExpression(null)).toBe(false);
14
+ });
15
+ });
16
+
17
+ describe('parseExpression', () => {
18
+ it('parses $hook.xxx', () => {
19
+ const ref = parseExpression('$hook.sales');
20
+ expect(ref).toEqual({ type: 'hook', hookId: 'sales', path: [] });
21
+ });
22
+
23
+ it('parses $hook.xxx.data.nested', () => {
24
+ const ref = parseExpression('$hook.sales.data.items');
25
+ expect(ref).toEqual({ type: 'hook', hookId: 'sales', path: ['data', 'items'] });
26
+ });
27
+
28
+ it('parses $state.xxx', () => {
29
+ const ref = parseExpression('$state.filter');
30
+ expect(ref).toEqual({ type: 'state', key: 'filter' });
31
+ });
32
+
33
+ it('parses $bindState.xxx', () => {
34
+ const ref = parseExpression('$bindState.searchTerm');
35
+ expect(ref).toEqual({ type: 'bindState', key: 'searchTerm' });
36
+ });
37
+
38
+ it('parses $action.xxx', () => {
39
+ const ref = parseExpression('$action.refresh');
40
+ expect(ref).toEqual({ type: 'action', actionId: 'refresh' });
41
+ });
42
+
43
+ it('parses $expr(...)', () => {
44
+ const ref = parseExpression('$expr(hook.sales.length > 0)');
45
+ expect(ref).toEqual({ type: 'expr', code: 'hook.sales.length > 0' });
46
+ });
47
+
48
+ it('parses $hook with deep nesting (a.b.c.d)', () => {
49
+ const ref = parseExpression('$hook.data.a.b.c.d');
50
+ expect(ref).toEqual({ type: 'hook', hookId: 'data', path: ['a', 'b', 'c', 'd'] });
51
+ });
52
+
53
+ it('parses hook IDs with hyphens', () => {
54
+ const ref = parseExpression('$hook.my-hook');
55
+ expect(ref).toEqual({ type: 'hook', hookId: 'my-hook', path: [] });
56
+ });
57
+
58
+ it('parses state keys with hyphens', () => {
59
+ const ref = parseExpression('$state.my-state');
60
+ expect(ref).toEqual({ type: 'state', key: 'my-state' });
61
+ });
62
+
63
+ it('parses action IDs with hyphens', () => {
64
+ const ref = parseExpression('$action.my-action');
65
+ expect(ref).toEqual({ type: 'action', actionId: 'my-action' });
66
+ });
67
+
68
+ it('parses $expr with nested parentheses', () => {
69
+ const ref = parseExpression('$expr(Math.max(hook.a, hook.b))');
70
+ expect(ref).toEqual({ type: 'expr', code: 'Math.max(hook.a, hook.b)' });
71
+ });
72
+
73
+ it('parses $item (whole object)', () => {
74
+ const ref = parseExpression('$item');
75
+ expect(ref).toEqual({ type: 'item', path: [] });
76
+ });
77
+
78
+ it('parses $item.name', () => {
79
+ const ref = parseExpression('$item.name');
80
+ expect(ref).toEqual({ type: 'item', path: ['name'] });
81
+ });
82
+
83
+ it('parses $item.a.b.c (deep path)', () => {
84
+ const ref = parseExpression('$item.a.b.c');
85
+ expect(ref).toEqual({ type: 'item', path: ['a', 'b', 'c'] });
86
+ });
87
+
88
+ it('returns null for invalid expressions', () => {
89
+ expect(parseExpression('$invalid')).toBeNull();
90
+ expect(parseExpression('$')).toBeNull();
91
+ expect(parseExpression('$$hook.x')).toBeNull();
92
+ });
93
+ });
@@ -0,0 +1,242 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ActionDefSchema } from '../action-def.js';
3
+ import { PageSpecSchema } from '../page-spec.js';
4
+ import { validatePageSpec } from '../validator.js';
5
+
6
+ describe('ActionDefSchema — CRUD actions', () => {
7
+ it('parses addItem action', () => {
8
+ const result = ActionDefSchema.safeParse({
9
+ type: 'addItem',
10
+ hookId: 'tasks',
11
+ stateKey: 'newTask',
12
+ idPrefix: 'TASK',
13
+ });
14
+ expect(result.success).toBe(true);
15
+ });
16
+
17
+ it('parses removeItem action', () => {
18
+ const result = ActionDefSchema.safeParse({
19
+ type: 'removeItem',
20
+ hookId: 'tasks',
21
+ key: 'id',
22
+ stateKey: 'editingTask',
23
+ });
24
+ expect(result.success).toBe(true);
25
+ });
26
+
27
+ it('parses updateItem action', () => {
28
+ const result = ActionDefSchema.safeParse({
29
+ type: 'updateItem',
30
+ hookId: 'tasks',
31
+ key: 'id',
32
+ stateKey: 'editingTask',
33
+ onComplete: { showEditDialog: false },
34
+ });
35
+ expect(result.success).toBe(true);
36
+ });
37
+
38
+ it('parses setState with onComplete', () => {
39
+ const result = ActionDefSchema.safeParse({
40
+ type: 'setState',
41
+ key: 'showDialog',
42
+ value: true,
43
+ onComplete: { newTask: { title: '' } },
44
+ });
45
+ expect(result.success).toBe(true);
46
+ });
47
+
48
+ it('rejects addItem without required hookId', () => {
49
+ const result = ActionDefSchema.safeParse({
50
+ type: 'addItem',
51
+ stateKey: 'newTask',
52
+ });
53
+ expect(result.success).toBe(false);
54
+ });
55
+
56
+ it('rejects removeItem without required key', () => {
57
+ const result = ActionDefSchema.safeParse({
58
+ type: 'removeItem',
59
+ hookId: 'tasks',
60
+ stateKey: 'editingTask',
61
+ });
62
+ expect(result.success).toBe(false);
63
+ });
64
+
65
+ it('rejects updateItem without required stateKey', () => {
66
+ const result = ActionDefSchema.safeParse({
67
+ type: 'updateItem',
68
+ hookId: 'tasks',
69
+ key: 'id',
70
+ });
71
+ expect(result.success).toBe(false);
72
+ });
73
+ });
74
+
75
+ describe('validatePageSpec — CRUD hookId validation', () => {
76
+ const baseSpec = {
77
+ id: 'test',
78
+ title: 'Test',
79
+ root: 'root',
80
+ elements: { root: { type: 'Stack', props: {} } },
81
+ state: {},
82
+ };
83
+
84
+ it('errors when hookId references undefined hook', () => {
85
+ const result = validatePageSpec({
86
+ ...baseSpec,
87
+ hooks: {},
88
+ actions: {
89
+ add: { type: 'addItem', hookId: 'missing', stateKey: 'x' },
90
+ },
91
+ });
92
+ expect(result.valid).toBe(false);
93
+ expect(result.errors.some((e) => e.message.includes('"missing" not defined'))).toBe(true);
94
+ });
95
+
96
+ it('errors when hookId references non-useState hook', () => {
97
+ const result = validatePageSpec({
98
+ ...baseSpec,
99
+ hooks: {
100
+ derived: { use: 'useDerived', from: 'tasks', params: {} },
101
+ tasks: { use: 'useState', params: { initial: [] } },
102
+ },
103
+ actions: {
104
+ add: { type: 'addItem', hookId: 'derived', stateKey: 'x' },
105
+ },
106
+ });
107
+ expect(result.valid).toBe(false);
108
+ expect(result.errors.some((e) => e.message.includes('not useState'))).toBe(true);
109
+ });
110
+
111
+ it('passes when hookId references useState hook', () => {
112
+ const result = validatePageSpec({
113
+ ...baseSpec,
114
+ hooks: {
115
+ tasks: { use: 'useState', params: { initial: [] } },
116
+ },
117
+ actions: {
118
+ add: { type: 'addItem', hookId: 'tasks', stateKey: 'newTask' },
119
+ },
120
+ });
121
+ expect(result.valid).toBe(true);
122
+ });
123
+
124
+ it('errors for removeItem targeting non-useState hook', () => {
125
+ const result = validatePageSpec({
126
+ ...baseSpec,
127
+ hooks: {
128
+ tasks: { use: 'useState', params: { initial: [] } },
129
+ derived: { use: 'useDerived', from: 'tasks', params: {} },
130
+ },
131
+ actions: {
132
+ remove: { type: 'removeItem', hookId: 'derived', key: 'id', stateKey: 'x' },
133
+ },
134
+ });
135
+ expect(result.valid).toBe(false);
136
+ expect(result.errors.some((e) => e.message.includes('not useState'))).toBe(true);
137
+ });
138
+
139
+ it('errors for updateItem targeting undefined hook', () => {
140
+ const result = validatePageSpec({
141
+ ...baseSpec,
142
+ hooks: {},
143
+ actions: {
144
+ update: { type: 'updateItem', hookId: 'missing', key: 'id', stateKey: 'x' },
145
+ },
146
+ });
147
+ expect(result.valid).toBe(false);
148
+ expect(result.errors.some((e) => e.message.includes('"missing" not defined'))).toBe(true);
149
+ });
150
+
151
+ it('does not validate refreshHook hookId against useState', () => {
152
+ const result = validatePageSpec({
153
+ ...baseSpec,
154
+ hooks: {
155
+ data: {
156
+ use: 'useSqlQuery',
157
+ params: { connection: 'db', query: 'SELECT 1' },
158
+ },
159
+ },
160
+ actions: {
161
+ refresh: { type: 'refreshHook', hookId: 'data' },
162
+ },
163
+ });
164
+ expect(result.valid).toBe(true);
165
+ });
166
+ });
167
+
168
+ describe('PageSpecSchema', () => {
169
+ it('validates a minimal page spec', () => {
170
+ const spec = {
171
+ id: 'test-page',
172
+ title: 'Test Page',
173
+ root: 'root',
174
+ elements: {
175
+ root: { type: 'Stack', props: { direction: 'vertical' } },
176
+ },
177
+ };
178
+
179
+ const result = PageSpecSchema.safeParse(spec);
180
+ expect(result.success).toBe(true);
181
+ if (result.success) {
182
+ expect(result.data.id).toBe('test-page');
183
+ expect(result.data.hooks).toEqual({});
184
+ expect(result.data.state).toEqual({});
185
+ expect(result.data.actions).toEqual({});
186
+ }
187
+ });
188
+
189
+ it('validates a full page spec with hooks and actions', () => {
190
+ const spec = {
191
+ id: 'sales-dashboard',
192
+ title: 'Sales Dashboard',
193
+ description: 'Monthly sales overview',
194
+ root: 'root',
195
+ elements: {
196
+ root: {
197
+ type: 'Stack',
198
+ props: { direction: 'vertical' },
199
+ children: ['header', 'table'],
200
+ },
201
+ header: { type: 'Header', props: { title: 'Sales' } },
202
+ table: {
203
+ type: 'DataTable',
204
+ props: { data: '$hook.sales', columns: [] },
205
+ },
206
+ },
207
+ hooks: {
208
+ sales: {
209
+ use: 'useSqlQuery',
210
+ params: {
211
+ connection: 'main-db',
212
+ query: 'SELECT * FROM sales',
213
+ refreshInterval: 60000,
214
+ },
215
+ },
216
+ },
217
+ state: { filter: '' },
218
+ actions: {
219
+ refresh: { type: 'refreshHook', hookId: 'sales' },
220
+ },
221
+ theme: { colorScheme: 'light', spacing: 'default' },
222
+ };
223
+
224
+ const result = PageSpecSchema.safeParse(spec);
225
+ expect(result.success).toBe(true);
226
+ });
227
+
228
+ it('rejects invalid hook definitions', () => {
229
+ const spec = {
230
+ id: 'test',
231
+ title: 'Test',
232
+ root: 'root',
233
+ elements: { root: { type: 'Stack', props: {} } },
234
+ hooks: {
235
+ bad: { use: 'useInvalid', params: {} },
236
+ },
237
+ };
238
+
239
+ const result = PageSpecSchema.safeParse(spec);
240
+ expect(result.success).toBe(false);
241
+ });
242
+ });
@@ -0,0 +1,189 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { validatePageSpec } from '../validator.js';
3
+
4
+ describe('validatePageSpec', () => {
5
+ const validSpec = {
6
+ id: 'test',
7
+ title: 'Test',
8
+ root: 'root',
9
+ elements: {
10
+ root: {
11
+ type: 'Stack',
12
+ props: { direction: 'vertical' },
13
+ children: ['header'],
14
+ },
15
+ header: { type: 'Header', props: { title: 'Hello' } },
16
+ },
17
+ hooks: {},
18
+ state: {},
19
+ actions: {},
20
+ };
21
+
22
+ it('validates a correct spec', () => {
23
+ const result = validatePageSpec(validSpec);
24
+ expect(result.valid).toBe(true);
25
+ expect(result.errors).toHaveLength(0);
26
+ });
27
+
28
+ it('detects missing root element', () => {
29
+ const result = validatePageSpec({
30
+ ...validSpec,
31
+ root: 'nonexistent',
32
+ });
33
+ expect(result.valid).toBe(false);
34
+ expect(result.errors.some((e) => e.message.includes('Root element'))).toBe(true);
35
+ });
36
+
37
+ it('detects missing child references', () => {
38
+ const result = validatePageSpec({
39
+ ...validSpec,
40
+ elements: {
41
+ root: {
42
+ type: 'Stack',
43
+ props: {},
44
+ children: ['missing-child'],
45
+ },
46
+ },
47
+ });
48
+ expect(result.valid).toBe(false);
49
+ expect(result.errors.some((e) => e.message.includes('Child element'))).toBe(true);
50
+ });
51
+
52
+ it('detects undefined hook references', () => {
53
+ const result = validatePageSpec({
54
+ ...validSpec,
55
+ elements: {
56
+ root: {
57
+ type: 'DataTable',
58
+ props: { data: '$hook.nonexistent' },
59
+ },
60
+ },
61
+ });
62
+ expect(result.valid).toBe(false);
63
+ expect(result.errors.some((e) => e.message.includes('Hook "nonexistent"'))).toBe(true);
64
+ });
65
+
66
+ it('detects unsafe SQL', () => {
67
+ const result = validatePageSpec({
68
+ ...validSpec,
69
+ hooks: {
70
+ bad: {
71
+ use: 'useSqlQuery',
72
+ params: {
73
+ connection: 'db',
74
+ query: 'DROP TABLE users',
75
+ },
76
+ },
77
+ },
78
+ });
79
+ expect(result.valid).toBe(false);
80
+ expect(result.errors.some((e) => e.message.includes('unsafe SQL'))).toBe(true);
81
+ });
82
+
83
+ it('detects hook cycle', () => {
84
+ const result = validatePageSpec({
85
+ ...validSpec,
86
+ hooks: {
87
+ a: { use: 'useDerived', from: 'b', params: {} },
88
+ b: { use: 'useDerived', from: 'a', params: {} },
89
+ },
90
+ });
91
+ expect(result.valid).toBe(false);
92
+ expect(result.errors.some((e) => e.message.includes('Circular dependency'))).toBe(true);
93
+ });
94
+
95
+ it('validates action references in elements', () => {
96
+ const result = validatePageSpec({
97
+ ...validSpec,
98
+ elements: {
99
+ root: {
100
+ type: 'Button',
101
+ props: { onClick: '$action.nonexistent' },
102
+ },
103
+ },
104
+ });
105
+ expect(result.valid).toBe(false);
106
+ expect(result.errors.some((e) => e.message.includes('Action "nonexistent"'))).toBe(true);
107
+ });
108
+
109
+ it('rejects non-SELECT SQL queries', () => {
110
+ const result = validatePageSpec({
111
+ ...validSpec,
112
+ hooks: {
113
+ insert: {
114
+ use: 'useSqlQuery',
115
+ params: {
116
+ connection: 'db',
117
+ query: 'INSERT INTO users VALUES (1)',
118
+ },
119
+ },
120
+ },
121
+ });
122
+ expect(result.valid).toBe(false);
123
+ });
124
+
125
+ it('detects dangling useDerived source', () => {
126
+ const result = validatePageSpec({
127
+ ...validSpec,
128
+ hooks: {
129
+ derived: { use: 'useDerived', from: 'nonexistent', params: {} },
130
+ },
131
+ });
132
+ expect(result.valid).toBe(false);
133
+ expect(result.errors.some((e) => e.message.includes('undefined source hook'))).toBe(true);
134
+ });
135
+
136
+ it('validates visibility expression hook references', () => {
137
+ const result = validatePageSpec({
138
+ ...validSpec,
139
+ elements: {
140
+ root: {
141
+ type: 'Stack',
142
+ props: {},
143
+ visible: { expr: '$hook.nonexistent' },
144
+ },
145
+ },
146
+ });
147
+ expect(result.valid).toBe(false);
148
+ expect(result.errors.some((e) => e.path.includes('visible.expr'))).toBe(true);
149
+ });
150
+
151
+ it('no warning for $item inside Repeater descendant', () => {
152
+ const result = validatePageSpec({
153
+ ...validSpec,
154
+ elements: {
155
+ root: {
156
+ type: 'Repeater',
157
+ props: { data: '$hook.items' },
158
+ children: ['itemText'],
159
+ },
160
+ itemText: {
161
+ type: 'Text',
162
+ props: { content: '$item.name' },
163
+ },
164
+ },
165
+ hooks: {
166
+ items: {
167
+ use: 'useSqlQuery',
168
+ params: { connection: 'viyv-db', query: 'SELECT name FROM items' },
169
+ },
170
+ },
171
+ });
172
+ expect(result.valid).toBe(true);
173
+ expect(result.warnings).toHaveLength(0);
174
+ });
175
+
176
+ it('warns for $item outside Repeater context', () => {
177
+ const result = validatePageSpec({
178
+ ...validSpec,
179
+ elements: {
180
+ root: {
181
+ type: 'Text',
182
+ props: { content: '$item.name' },
183
+ },
184
+ },
185
+ });
186
+ expect(result.valid).toBe(true); // warning, not error
187
+ expect(result.warnings.some((w) => w.message.includes('$item expression used outside Repeater'))).toBe(true);
188
+ });
189
+ });
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod';
2
+
3
+ export const SetStateActionSchema = z.object({
4
+ type: z.literal('setState'),
5
+ key: z.string().min(1),
6
+ value: z.unknown().optional(),
7
+ onComplete: z.record(z.unknown()).optional(),
8
+ });
9
+
10
+ export const RefreshHookActionSchema = z.object({
11
+ type: z.literal('refreshHook'),
12
+ hookId: z.string().min(1),
13
+ });
14
+
15
+ export const NavigateActionSchema = z.object({
16
+ type: z.literal('navigate'),
17
+ url: z.string().min(1),
18
+ });
19
+
20
+ export const SubmitFormActionSchema = z.object({
21
+ type: z.literal('submitForm'),
22
+ url: z.string().min(1),
23
+ method: z.enum(['POST', 'PUT', 'PATCH']).default('POST'),
24
+ stateKey: z.string().optional(),
25
+ onComplete: z.record(z.unknown()).optional(),
26
+ });
27
+
28
+ export const AddItemActionSchema = z.object({
29
+ type: z.literal('addItem'),
30
+ hookId: z.string().min(1),
31
+ stateKey: z.string().min(1),
32
+ idField: z.string().optional(),
33
+ idPrefix: z.string().optional(),
34
+ onComplete: z.record(z.unknown()).optional(),
35
+ });
36
+
37
+ export const RemoveItemActionSchema = z.object({
38
+ type: z.literal('removeItem'),
39
+ hookId: z.string().min(1),
40
+ key: z.string().min(1),
41
+ stateKey: z.string().min(1),
42
+ onComplete: z.record(z.unknown()).optional(),
43
+ });
44
+
45
+ export const UpdateItemActionSchema = z.object({
46
+ type: z.literal('updateItem'),
47
+ hookId: z.string().min(1),
48
+ key: z.string().min(1),
49
+ stateKey: z.string().min(1),
50
+ onComplete: z.record(z.unknown()).optional(),
51
+ });
52
+
53
+ export const ActionDefSchema = z.discriminatedUnion('type', [
54
+ SetStateActionSchema,
55
+ RefreshHookActionSchema,
56
+ NavigateActionSchema,
57
+ SubmitFormActionSchema,
58
+ AddItemActionSchema,
59
+ RemoveItemActionSchema,
60
+ UpdateItemActionSchema,
61
+ ]);
62
+
63
+ export type ActionDef = z.infer<typeof ActionDefSchema>;
package/src/catalog.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { z } from 'zod';
2
+
3
+ export interface ComponentMeta {
4
+ type: string;
5
+ label: string;
6
+ description: string;
7
+ category: 'layout' | 'display' | 'data' | 'chart' | 'input' | 'control' | 'navigation' | 'overlay' | 'feedback' | 'map';
8
+ propsSchema: z.ZodType;
9
+ acceptsChildren: boolean;
10
+ /** When true, ElementRenderer passes `open` prop from visibility instead of unmounting */
11
+ overlay?: boolean;
12
+ }
13
+
14
+ export interface ComponentCatalog {
15
+ components: Record<string, ComponentMeta>;
16
+ }
17
+
18
+ export function defineCatalog(components: ComponentMeta[]): ComponentCatalog {
19
+ const map: Record<string, ComponentMeta> = {};
20
+ for (const comp of components) {
21
+ map[comp.type] = comp;
22
+ }
23
+ return { components: map };
24
+ }
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ColumnMetaSchema = z.object({
4
+ name: z.string().min(1),
5
+ type: z.string().min(1),
6
+ nullable: z.boolean().default(false),
7
+ description: z.string().optional(),
8
+ });
9
+
10
+ export const TableMetaSchema = z.object({
11
+ name: z.string().min(1),
12
+ columns: z.array(ColumnMetaSchema),
13
+ description: z.string().optional(),
14
+ rowCount: z.number().optional(),
15
+ });
16
+
17
+ export const EndpointMetaSchema = z.object({
18
+ path: z.string().min(1),
19
+ method: z.enum(['GET', 'POST']),
20
+ description: z.string().optional(),
21
+ responseSchema: z.record(z.unknown()).optional(),
22
+ });
23
+
24
+ export const DataSourceMetaSchema = z.object({
25
+ id: z.string().min(1),
26
+ name: z.string().min(1),
27
+ type: z.enum(['sql', 'rest', 'static']),
28
+ description: z.string().optional(),
29
+ tables: z.array(TableMetaSchema).optional(),
30
+ endpoints: z.array(EndpointMetaSchema).optional(),
31
+ });
32
+
33
+ export type ColumnMeta = z.infer<typeof ColumnMetaSchema>;
34
+ export type TableMeta = z.infer<typeof TableMetaSchema>;
35
+ export type EndpointMeta = z.infer<typeof EndpointMetaSchema>;
36
+ export type DataSourceMeta = z.infer<typeof DataSourceMetaSchema>;
37
+
38
+ /**
39
+ * DataConnector port interface.
40
+ * Implemented in the server package for each data source type.
41
+ */
42
+ export interface DataConnector {
43
+ readonly meta: DataSourceMeta;
44
+ describe(): Promise<DataSourceMeta>;
45
+ query(params: Record<string, unknown>): Promise<unknown>;
46
+ }
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+
3
+ export const VisibilityConditionSchema = z
4
+ .object({
5
+ expr: z.string(),
6
+ })
7
+ .optional();
8
+
9
+ export const ElementDefSchema = z.object({
10
+ type: z.string().min(1),
11
+ props: z.record(z.unknown()).default({}),
12
+ children: z.array(z.string().min(1)).optional(),
13
+ visible: VisibilityConditionSchema,
14
+ });
15
+
16
+ export type ElementDef = z.infer<typeof ElementDefSchema>;
17
+ export type VisibilityCondition = z.infer<typeof VisibilityConditionSchema>;