openxiangda-devkit-core 2.0.0-alpha.72 → 2.0.0-alpha.75
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/application-services.d.ts +9 -0
- package/dist/application-services.d.ts.map +1 -1
- package/dist/application-services.js +11 -1
- package/dist/application-services.js.map +1 -1
- package/dist/compiler/ai-catalog.d.ts.map +1 -1
- package/dist/compiler/ai-catalog.js +38 -4
- package/dist/compiler/ai-catalog.js.map +1 -1
- package/dist/compiler/bundle.d.ts.map +1 -1
- package/dist/compiler/bundle.js +234 -6
- package/dist/compiler/bundle.js.map +1 -1
- package/dist/compiler/config.d.ts +46 -1
- package/dist/compiler/config.d.ts.map +1 -1
- package/dist/compiler/config.js +441 -6
- package/dist/compiler/config.js.map +1 -1
- package/dist/compiler/field-codec.d.ts +7 -0
- package/dist/compiler/field-codec.d.ts.map +1 -1
- package/dist/compiler/field-codec.js +7 -0
- package/dist/compiler/field-codec.js.map +1 -1
- package/dist/compiler/schema-composition.js +8 -0
- package/dist/compiler/schema-composition.js.map +1 -1
- package/dist/control-plane-client.d.ts +2 -2
- package/dist/control-plane-client.d.ts.map +1 -1
- package/dist/workspace-loader.js +1 -1
- package/dist/workspace-loader.js.map +1 -1
- package/package.json +2 -2
package/dist/compiler/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SCHEMA_VERSIONS, DATA_FIELD_TYPES, PLATFORM_EVENT_TYPES_V2, nativePlatformCapabilityCatalog, validateDataResource, } from 'openxiangda-contracts';
|
|
2
2
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
3
3
|
import { CronExpressionParser } from 'cron-parser';
|
|
4
|
-
import { fieldNullable,
|
|
4
|
+
import { fieldNullable, supportsGeneratedMutation, } from './field-codec.js';
|
|
5
5
|
import { supportsFilter, supportsSearch, supportsSort, } from './field-query-plan.js';
|
|
6
6
|
import { physicalPlanForField } from './field-physical-plan.js';
|
|
7
7
|
import { SEMANTIC_FIELD_PATH_PATTERN, semanticFieldPathRoot, validateAuthzSemanticBindings, } from './field-policy-path.js';
|
|
@@ -19,6 +19,150 @@ const DATE_TRIGGER_RESERVED_DATA_FIELDS = new Set([
|
|
|
19
19
|
'dueAt',
|
|
20
20
|
]);
|
|
21
21
|
const APP_DATA_FIELD_TYPES = new Set(DATA_FIELD_TYPES);
|
|
22
|
+
function adminNavigationItem(page, options = {}) {
|
|
23
|
+
return Object.freeze({ page: Object.freeze(page), ...options });
|
|
24
|
+
}
|
|
25
|
+
/** Explicitly owns the complete admin menu; the compiler never appends pages. */
|
|
26
|
+
export function defineAdminNavigation(groups) {
|
|
27
|
+
return Object.freeze(groups.map(group => Object.freeze({
|
|
28
|
+
...group,
|
|
29
|
+
items: Object.freeze(group.items.map(item => Object.freeze({ ...item }))),
|
|
30
|
+
})));
|
|
31
|
+
}
|
|
32
|
+
export function adminNavigationGroup(code, label, items, options = {}) {
|
|
33
|
+
return Object.freeze({ code, label, items: Object.freeze([...items]), ...options });
|
|
34
|
+
}
|
|
35
|
+
export function adminResourcePage(resourceCode, options) {
|
|
36
|
+
return adminNavigationItem({ kind: 'resource', resourceCode }, options);
|
|
37
|
+
}
|
|
38
|
+
export function adminOperationPage(routeCode, options) {
|
|
39
|
+
return adminNavigationItem({ kind: 'operation', routeCode }, options);
|
|
40
|
+
}
|
|
41
|
+
export function adminApplicationTodoCenterPage(options) {
|
|
42
|
+
return adminNavigationItem({ kind: 'application-todo-center' }, options);
|
|
43
|
+
}
|
|
44
|
+
export function adminWorkflowWorkCenterPage(options) {
|
|
45
|
+
return adminNavigationItem({ kind: 'workflow-work-center' }, options);
|
|
46
|
+
}
|
|
47
|
+
export function adminWorkflowLaunchPage(workflowCode, options) {
|
|
48
|
+
return adminNavigationItem({ kind: 'workflow-launch', workflowCode }, options);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Produces a deterministic one-time authoring suggestion for tooling. The
|
|
52
|
+
* compiler never calls this function and never mutates a saved declaration.
|
|
53
|
+
*/
|
|
54
|
+
export function suggestAdminNavigation(config) {
|
|
55
|
+
const groups = [];
|
|
56
|
+
const resources = [...(config.data?.resources || [])]
|
|
57
|
+
.filter(resource => resource.surface?.generated?.list !== false)
|
|
58
|
+
.sort((left, right) => left.code < right.code ? -1 : left.code > right.code ? 1 : 0);
|
|
59
|
+
if (resources.length > 0) {
|
|
60
|
+
groups.push(adminNavigationGroup('data-management', '数据管理', resources.map(resource => adminResourcePage(resource.code, { label: resource.name })), { icon: 'database', order: 100 }));
|
|
61
|
+
}
|
|
62
|
+
const routes = [...(config.frontend.routes || [])]
|
|
63
|
+
.filter(route => route.surface === 'admin' && !/[:*]/.test(route.path))
|
|
64
|
+
.sort((left, right) => left.code < right.code ? -1 : left.code > right.code ? 1 : 0);
|
|
65
|
+
if (routes.length > 0) {
|
|
66
|
+
groups.push(adminNavigationGroup('business-operations', '业务操作', routes.map(route => adminOperationPage(route.code, { label: route.label })), { icon: 'operations', order: 200 }));
|
|
67
|
+
}
|
|
68
|
+
const workflowCodes = [
|
|
69
|
+
...new Set((config.workflows?.definitions || []).map(declaration => declaration.definition.code)),
|
|
70
|
+
].sort();
|
|
71
|
+
const workflowItems = [];
|
|
72
|
+
if (workflowCodes.length > 0) {
|
|
73
|
+
workflowItems.push(adminWorkflowWorkCenterPage({ label: '我的审批' }));
|
|
74
|
+
}
|
|
75
|
+
for (const code of workflowCodes) {
|
|
76
|
+
const declarations = (config.workflows?.definitions || []).filter(declaration => declaration.definition.code === code);
|
|
77
|
+
const activeVersion = config.workflows?.activations.find(activation => activation.workflowCode === code)?.definitionVersion;
|
|
78
|
+
const selected = declarations.find(declaration => declaration.version === activeVersion) ||
|
|
79
|
+
[...declarations].sort((left, right) => right.version - left.version)[0];
|
|
80
|
+
if (selected?.launch?.mode === 'standalone') {
|
|
81
|
+
workflowItems.push(adminWorkflowLaunchPage(code, {
|
|
82
|
+
label: `发起${selected.definition.title}`,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (workflowItems.length > 0) {
|
|
87
|
+
groups.push(adminNavigationGroup('workflow-center', '流程中心', workflowItems, {
|
|
88
|
+
icon: 'workflow',
|
|
89
|
+
order: 300,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
return defineAdminNavigation(groups);
|
|
93
|
+
}
|
|
94
|
+
function renderAdminNavigationOptions(options) {
|
|
95
|
+
const entries = [
|
|
96
|
+
...(options.label !== undefined
|
|
97
|
+
? [`label: ${JSON.stringify(options.label)}`]
|
|
98
|
+
: []),
|
|
99
|
+
...(options.icon !== undefined
|
|
100
|
+
? [`icon: ${JSON.stringify(options.icon)}`]
|
|
101
|
+
: []),
|
|
102
|
+
...(options.order !== undefined ? [`order: ${options.order}`] : []),
|
|
103
|
+
];
|
|
104
|
+
return entries.length > 0 ? `{ ${entries.join(', ')} }` : undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Renders the deterministic proposal as a typed expression that an AI author
|
|
108
|
+
* can copy once into frontend.admin.navigation and then edit normally.
|
|
109
|
+
*/
|
|
110
|
+
export function renderAdminNavigationSuggestion(config) {
|
|
111
|
+
const navigation = suggestAdminNavigation(config);
|
|
112
|
+
const imports = new Set(['defineAdminNavigation']);
|
|
113
|
+
const groups = navigation.map(group => {
|
|
114
|
+
imports.add('adminNavigationGroup');
|
|
115
|
+
const items = group.items.map(item => {
|
|
116
|
+
const options = renderAdminNavigationOptions(item);
|
|
117
|
+
let expression;
|
|
118
|
+
switch (item.page.kind) {
|
|
119
|
+
case 'resource':
|
|
120
|
+
imports.add('adminResourcePage');
|
|
121
|
+
expression = `adminResourcePage(${JSON.stringify(item.page.resourceCode)}${options ? `, ${options}` : ''})`;
|
|
122
|
+
break;
|
|
123
|
+
case 'operation':
|
|
124
|
+
imports.add('adminOperationPage');
|
|
125
|
+
expression = `adminOperationPage(${JSON.stringify(item.page.routeCode)}${options ? `, ${options}` : ''})`;
|
|
126
|
+
break;
|
|
127
|
+
case 'application-todo-center':
|
|
128
|
+
imports.add('adminApplicationTodoCenterPage');
|
|
129
|
+
expression = `adminApplicationTodoCenterPage(${options || ''})`;
|
|
130
|
+
break;
|
|
131
|
+
case 'workflow-work-center':
|
|
132
|
+
imports.add('adminWorkflowWorkCenterPage');
|
|
133
|
+
expression = `adminWorkflowWorkCenterPage(${options || ''})`;
|
|
134
|
+
break;
|
|
135
|
+
case 'workflow-launch':
|
|
136
|
+
imports.add('adminWorkflowLaunchPage');
|
|
137
|
+
expression = `adminWorkflowLaunchPage(${JSON.stringify(item.page.workflowCode)}${options ? `, ${options}` : ''})`;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
return ` ${expression},`;
|
|
141
|
+
});
|
|
142
|
+
const options = renderAdminNavigationOptions({
|
|
143
|
+
icon: group.icon,
|
|
144
|
+
order: group.order,
|
|
145
|
+
});
|
|
146
|
+
return [
|
|
147
|
+
' adminNavigationGroup(',
|
|
148
|
+
` ${JSON.stringify(group.code)},`,
|
|
149
|
+
` ${JSON.stringify(group.label)},`,
|
|
150
|
+
' [',
|
|
151
|
+
...items,
|
|
152
|
+
' ],',
|
|
153
|
+
...(options ? [` ${options}`] : []),
|
|
154
|
+
' ),',
|
|
155
|
+
].join('\n');
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
module: 'openxiangda/config',
|
|
159
|
+
imports: [...imports].sort(),
|
|
160
|
+
proposal: navigation,
|
|
161
|
+
expression: ['defineAdminNavigation([', ...groups, '])'].join('\n'),
|
|
162
|
+
groupCount: navigation.length,
|
|
163
|
+
itemCount: navigation.reduce((count, group) => count + group.items.length, 0),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
22
166
|
const authorizationProjectionFieldPathSchema = {
|
|
23
167
|
type: 'string',
|
|
24
168
|
pattern: '^[A-Za-z_][A-Za-z0-9_]{0,127}(?:\\.(?:value|snapshot\\.[A-Za-z_][A-Za-z0-9_]{0,127}))?$',
|
|
@@ -625,6 +769,7 @@ export function validateAppConfig(value) {
|
|
|
625
769
|
diagnostics.push(diagnostic('APP_CONFIG_BACKEND_RESOURCE_PROFILE_UNSUPPORTED', 'backend.resourceProfile 必须是 light 或 standard', 'backend.resourceProfile'));
|
|
626
770
|
}
|
|
627
771
|
validateFrontendRoutes(frontend.routes, diagnostics);
|
|
772
|
+
validateAdminNavigation(config, diagnostics);
|
|
628
773
|
validatePerspectives(config.perspectives, object(config.authz).roles, diagnostics);
|
|
629
774
|
const declaredResourceCodes = new Set((Array.isArray(object(config.data).resources)
|
|
630
775
|
? object(config.data).resources
|
|
@@ -765,6 +910,24 @@ export function validateAppConfig(value) {
|
|
|
765
910
|
if (!Array.isArray(authz.roles)) {
|
|
766
911
|
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLES_REQUIRED', 'authz.roles 必须是数组', 'authz.roles'));
|
|
767
912
|
}
|
|
913
|
+
const forbiddenNativeMutationCapabilities = new Map();
|
|
914
|
+
for (const rawResource of Array.isArray(data.resources)
|
|
915
|
+
? data.resources
|
|
916
|
+
: []) {
|
|
917
|
+
const resource = object(rawResource);
|
|
918
|
+
const mutationOwner = string(object(resource.surface).mutationOwner) || 'native';
|
|
919
|
+
if (mutationOwner === 'native')
|
|
920
|
+
continue;
|
|
921
|
+
for (const operation of ['create', 'update', 'delete']) {
|
|
922
|
+
const capability = string(object(resource.capabilities)[operation]);
|
|
923
|
+
if (capability) {
|
|
924
|
+
forbiddenNativeMutationCapabilities.set(capability, {
|
|
925
|
+
resourceCode: string(resource.code),
|
|
926
|
+
operation,
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
768
931
|
const roleCodes = new Set();
|
|
769
932
|
roles.forEach((raw, index) => {
|
|
770
933
|
const role = object(raw);
|
|
@@ -791,6 +954,10 @@ export function validateAppConfig(value) {
|
|
|
791
954
|
seenCapabilities.has(capability)) {
|
|
792
955
|
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_CAPABILITY_INVALID', 'capability 必须是非空稳定标识,且同一角色内不能重复', `authz.roles[${index}].capabilities[${capabilityIndex}]`));
|
|
793
956
|
}
|
|
957
|
+
const forbiddenMutation = forbiddenNativeMutationCapabilities.get(capability);
|
|
958
|
+
if (forbiddenMutation) {
|
|
959
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_NATIVE_MUTATION_GRANT_FORBIDDEN', `${forbiddenMutation.resourceCode} 的 ${forbiddenMutation.operation} 由非 Native owner 管理,应用角色不能获得对应 Native Data capability`, `authz.roles[${index}].capabilities[${capabilityIndex}]`));
|
|
960
|
+
}
|
|
794
961
|
seenCapabilities.add(capability);
|
|
795
962
|
});
|
|
796
963
|
});
|
|
@@ -1580,6 +1747,10 @@ export function validateAppConfig(value) {
|
|
|
1580
1747
|
const editableParameters = Array.isArray(workflows.editableParameters)
|
|
1581
1748
|
? workflows.editableParameters
|
|
1582
1749
|
: [];
|
|
1750
|
+
const frontendRoutesByCode = new Map((Array.isArray(frontend.routes) ? frontend.routes : []).map(routeValue => {
|
|
1751
|
+
const route = object(routeValue);
|
|
1752
|
+
return [string(route.code), route];
|
|
1753
|
+
}));
|
|
1583
1754
|
const definitionByKey = new Map();
|
|
1584
1755
|
const bindingByKey = new Map();
|
|
1585
1756
|
const providerKeys = new Set();
|
|
@@ -1630,9 +1801,62 @@ export function validateAppConfig(value) {
|
|
|
1630
1801
|
const definition = object(declaration.definition);
|
|
1631
1802
|
const version = Number(declaration.version);
|
|
1632
1803
|
const path = `workflows.definitions[${index}]`;
|
|
1804
|
+
const launch = object(declaration.launch);
|
|
1805
|
+
if (Object.keys(declaration).some(key => !['version', 'definition', 'launch', 'detailRouteCode'].includes(key))) {
|
|
1806
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DEFINITION_DECLARATION_INVALID', 'Workflow definition declaration 只能声明 version、definition、launch 和 detailRouteCode', path));
|
|
1807
|
+
}
|
|
1808
|
+
if (declaration.launch !== undefined &&
|
|
1809
|
+
(Object.keys(launch).some(key => key !== 'mode') ||
|
|
1810
|
+
![
|
|
1811
|
+
'standalone',
|
|
1812
|
+
'custom-page',
|
|
1813
|
+
'hidden-handoff',
|
|
1814
|
+
'work-center-only',
|
|
1815
|
+
].includes(string(launch.mode)))) {
|
|
1816
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_LAUNCH_MODE_INVALID', 'Workflow launch.mode 必须是 standalone、custom-page、hidden-handoff 或 work-center-only', `${path}.launch.mode`));
|
|
1817
|
+
}
|
|
1818
|
+
if (declaration.detailRouteCode !== undefined) {
|
|
1819
|
+
const detailRouteCode = object(declaration.detailRouteCode);
|
|
1820
|
+
const desktopCode = string(detailRouteCode.desktop);
|
|
1821
|
+
const mobileCode = string(detailRouteCode.mobile);
|
|
1822
|
+
if (Object.keys(detailRouteCode).some(key => !['desktop', 'mobile'].includes(key)) ||
|
|
1823
|
+
!OPERATION_CODE_PATTERN.test(desktopCode) ||
|
|
1824
|
+
!OPERATION_CODE_PATTERN.test(mobileCode) ||
|
|
1825
|
+
desktopCode === mobileCode) {
|
|
1826
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_INVALID', 'detailRouteCode 必须声明两个不同且合法的 desktop/mobile route code', `${path}.detailRouteCode`));
|
|
1827
|
+
}
|
|
1828
|
+
else {
|
|
1829
|
+
for (const [device, code, surface] of [
|
|
1830
|
+
['desktop', desktopCode, 'admin'],
|
|
1831
|
+
['mobile', mobileCode, 'user'],
|
|
1832
|
+
]) {
|
|
1833
|
+
const route = frontendRoutesByCode.get(code);
|
|
1834
|
+
const routePointer = `${path}.detailRouteCode.${device}`;
|
|
1835
|
+
if (!route) {
|
|
1836
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_REFERENCE_MISSING', `detailRouteCode.${device} 必须引用已声明 frontend route`, routePointer));
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
if (string(route.surface) !== surface) {
|
|
1840
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_SURFACE_INVALID', `detailRouteCode.${device} 必须引用 ${surface} surface`, routePointer));
|
|
1841
|
+
}
|
|
1842
|
+
const parameters = [
|
|
1843
|
+
...string(route.path).matchAll(/:([A-Za-z][A-Za-z0-9_]*)/g),
|
|
1844
|
+
].map(match => match[1]);
|
|
1845
|
+
if (string(route.path).includes('*') ||
|
|
1846
|
+
parameters.length !== 1 ||
|
|
1847
|
+
parameters[0] !== 'instanceId') {
|
|
1848
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_PATH_INVALID', `detailRouteCode.${device} 路由必须且只能声明 :instanceId 动态参数`, routePointer));
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1633
1853
|
if (!Number.isInteger(version) || version < 1) {
|
|
1634
1854
|
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_VERSION_INVALID', 'Workflow definition version 必须是正整数', `${path}.version`));
|
|
1635
1855
|
}
|
|
1856
|
+
if (string(object(declaration.definition).title) ===
|
|
1857
|
+
string(object(declaration.definition).code)) {
|
|
1858
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_USER_TITLE_REQUIRED', 'Workflow title 必须是用户可读本地化名称,不能等于内部 code', `${path}.definition.title`));
|
|
1859
|
+
}
|
|
1636
1860
|
for (const error of validateWorkflowDefinition(definition)) {
|
|
1637
1861
|
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DEFINITION_INVALID', error, path));
|
|
1638
1862
|
}
|
|
@@ -1892,7 +2116,6 @@ function validateFrontendRoutes(rawRoutes, diagnostics) {
|
|
|
1892
2116
|
'path',
|
|
1893
2117
|
'label',
|
|
1894
2118
|
'surface',
|
|
1895
|
-
'navigation',
|
|
1896
2119
|
'parentCode',
|
|
1897
2120
|
'capability',
|
|
1898
2121
|
'access',
|
|
@@ -1932,8 +2155,6 @@ function validateFrontendRoutes(rawRoutes, diagnostics) {
|
|
|
1932
2155
|
paths.has(routePath) ||
|
|
1933
2156
|
!string(route.label) ||
|
|
1934
2157
|
!['admin', 'user'].includes(string(route.surface)) ||
|
|
1935
|
-
(route.navigation !== undefined &&
|
|
1936
|
-
!['menu', 'hidden'].includes(string(route.navigation))) ||
|
|
1937
2158
|
(route.capability !== undefined &&
|
|
1938
2159
|
!CAPABILITY_PATTERN.test(string(route.capability))) ||
|
|
1939
2160
|
accessInvalid ||
|
|
@@ -1968,6 +2189,160 @@ function validateFrontendRoutes(rawRoutes, diagnostics) {
|
|
|
1968
2189
|
}
|
|
1969
2190
|
}
|
|
1970
2191
|
}
|
|
2192
|
+
const ADMIN_NAVIGATION_ICONS = new Set([
|
|
2193
|
+
'overview',
|
|
2194
|
+
'database',
|
|
2195
|
+
'operations',
|
|
2196
|
+
'workflow',
|
|
2197
|
+
'members',
|
|
2198
|
+
'calendar',
|
|
2199
|
+
'document',
|
|
2200
|
+
'folder',
|
|
2201
|
+
'settings',
|
|
2202
|
+
]);
|
|
2203
|
+
function validAdminOrder(value) {
|
|
2204
|
+
return (value === undefined ||
|
|
2205
|
+
(Number.isSafeInteger(value) && Math.abs(Number(value)) <= 10_000));
|
|
2206
|
+
}
|
|
2207
|
+
function validateAdminNavigation(config, diagnostics) {
|
|
2208
|
+
const frontend = object(config.frontend);
|
|
2209
|
+
const admin = object(frontend.admin);
|
|
2210
|
+
const rawNavigation = admin.navigation;
|
|
2211
|
+
if (frontend.admin !== undefined &&
|
|
2212
|
+
(Object.keys(admin).some(key => key !== 'navigation') ||
|
|
2213
|
+
!Array.isArray(rawNavigation))) {
|
|
2214
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_NAVIGATION_INVALID', 'frontend.admin 只接受 navigation 数组', 'frontend.admin.navigation'));
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
const groups = Array.isArray(rawNavigation) ? rawNavigation : [];
|
|
2218
|
+
if (groups.length > 100) {
|
|
2219
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_NAVIGATION_LIMIT_EXCEEDED', '后台导航最多包含 100 个业务分组', 'frontend.admin.navigation'));
|
|
2220
|
+
}
|
|
2221
|
+
const resources = new Map((config.data?.resources || []).map(resource => [resource.code, resource]));
|
|
2222
|
+
const routes = new Map((config.frontend.routes || []).map(route => [route.code, route]));
|
|
2223
|
+
const workflows = new Map();
|
|
2224
|
+
const workflowDefinitionsByCode = new Map();
|
|
2225
|
+
for (const declaration of config.workflows?.definitions || []) {
|
|
2226
|
+
const declarations = workflowDefinitionsByCode.get(declaration.definition.code) || [];
|
|
2227
|
+
declarations.push(declaration);
|
|
2228
|
+
workflowDefinitionsByCode.set(declaration.definition.code, declarations);
|
|
2229
|
+
}
|
|
2230
|
+
for (const [code, declarations] of workflowDefinitionsByCode) {
|
|
2231
|
+
const activeVersion = config.workflows?.activations.find(activation => activation.workflowCode === code)?.definitionVersion;
|
|
2232
|
+
const selected = declarations.find(declaration => declaration.version === activeVersion) ||
|
|
2233
|
+
[...declarations].sort((left, right) => right.version - left.version)[0];
|
|
2234
|
+
if (selected) {
|
|
2235
|
+
workflows.set(code, {
|
|
2236
|
+
title: selected.definition.title,
|
|
2237
|
+
mode: selected.launch?.mode || 'work-center-only',
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
const groupCodes = new Set();
|
|
2242
|
+
const pageKeys = new Set();
|
|
2243
|
+
let itemCount = 0;
|
|
2244
|
+
groups.forEach((rawGroup, groupIndex) => {
|
|
2245
|
+
const group = object(rawGroup);
|
|
2246
|
+
const path = `frontend.admin.navigation[${groupIndex}]`;
|
|
2247
|
+
const code = string(group.code);
|
|
2248
|
+
const label = string(group.label);
|
|
2249
|
+
const items = Array.isArray(group.items) ? group.items : [];
|
|
2250
|
+
if (Object.keys(group).some(key => !['code', 'label', 'icon', 'order', 'items'].includes(key)) ||
|
|
2251
|
+
!OPERATION_CODE_PATTERN.test(code) ||
|
|
2252
|
+
groupCodes.has(code) ||
|
|
2253
|
+
!label ||
|
|
2254
|
+
label === code ||
|
|
2255
|
+
(group.icon !== undefined &&
|
|
2256
|
+
!ADMIN_NAVIGATION_ICONS.has(string(group.icon))) ||
|
|
2257
|
+
!validAdminOrder(group.order)) {
|
|
2258
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_GROUP_INVALID', '导航分组必须声明唯一 code、用户 label、受支持 icon/order 和 items', path));
|
|
2259
|
+
}
|
|
2260
|
+
if (items.length === 0) {
|
|
2261
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_GROUP_ORPHANED', '导航分组必须至少引用一个可见页面', `${path}.items`));
|
|
2262
|
+
}
|
|
2263
|
+
groupCodes.add(code);
|
|
2264
|
+
items.forEach((rawItem, itemIndex) => {
|
|
2265
|
+
itemCount += 1;
|
|
2266
|
+
const item = object(rawItem);
|
|
2267
|
+
const itemPath = `${path}.items[${itemIndex}]`;
|
|
2268
|
+
const page = object(item.page);
|
|
2269
|
+
const kind = string(page.kind);
|
|
2270
|
+
let pageKey = '';
|
|
2271
|
+
let defaultLabel = '';
|
|
2272
|
+
let internalCode = '';
|
|
2273
|
+
let exists = false;
|
|
2274
|
+
let standaloneViolation = false;
|
|
2275
|
+
if (kind === 'resource') {
|
|
2276
|
+
const resourceCode = string(page.resourceCode);
|
|
2277
|
+
const resource = resources.get(resourceCode);
|
|
2278
|
+
pageKey = `resource:${resourceCode}:list`;
|
|
2279
|
+
defaultLabel = resource?.name || '';
|
|
2280
|
+
internalCode = resourceCode;
|
|
2281
|
+
exists = Boolean(resource?.surface?.generated?.list);
|
|
2282
|
+
}
|
|
2283
|
+
else if (kind === 'operation') {
|
|
2284
|
+
const routeCode = string(page.routeCode);
|
|
2285
|
+
const route = routes.get(routeCode);
|
|
2286
|
+
pageKey = `operation:${routeCode}`;
|
|
2287
|
+
defaultLabel = route?.label || '';
|
|
2288
|
+
internalCode = routeCode;
|
|
2289
|
+
exists = Boolean(route && route.surface === 'admin' && !/[:*]/.test(route.path));
|
|
2290
|
+
}
|
|
2291
|
+
else if (kind === 'application-todo-center') {
|
|
2292
|
+
pageKey = 'application:todo-center';
|
|
2293
|
+
defaultLabel = '待办中心';
|
|
2294
|
+
internalCode = pageKey;
|
|
2295
|
+
exists = true;
|
|
2296
|
+
}
|
|
2297
|
+
else if (kind === 'workflow-work-center') {
|
|
2298
|
+
pageKey = 'workflow:work-center';
|
|
2299
|
+
defaultLabel = '我的审批';
|
|
2300
|
+
internalCode = pageKey;
|
|
2301
|
+
exists = workflows.size > 0;
|
|
2302
|
+
}
|
|
2303
|
+
else if (kind === 'workflow-launch') {
|
|
2304
|
+
const workflowCode = string(page.workflowCode);
|
|
2305
|
+
const workflow = workflows.get(workflowCode);
|
|
2306
|
+
pageKey = `workflow:${workflowCode}:launch`;
|
|
2307
|
+
defaultLabel = workflow ? `发起${workflow.title}` : '';
|
|
2308
|
+
internalCode = workflowCode;
|
|
2309
|
+
standaloneViolation = Boolean(workflow && workflow.mode !== 'standalone');
|
|
2310
|
+
exists = Boolean(workflow && workflow.mode === 'standalone');
|
|
2311
|
+
}
|
|
2312
|
+
if (Object.keys(item).some(key => !['page', 'label', 'icon', 'order'].includes(key)) ||
|
|
2313
|
+
![
|
|
2314
|
+
'resource',
|
|
2315
|
+
'operation',
|
|
2316
|
+
'application-todo-center',
|
|
2317
|
+
'workflow-work-center',
|
|
2318
|
+
'workflow-launch',
|
|
2319
|
+
].includes(kind) ||
|
|
2320
|
+
(item.icon !== undefined &&
|
|
2321
|
+
!ADMIN_NAVIGATION_ICONS.has(string(item.icon))) ||
|
|
2322
|
+
!validAdminOrder(item.order)) {
|
|
2323
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_ITEM_INVALID', '导航项必须使用受支持的页面 helper、label、icon 和 order', itemPath));
|
|
2324
|
+
}
|
|
2325
|
+
if (standaloneViolation) {
|
|
2326
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_WORKFLOW_NOT_STANDALONE', '只有 self-contained standalone Workflow launch page 可以进入菜单', `${itemPath}.page`));
|
|
2327
|
+
}
|
|
2328
|
+
else if (!exists) {
|
|
2329
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_PAGE_NOT_FOUND', '导航引用必须指向存在且可进入菜单的 page registry 页面', `${itemPath}.page`));
|
|
2330
|
+
}
|
|
2331
|
+
if (pageKey && pageKeys.has(pageKey)) {
|
|
2332
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_PAGE_DUPLICATE', `同一页面不能重复出现在导航中: ${pageKey}`, `${itemPath}.page`));
|
|
2333
|
+
}
|
|
2334
|
+
if (pageKey)
|
|
2335
|
+
pageKeys.add(pageKey);
|
|
2336
|
+
const label = string(item.label) || defaultLabel;
|
|
2337
|
+
if (!label || label === internalCode || label === pageKey) {
|
|
2338
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_USER_LABEL_REQUIRED', '导航页面必须解析为用户可读 label,不能泄漏内部 code', item.label === undefined ? `${itemPath}.page` : `${itemPath}.label`));
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
});
|
|
2342
|
+
if (itemCount > 500) {
|
|
2343
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_NAVIGATION_LIMIT_EXCEEDED', '后台导航最多包含 500 个页面引用', 'frontend.admin.navigation'));
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
1971
2346
|
function validateAuthorizationTransitions(rawTransitions, _roleCodes, diagnostics) {
|
|
1972
2347
|
if (rawTransitions !== undefined && !Array.isArray(rawTransitions)) {
|
|
1973
2348
|
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_TRANSITIONS_INVALID', 'authz.authorizationTransitions 必须是数组', 'authz.authorizationTransitions'));
|
|
@@ -2161,6 +2536,15 @@ export function resourceReadPolicy(input) {
|
|
|
2161
2536
|
}
|
|
2162
2537
|
export function materializeDataResource(appCode, declaration) {
|
|
2163
2538
|
const capabilities = resourceCapabilityCodes(appCode, declaration.code);
|
|
2539
|
+
const mutationOwner = declaration.mutationOwner || 'native';
|
|
2540
|
+
const nativeMutations = mutationOwner === 'native';
|
|
2541
|
+
const generated = {
|
|
2542
|
+
list: declaration.generated?.list ?? true,
|
|
2543
|
+
detail: declaration.generated?.detail ?? true,
|
|
2544
|
+
create: declaration.generated?.create ?? nativeMutations,
|
|
2545
|
+
update: declaration.generated?.update ?? nativeMutations,
|
|
2546
|
+
delete: declaration.generated?.delete ?? nativeMutations,
|
|
2547
|
+
};
|
|
2164
2548
|
const fields = declaration.fields.map(field => {
|
|
2165
2549
|
const definition = {
|
|
2166
2550
|
code: field.code,
|
|
@@ -2176,6 +2560,8 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
2176
2560
|
...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),
|
|
2177
2561
|
...(field.precision !== undefined ? { precision: field.precision } : {}),
|
|
2178
2562
|
...(field.scale !== undefined ? { scale: field.scale } : {}),
|
|
2563
|
+
...(field.min !== undefined ? { min: field.min } : {}),
|
|
2564
|
+
...(field.max !== undefined ? { max: field.max } : {}),
|
|
2179
2565
|
...(field.timePrecision ? { timePrecision: field.timePrecision } : {}),
|
|
2180
2566
|
...(field.file ? { file: field.file } : {}),
|
|
2181
2567
|
...(field.serial ? { serial: field.serial } : {}),
|
|
@@ -2194,8 +2580,13 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
2194
2580
|
.filter(field => field.filter === true)
|
|
2195
2581
|
.map(field => field.code);
|
|
2196
2582
|
const access = (field, operation) => {
|
|
2197
|
-
if (
|
|
2583
|
+
if (operation !== 'read' &&
|
|
2584
|
+
!supportsGeneratedMutation({
|
|
2585
|
+
type: field.type,
|
|
2586
|
+
system: field.system,
|
|
2587
|
+
})) {
|
|
2198
2588
|
return [];
|
|
2589
|
+
}
|
|
2199
2590
|
const override = field.access?.[operation];
|
|
2200
2591
|
if (override === false)
|
|
2201
2592
|
return [];
|
|
@@ -2204,7 +2595,8 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
2204
2595
|
return [capabilities[operation]];
|
|
2205
2596
|
};
|
|
2206
2597
|
const surface = {
|
|
2207
|
-
|
|
2598
|
+
mutationOwner,
|
|
2599
|
+
generated,
|
|
2208
2600
|
fields: Object.fromEntries(declaration.fields.map(field => [
|
|
2209
2601
|
field.code,
|
|
2210
2602
|
{
|
|
@@ -2220,6 +2612,8 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
2220
2612
|
...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),
|
|
2221
2613
|
...(field.precision !== undefined ? { precision: field.precision } : {}),
|
|
2222
2614
|
...(field.scale !== undefined ? { scale: field.scale } : {}),
|
|
2615
|
+
...(field.min !== undefined ? { min: field.min } : {}),
|
|
2616
|
+
...(field.max !== undefined ? { max: field.max } : {}),
|
|
2223
2617
|
...(field.file?.maxCount !== undefined
|
|
2224
2618
|
? { maxCount: field.file.maxCount }
|
|
2225
2619
|
: {}),
|
|
@@ -2267,6 +2661,7 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
2267
2661
|
code: declaration.code,
|
|
2268
2662
|
name: declaration.name,
|
|
2269
2663
|
schema: { fields },
|
|
2664
|
+
...(declaration.invariants ? { invariants: declaration.invariants } : {}),
|
|
2270
2665
|
surface,
|
|
2271
2666
|
capabilities,
|
|
2272
2667
|
...(declaration.dataPolicyCode
|
|
@@ -2292,7 +2687,10 @@ export function validateAppDeclaration(value) {
|
|
|
2292
2687
|
const resourceKeys = new Set([
|
|
2293
2688
|
'code',
|
|
2294
2689
|
'name',
|
|
2690
|
+
'mutationOwner',
|
|
2691
|
+
'generated',
|
|
2295
2692
|
'fields',
|
|
2693
|
+
'invariants',
|
|
2296
2694
|
'list',
|
|
2297
2695
|
'form',
|
|
2298
2696
|
'detail',
|
|
@@ -2310,6 +2708,8 @@ export function validateAppDeclaration(value) {
|
|
|
2310
2708
|
'maxLength',
|
|
2311
2709
|
'precision',
|
|
2312
2710
|
'scale',
|
|
2711
|
+
'min',
|
|
2712
|
+
'max',
|
|
2313
2713
|
'timePrecision',
|
|
2314
2714
|
'file',
|
|
2315
2715
|
'serial',
|
|
@@ -2332,6 +2732,26 @@ export function validateAppDeclaration(value) {
|
|
|
2332
2732
|
}
|
|
2333
2733
|
}
|
|
2334
2734
|
const fields = Array.isArray(resource.fields) ? resource.fields : [];
|
|
2735
|
+
const mutationOwner = string(resource.mutationOwner) || 'native';
|
|
2736
|
+
const generated = object(resource.generated);
|
|
2737
|
+
if (!['native', 'action', 'readonly', 'workflow'].includes(mutationOwner)) {
|
|
2738
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_MUTATION_OWNER_INVALID', 'mutationOwner 必须是 native、action、readonly 或 workflow', `${resourcePath}.mutationOwner`));
|
|
2739
|
+
}
|
|
2740
|
+
for (const key of Object.keys(generated)) {
|
|
2741
|
+
if (!['list', 'detail', 'create', 'update', 'delete'].includes(key)) {
|
|
2742
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_GENERATED_SURFACE_INVALID', `generated.${key} 不是受支持的标准页面/操作`, `${resourcePath}.generated.${key}`));
|
|
2743
|
+
}
|
|
2744
|
+
else if (typeof generated[key] !== 'boolean') {
|
|
2745
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_GENERATED_SURFACE_INVALID', `generated.${key} 必须是 boolean`, `${resourcePath}.generated.${key}`));
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
if (mutationOwner !== 'native') {
|
|
2749
|
+
for (const operation of ['create', 'update', 'delete']) {
|
|
2750
|
+
if (generated[operation] === true) {
|
|
2751
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_NATIVE_MUTATION_OWNER_CONFLICT', `${mutationOwner} owns mutations; Native ${operation} cannot be generated`, `${resourcePath}.generated.${operation}`));
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2335
2755
|
if (fields.length === 0) {
|
|
2336
2756
|
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_FIELDS_REQUIRED', '资源必须直接声明非空 fields 数组', `${resourcePath}.fields`));
|
|
2337
2757
|
return;
|
|
@@ -2414,6 +2834,21 @@ export function validateAppDeclaration(value) {
|
|
|
2414
2834
|
if (code)
|
|
2415
2835
|
declaredCodes.add(code);
|
|
2416
2836
|
});
|
|
2837
|
+
for (const operation of ['create', 'update']) {
|
|
2838
|
+
const enabled = generated[operation] ?? mutationOwner === 'native';
|
|
2839
|
+
const writable = fields.some(rawField => {
|
|
2840
|
+
const field = object(rawField);
|
|
2841
|
+
return (APP_DATA_FIELD_TYPES.has(string(field.type)) &&
|
|
2842
|
+
supportsGeneratedMutation({
|
|
2843
|
+
type: string(field.type),
|
|
2844
|
+
system: field.system === true,
|
|
2845
|
+
}) &&
|
|
2846
|
+
object(field.access)[operation] !== false);
|
|
2847
|
+
});
|
|
2848
|
+
if (materializable && enabled && !writable) {
|
|
2849
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_WRITABLE_FIELDS_REQUIRED', `generated.${operation} 需要至少一个可写业务字段`, `${resourcePath}.generated.${operation}`));
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2417
2852
|
const defaultSortField = string(object(resource.list).defaultSort && object(object(resource.list).defaultSort).field);
|
|
2418
2853
|
if (defaultSortField && !declaredCodes.has(defaultSortField)) {
|
|
2419
2854
|
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SORT_FIELD_INVALID', '默认排序字段必须在同一资源 fields 中声明', `${resourcePath}.list.defaultSort.field`));
|