-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapp.zod.ts
More file actions
384 lines (338 loc) · 13.6 KB
/
app.zod.ts
File metadata and controls
384 lines (338 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod';
import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod';
import { SharingConfigSchema, EmbedConfigSchema } from './sharing.zod';
/**
* Base Navigation Item Schema
* Shared properties for all navigation types.
*
* **NAMING CONVENTION:**
* Navigation item IDs are used in URLs and configuration and must be lowercase snake_case.
*
* @example Good IDs
* - 'menu_accounts'
* - 'page_dashboard'
* - 'nav_settings'
*
* @example Bad IDs (will be rejected)
* - 'MenuAccounts' (PascalCase)
* - 'Page Dashboard' (spaces)
*/
const BaseNavItemSchema = z.object({
/** Unique identifier for the item */
id: SnakeCaseIdentifierSchema.describe('Unique identifier for this navigation item (lowercase snake_case)'),
/** Display label */
label: I18nLabelSchema.describe('Display proper label'),
/** Icon name (Lucide) */
icon: z.string().optional().describe('Icon name'),
/** Sort order within the same level (lower numbers appear first) */
order: z.number().optional().describe('Sort order within the same level (lower = first)'),
/** Badge text or count displayed on the navigation item (e.g. "3", "New") */
badge: z.union([z.string(), z.number()]).optional().describe('Badge text or count displayed on the item'),
/**
* Visibility condition.
* Formula expression returning boolean.
* e.g. "user.is_admin || user.department == 'sales'"
*/
visible: z.string().optional().describe('Visibility formula condition'),
/** Permissions required to see/access this navigation item */
requiredPermissions: z.array(z.string()).optional().describe('Permissions required to access this item'),
});
/**
* 1. Object Navigation Item
* Navigates to an object's list view.
*/
export const ObjectNavItemSchema = BaseNavItemSchema.extend({
type: z.literal('object'),
objectName: z.string().describe('Target object name'),
viewName: z.string().optional().describe('Default list view to open. Defaults to "all"'),
});
/**
* 2. Dashboard Navigation Item
* Navigates to a specific dashboard.
*/
export const DashboardNavItemSchema = BaseNavItemSchema.extend({
type: z.literal('dashboard'),
dashboardName: z.string().describe('Target dashboard name'),
});
/**
* 3. Page Navigation Item
* Navigates to a custom UI page/component.
*/
export const PageNavItemSchema = BaseNavItemSchema.extend({
type: z.literal('page'),
pageName: z.string().describe('Target custom page component name'),
params: z.record(z.string(), z.unknown()).optional().describe('Parameters passed to the page context'),
});
/**
* 4. URL Navigation Item
* Navigates to an external or absolute URL.
*/
export const UrlNavItemSchema = BaseNavItemSchema.extend({
type: z.literal('url'),
url: z.string().describe('Target external URL'),
target: z.enum(['_self', '_blank']).default('_self').describe('Link target window'),
});
/**
* 5. Report Navigation Item
* Navigates to a specific report.
*/
export const ReportNavItemSchema = BaseNavItemSchema.extend({
type: z.literal('report'),
reportName: z.string().describe('Target report name'),
});
/**
* 6. Action Navigation Item
* Triggers an action (e.g. opening a flow, running a script, or launching a screen action).
*/
export const ActionNavItemSchema = BaseNavItemSchema.extend({
type: z.literal('action'),
actionDef: z.object({
actionName: z.string().describe('Action machine name to execute'),
params: z.record(z.string(), z.unknown()).optional().describe('Parameters passed to the action'),
}).describe('Action definition to execute when clicked'),
});
/**
* 7. Group Navigation Item
* A container for child navigation items (Sub-menu).
* Does not perform navigation itself.
*/
export const GroupNavItemSchema = BaseNavItemSchema.extend({
type: z.literal('group'),
expanded: z.boolean().default(false).describe('Default expansion state in sidebar'),
// children property is added in the recursive definition below
});
/**
* Recursive Union of all navigation item types.
* Allows constructing an unlimited-depth navigation tree.
*/
export const NavigationItemSchema: z.ZodType<any> = z.lazy(() =>
z.union([
ObjectNavItemSchema.extend({
children: z.array(NavigationItemSchema).optional().describe('Child navigation items (e.g. specific views)'),
}),
DashboardNavItemSchema,
PageNavItemSchema,
UrlNavItemSchema,
ReportNavItemSchema,
ActionNavItemSchema,
GroupNavItemSchema.extend({
children: z.array(NavigationItemSchema).describe('Child navigation items'),
})
])
);
/**
* App Branding Configuration
* Allows configuring the look and feel of the specific app.
*/
export const AppBrandingSchema = z.object({
primaryColor: z.string().optional().describe('Primary theme color hex code'),
logo: z.string().optional().describe('Custom logo URL for this app'),
favicon: z.string().optional().describe('Custom favicon URL for this app'),
});
/**
* Navigation Area Schema
*
* A logical grouping (zone/section) of navigation items, similar to Salesforce "App Areas"
* or Dynamics 365 "Site Map Areas". Each area represents a business domain (e.g. Sales, Service, Settings)
* and contains its own independent navigation tree.
*
* Areas allow large applications to partition navigation by business function while
* keeping a single AppSchema definition. The runtime may render areas as top-level tabs,
* sidebar sections, or a switchable navigation context.
*
* @example
* ```ts
* const salesArea: NavigationArea = {
* id: 'area_sales',
* label: 'Sales',
* icon: 'briefcase',
* order: 1,
* navigation: [
* { id: 'nav_leads', type: 'object', label: 'Leads', objectName: 'lead' },
* { id: 'nav_opportunities', type: 'object', label: 'Opportunities', objectName: 'opportunity' },
* ],
* };
* ```
*/
export const NavigationAreaSchema = z.object({
/** Unique area identifier */
id: SnakeCaseIdentifierSchema.describe('Unique area identifier (lowercase snake_case)'),
/** Display label */
label: I18nLabelSchema.describe('Area display label'),
/** Icon name (Lucide) */
icon: z.string().optional().describe('Area icon name'),
/** Sort order among areas (lower = first) */
order: z.number().optional().describe('Sort order among areas (lower = first)'),
/** Area description */
description: I18nLabelSchema.optional().describe('Area description'),
/**
* Visibility condition.
* Formula expression returning boolean.
*/
visible: z.string().optional().describe('Visibility formula condition for this area'),
/** Permissions required to access this area */
requiredPermissions: z.array(z.string()).optional().describe('Permissions required to access this area'),
/** Navigation items within this area */
navigation: z.array(NavigationItemSchema).describe('Navigation items within this area'),
});
/**
* Schema for Applications (Apps).
* A logical container for business functionality (e.g., "Sales CRM", "HR Portal").
*
* **NAMING CONVENTION:**
* App names are used in URLs and routing and must be lowercase snake_case.
* Prefix with 'app_' is recommended for clarity.
*
* @example Good app names
* - 'app_crm'
* - 'app_finance'
* - 'app_portal'
* - 'sales_app'
*
* @example Bad app names (will be rejected)
* - 'CRM' (uppercase)
* - 'FinanceApp' (mixed case)
* - 'Sales App' (spaces)
*/
/**
* App Configuration Schema
* Defines a business application container, including its navigation, branding, and permissions.
*
* The App is the top-level navigation shell. The `navigation[]` field holds the complete
* sidebar tree with unlimited nesting depth via `type: 'group'` items. Pages are referenced
* by name via `type: 'page'` items and defined independently.
*
* @example CRM App with nested navigation tree
* {
* name: "crm",
* label: "Sales CRM",
* icon: "briefcase",
* navigation: [
* { type: "group", id: "grp_sales", label: "Sales Cloud", expanded: true, children: [
* { type: "page", id: "nav_pipeline", label: "Pipeline", pageName: "page_pipeline" },
* { type: "page", id: "nav_accounts", label: "Accounts", pageName: "page_accounts" },
* ]},
* { type: "page", id: "nav_settings", label: "Settings", pageName: "admin_settings" },
* ]
* }
*/
export const AppSchema = z.object({
/** Machine name (id) */
name: SnakeCaseIdentifierSchema.describe('App unique machine name (lowercase snake_case)'),
/** Display label */
label: I18nLabelSchema.describe('App display label'),
/** App version */
version: z.string().optional().describe('App version'),
/** Description */
description: I18nLabelSchema.optional().describe('App description'),
/** Icon name (Lucide) */
icon: z.string().optional().describe('App icon used in the App Launcher'),
/** Branding/Theming Configuration */
branding: AppBrandingSchema.optional().describe('App-specific branding'),
/** Application status */
active: z.boolean().optional().default(true).describe('Whether the app is enabled'),
/** Is this the default app for new users? */
isDefault: z.boolean().optional().default(false).describe('Is default app'),
/**
* Full Navigation Tree — supports unlimited nesting depth.
* Pages are referenced by name via `type: 'page'` items.
* Groups can contain other groups for arbitrary sidebar depth.
*
* For simple apps, use `navigation` directly.
* For enterprise apps with multiple business domains, use `areas` instead.
*/
navigation: z.array(NavigationItemSchema).optional()
.describe('Full navigation tree for the app sidebar'),
/**
* Navigation Areas — partitions navigation by business domain.
* Each area defines an independent navigation tree (e.g. Sales, Service, Settings).
* When areas are defined, they take precedence over the top-level `navigation` array.
*
* @example
* ```ts
* areas: [
* { id: 'area_sales', label: 'Sales', icon: 'briefcase', order: 1, navigation: [...] },
* { id: 'area_service', label: 'Service', icon: 'headset', order: 2, navigation: [...] },
* ]
* ```
*/
areas: z.array(NavigationAreaSchema).optional()
.describe('Navigation areas for partitioning navigation by business domain'),
/**
* App-level Home Page Override
* ID of the navigation item to act as the landing page.
* If not set, usually defaults to the first navigation item.
*/
homePageId: z.string().optional().describe('ID of the navigation item to serve as landing page'),
/**
* Access Control
* List of permissions required to access this app.
* Modern replacement for role/profile based assignment.
* Example: ["app.access.crm"]
*/
requiredPermissions: z.array(z.string()).optional().describe('Permissions required to access this app'),
/**
* Package Components (For config file convenience)
* In a real monorepo these might be auto-discovered, but here we allow explicit registration.
*/
objects: z.array(z.unknown()).optional().describe('Objects belonging to this app'),
apis: z.array(z.unknown()).optional().describe('Custom APIs belonging to this app'),
/** Sharing configuration for public access */
sharing: SharingConfigSchema.optional().describe('Public sharing configuration'),
/** Embed configuration for iframe embedding */
embed: EmbedConfigSchema.optional().describe('Iframe embedding configuration'),
/** Mobile navigation mode */
mobileNavigation: z.object({
mode: z.enum(['drawer', 'bottom_nav', 'hamburger']).default('drawer')
.describe('Mobile navigation mode: drawer sidebar, bottom navigation bar, or hamburger menu'),
bottomNavItems: z.array(z.string()).optional()
.describe('Navigation item IDs to show in bottom nav (max 5)'),
}).optional().describe('Mobile-specific navigation configuration'),
/** ARIA accessibility attributes */
aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes for the application'),
});
/**
* App Factory Helper
*/
export const App = {
create: (config: z.input<typeof AppSchema>): App => AppSchema.parse(config),
} as const;
/**
* Type-safe factory for creating application definitions.
*
* Validates the config at creation time using Zod `.parse()`.
*
* @example CRM App with nested navigation tree
* ```ts
* const crmApp = defineApp({
* name: 'crm',
* label: 'Sales CRM',
* navigation: [
* { id: 'grp_sales', type: 'group', label: 'Sales Cloud', expanded: true, children: [
* { id: 'nav_pipeline', type: 'page', label: 'Pipeline', pageName: 'page_pipeline' },
* { id: 'nav_accounts', type: 'page', label: 'Accounts', pageName: 'page_accounts' },
* ]},
* { id: 'nav_settings', type: 'page', label: 'Settings', pageName: 'admin_settings' },
* ],
* });
* ```
*/
export function defineApp(config: z.input<typeof AppSchema>): App {
return AppSchema.parse(config);
}
// Main Types
export type App = z.infer<typeof AppSchema>;
export type AppInput = z.input<typeof AppSchema>;
export type AppBranding = z.infer<typeof AppBrandingSchema>;
export type NavigationItem = z.infer<typeof NavigationItemSchema>;
export type NavigationArea = z.infer<typeof NavigationAreaSchema>;
// Discriminated Item Types (Helper exports)
export type ObjectNavItem = z.infer<typeof ObjectNavItemSchema>;
export type DashboardNavItem = z.infer<typeof DashboardNavItemSchema>;
export type PageNavItem = z.infer<typeof PageNavItemSchema>;
export type UrlNavItem = z.infer<typeof UrlNavItemSchema>;
export type ReportNavItem = z.infer<typeof ReportNavItemSchema>;
export type ActionNavItem = z.infer<typeof ActionNavItemSchema>;
export type GroupNavItem = z.infer<typeof GroupNavItemSchema> & { children: NavigationItem[] };