openxiangda-devkit-core 2.0.0-alpha.98 → 2.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.
- package/LICENSE +21 -0
- package/README.md +3 -9
- package/dist/app-spec.d.ts +30 -12
- package/dist/app-spec.d.ts.map +1 -1
- package/dist/app-spec.js +238 -57
- package/dist/app-spec.js.map +1 -1
- package/dist/application-services.d.ts +625 -42
- package/dist/application-services.d.ts.map +1 -1
- package/dist/application-services.js +322 -85
- package/dist/application-services.js.map +1 -1
- package/dist/application-ui-contract.d.ts +5 -0
- package/dist/application-ui-contract.d.ts.map +1 -0
- package/dist/application-ui-contract.js +149 -0
- package/dist/application-ui-contract.js.map +1 -0
- package/dist/backend-image-build.d.ts +6 -0
- package/dist/backend-image-build.d.ts.map +1 -1
- package/dist/backend-image-build.js +51 -11
- package/dist/backend-image-build.js.map +1 -1
- package/dist/backend-image-upload.d.ts +34 -0
- package/dist/backend-image-upload.d.ts.map +1 -0
- package/dist/backend-image-upload.js +118 -0
- package/dist/backend-image-upload.js.map +1 -0
- package/dist/command-process.d.ts +14 -0
- package/dist/command-process.d.ts.map +1 -0
- package/dist/command-process.js +51 -0
- package/dist/command-process.js.map +1 -0
- package/dist/command-registry.d.ts +44 -4
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +10 -6
- package/dist/command-registry.js.map +1 -1
- package/dist/compiler/ai-catalog.d.ts.map +1 -1
- package/dist/compiler/ai-catalog.js +2 -1
- package/dist/compiler/ai-catalog.js.map +1 -1
- package/dist/compiler/application-model.d.ts +60 -0
- package/dist/compiler/application-model.d.ts.map +1 -0
- package/dist/compiler/application-model.js +141 -0
- package/dist/compiler/application-model.js.map +1 -0
- package/dist/compiler/bundle.d.ts.map +1 -1
- package/dist/compiler/bundle.js +196 -98
- package/dist/compiler/bundle.js.map +1 -1
- package/dist/compiler/config.d.ts +36 -10
- package/dist/compiler/config.d.ts.map +1 -1
- package/dist/compiler/config.js +536 -171
- package/dist/compiler/config.js.map +1 -1
- package/dist/compiler/field-surface.js +1 -1
- package/dist/compiler/field-surface.js.map +1 -1
- package/dist/compiler/package-compiler.d.ts.map +1 -1
- package/dist/compiler/package-compiler.js +12 -0
- package/dist/compiler/package-compiler.js.map +1 -1
- package/dist/compiler/permission-review.d.ts +139 -0
- package/dist/compiler/permission-review.d.ts.map +1 -0
- package/dist/compiler/permission-review.js +131 -0
- package/dist/compiler/permission-review.js.map +1 -0
- package/dist/configuration-preflight.d.ts +7 -0
- package/dist/configuration-preflight.d.ts.map +1 -0
- package/dist/configuration-preflight.js +13 -0
- package/dist/configuration-preflight.js.map +1 -0
- package/dist/control-plane-client.d.ts +66 -61
- package/dist/control-plane-client.d.ts.map +1 -1
- package/dist/control-plane-client.js +99 -8
- package/dist/control-plane-client.js.map +1 -1
- package/dist/delivery-cache.d.ts +34 -0
- package/dist/delivery-cache.d.ts.map +1 -0
- package/dist/delivery-cache.js +186 -0
- package/dist/delivery-cache.js.map +1 -0
- package/dist/delivery-source.d.ts +17 -0
- package/dist/delivery-source.d.ts.map +1 -0
- package/dist/delivery-source.js +85 -0
- package/dist/delivery-source.js.map +1 -0
- package/dist/deployment-observer.d.ts +14 -0
- package/dist/deployment-observer.d.ts.map +1 -0
- package/dist/deployment-observer.js +68 -0
- package/dist/deployment-observer.js.map +1 -0
- package/dist/deployment.d.ts +17 -1
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +55 -15
- package/dist/deployment.js.map +1 -1
- package/dist/design-readiness.d.ts +24 -0
- package/dist/design-readiness.d.ts.map +1 -0
- package/dist/design-readiness.js +102 -0
- package/dist/design-readiness.js.map +1 -0
- package/dist/developer-errors.d.ts +11 -0
- package/dist/developer-errors.d.ts.map +1 -0
- package/dist/developer-errors.js +16 -0
- package/dist/developer-errors.js.map +1 -0
- package/dist/developer-operations.d.ts +190 -0
- package/dist/developer-operations.d.ts.map +1 -0
- package/dist/developer-operations.js +65 -0
- package/dist/developer-operations.js.map +1 -0
- package/dist/development-lifecycle.d.ts +75 -0
- package/dist/development-lifecycle.d.ts.map +1 -0
- package/dist/development-lifecycle.js +189 -0
- package/dist/development-lifecycle.js.map +1 -0
- package/dist/documentation.d.ts +111 -0
- package/dist/documentation.d.ts.map +1 -0
- package/dist/documentation.js +112 -0
- package/dist/documentation.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/operation-progress.d.ts +26 -0
- package/dist/operation-progress.d.ts.map +1 -0
- package/dist/operation-progress.js +92 -0
- package/dist/operation-progress.js.map +1 -0
- package/dist/optional-backend.d.ts +14 -0
- package/dist/optional-backend.d.ts.map +1 -0
- package/dist/optional-backend.js +145 -0
- package/dist/optional-backend.js.map +1 -0
- package/dist/session.d.ts +22 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +36 -0
- package/dist/session.js.map +1 -1
- package/dist/workspace-loader.d.ts.map +1 -1
- package/dist/workspace-loader.js +17 -2
- package/dist/workspace-loader.js.map +1 -1
- package/dist/workspace-operation.d.ts +3 -0
- package/dist/workspace-operation.d.ts.map +1 -0
- package/dist/workspace-operation.js +45 -0
- package/dist/workspace-operation.js.map +1 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +1 -0
- package/dist/workspace.js.map +1 -1
- package/package.json +18 -4
- package/templates/backend/Dockerfile +20 -0
- package/templates/backend/package.json +27 -0
- package/templates/backend/src/app.module.ts +23 -0
- package/templates/backend/src/main.ts +5 -0
- package/templates/backend/tsconfig.json +9 -0
package/dist/compiler/config.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { materializeApplicationModules } from './application-model.js';
|
|
2
|
+
import { nativeFieldRequiresCreateInputV2 } from 'openxiangda-contracts/native-compiler';
|
|
1
3
|
import { SCHEMA_VERSIONS, DATA_FIELD_TYPES, PLATFORM_EVENT_TYPES_V2, nativePlatformCapabilityCatalog, validateDataResource, WORKFLOW_SUMMARY_MAX_FIELDS, WORKFLOW_SUMMARY_TEXT_LONG_MAX_BYTES, isWorkflowSummaryFieldType, } from 'openxiangda-contracts';
|
|
2
4
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
3
5
|
import { CronExpressionParser } from 'cron-parser';
|
|
@@ -33,20 +35,58 @@ export function defineAdminNavigation(groups) {
|
|
|
33
35
|
export function adminNavigationGroup(code, label, items, options = {}) {
|
|
34
36
|
return Object.freeze({ code, label, items: Object.freeze([...items]), ...options });
|
|
35
37
|
}
|
|
36
|
-
export function adminResourcePage(resourceCode, options) {
|
|
37
|
-
|
|
38
|
+
export function adminResourcePage(resourceCode, options = {}) {
|
|
39
|
+
const { viewCode, ...display } = options;
|
|
40
|
+
return adminNavigationItem({ kind: 'resource', resourceCode, ...(viewCode ? { viewCode } : {}) }, display);
|
|
38
41
|
}
|
|
39
42
|
export function adminOperationPage(routeCode, options) {
|
|
40
43
|
return adminNavigationItem({ kind: 'operation', routeCode }, options);
|
|
41
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* @deprecated The Todo Center is a user Surface. Existing declarations are
|
|
47
|
+
* normalized to `frontend.user.applicationTodoCenter = true` and are not
|
|
48
|
+
* retained in the admin navigation contract.
|
|
49
|
+
*/
|
|
42
50
|
export function adminApplicationTodoCenterPage(options) {
|
|
43
|
-
return
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
page: Object.freeze({ kind: 'application-todo-center' }),
|
|
53
|
+
...options,
|
|
54
|
+
});
|
|
44
55
|
}
|
|
45
|
-
|
|
46
|
-
return
|
|
56
|
+
function isLegacyApplicationTodoCenterItem(item) {
|
|
57
|
+
return (item.page.kind ===
|
|
58
|
+
'application-todo-center');
|
|
47
59
|
}
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
function normalizeLegacyUserSurfaceAuthoring(declaration) {
|
|
61
|
+
const admin = declaration.frontend.admin;
|
|
62
|
+
const navigation = admin?.navigation;
|
|
63
|
+
if (!navigation?.some(group => group.items.some(isLegacyApplicationTodoCenterItem))) {
|
|
64
|
+
return declaration;
|
|
65
|
+
}
|
|
66
|
+
const normalizedNavigation = navigation
|
|
67
|
+
.map(group => ({
|
|
68
|
+
...group,
|
|
69
|
+
items: group.items.filter(item => !isLegacyApplicationTodoCenterItem(item)),
|
|
70
|
+
}))
|
|
71
|
+
.filter(group => group.items.length > 0);
|
|
72
|
+
return {
|
|
73
|
+
...declaration,
|
|
74
|
+
frontend: {
|
|
75
|
+
...declaration.frontend,
|
|
76
|
+
user: {
|
|
77
|
+
...declaration.frontend.user,
|
|
78
|
+
applicationTodoCenter: true,
|
|
79
|
+
},
|
|
80
|
+
...(admin
|
|
81
|
+
? {
|
|
82
|
+
admin: {
|
|
83
|
+
...admin,
|
|
84
|
+
navigation: defineAdminNavigation(normalizedNavigation),
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
: {}),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
50
90
|
}
|
|
51
91
|
/**
|
|
52
92
|
* Produces a deterministic one-time authoring suggestion for tooling. The
|
|
@@ -55,10 +95,18 @@ export function adminWorkflowLaunchPage(workflowCode, options) {
|
|
|
55
95
|
export function suggestAdminNavigation(config) {
|
|
56
96
|
const groups = [];
|
|
57
97
|
const resources = [...(config.data?.resources || [])]
|
|
58
|
-
.filter(resource => resource.surface?.generated?.list !== false)
|
|
59
98
|
.sort((left, right) => left.code < right.code ? -1 : left.code > right.code ? 1 : 0);
|
|
60
|
-
|
|
61
|
-
|
|
99
|
+
const resourceItems = resources.flatMap(resource => [
|
|
100
|
+
...(resource.surface?.generated?.list !== false
|
|
101
|
+
? [adminResourcePage(resource.code, { label: resource.name })]
|
|
102
|
+
: []),
|
|
103
|
+
...[...(resource.surface?.views || [])]
|
|
104
|
+
.filter(view => view.generated.list !== false)
|
|
105
|
+
.sort((left, right) => left.code.localeCompare(right.code, 'en'))
|
|
106
|
+
.map(view => adminResourcePage(resource.code, { viewCode: view.code, label: view.name })),
|
|
107
|
+
]);
|
|
108
|
+
if (resourceItems.length > 0) {
|
|
109
|
+
groups.push(adminNavigationGroup('data-management', '数据管理', resourceItems, { icon: 'database', order: 100 }));
|
|
62
110
|
}
|
|
63
111
|
const routes = [...(config.frontend.routes || [])]
|
|
64
112
|
.filter(route => route.surface === 'admin' && !/[:*]/.test(route.path))
|
|
@@ -66,34 +114,13 @@ export function suggestAdminNavigation(config) {
|
|
|
66
114
|
if (routes.length > 0) {
|
|
67
115
|
groups.push(adminNavigationGroup('business-operations', '业务操作', routes.map(route => adminOperationPage(route.code, { label: route.label })), { icon: 'operations', order: 200 }));
|
|
68
116
|
}
|
|
69
|
-
const workflowCodes = [
|
|
70
|
-
...new Set((config.workflows?.definitions || []).map(declaration => declaration.definition.code)),
|
|
71
|
-
].sort();
|
|
72
|
-
const workflowItems = [];
|
|
73
|
-
if (workflowCodes.length > 0) {
|
|
74
|
-
workflowItems.push(adminWorkflowWorkCenterPage({ label: '我的审批' }));
|
|
75
|
-
}
|
|
76
|
-
for (const code of workflowCodes) {
|
|
77
|
-
const declarations = (config.workflows?.definitions || []).filter(declaration => declaration.definition.code === code);
|
|
78
|
-
const activeVersion = config.workflows?.activations.find(activation => activation.workflowCode === code)?.definitionVersion;
|
|
79
|
-
const selected = declarations.find(declaration => declaration.version === activeVersion) ||
|
|
80
|
-
[...declarations].sort((left, right) => right.version - left.version)[0];
|
|
81
|
-
if (selected?.launch?.mode === 'standalone') {
|
|
82
|
-
workflowItems.push(adminWorkflowLaunchPage(code, {
|
|
83
|
-
label: `发起${selected.definition.title}`,
|
|
84
|
-
}));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (workflowItems.length > 0) {
|
|
88
|
-
groups.push(adminNavigationGroup('workflow-center', '流程中心', workflowItems, {
|
|
89
|
-
icon: 'workflow',
|
|
90
|
-
order: 300,
|
|
91
|
-
}));
|
|
92
|
-
}
|
|
93
117
|
return defineAdminNavigation(groups);
|
|
94
118
|
}
|
|
95
119
|
function renderAdminNavigationOptions(options) {
|
|
96
120
|
const entries = [
|
|
121
|
+
...(options.viewCode !== undefined
|
|
122
|
+
? [`viewCode: ${JSON.stringify(options.viewCode)}`]
|
|
123
|
+
: []),
|
|
97
124
|
...(options.label !== undefined
|
|
98
125
|
? [`label: ${JSON.stringify(options.label)}`]
|
|
99
126
|
: []),
|
|
@@ -114,7 +141,9 @@ export function renderAdminNavigationSuggestion(config) {
|
|
|
114
141
|
const groups = navigation.map(group => {
|
|
115
142
|
imports.add('adminNavigationGroup');
|
|
116
143
|
const items = group.items.map(item => {
|
|
117
|
-
const options = renderAdminNavigationOptions(item
|
|
144
|
+
const options = renderAdminNavigationOptions({ ...item,
|
|
145
|
+
...(item.page.kind === 'resource' && item.page.viewCode ? { viewCode: item.page.viewCode } : {}),
|
|
146
|
+
});
|
|
118
147
|
let expression;
|
|
119
148
|
switch (item.page.kind) {
|
|
120
149
|
case 'resource':
|
|
@@ -125,18 +154,6 @@ export function renderAdminNavigationSuggestion(config) {
|
|
|
125
154
|
imports.add('adminOperationPage');
|
|
126
155
|
expression = `adminOperationPage(${JSON.stringify(item.page.routeCode)}${options ? `, ${options}` : ''})`;
|
|
127
156
|
break;
|
|
128
|
-
case 'application-todo-center':
|
|
129
|
-
imports.add('adminApplicationTodoCenterPage');
|
|
130
|
-
expression = `adminApplicationTodoCenterPage(${options || ''})`;
|
|
131
|
-
break;
|
|
132
|
-
case 'workflow-work-center':
|
|
133
|
-
imports.add('adminWorkflowWorkCenterPage');
|
|
134
|
-
expression = `adminWorkflowWorkCenterPage(${options || ''})`;
|
|
135
|
-
break;
|
|
136
|
-
case 'workflow-launch':
|
|
137
|
-
imports.add('adminWorkflowLaunchPage');
|
|
138
|
-
expression = `adminWorkflowLaunchPage(${JSON.stringify(item.page.workflowCode)}${options ? `, ${options}` : ''})`;
|
|
139
|
-
break;
|
|
140
157
|
}
|
|
141
158
|
return ` ${expression},`;
|
|
142
159
|
});
|
|
@@ -604,7 +621,6 @@ export function backendRuntimeRequired(config) {
|
|
|
604
621
|
config.events?.subscriptions?.length ||
|
|
605
622
|
config.events?.timers?.length ||
|
|
606
623
|
config.events?.dateTriggers?.length ||
|
|
607
|
-
config.workflows?.activations?.length ||
|
|
608
624
|
config.workflows?.providers?.length)));
|
|
609
625
|
}
|
|
610
626
|
function object(value) {
|
|
@@ -834,7 +850,10 @@ export function validateAppConfig(value) {
|
|
|
834
850
|
validateApplicationAuthentication(config, diagnostics);
|
|
835
851
|
validateFrontendDevicePolicy(config, diagnostics);
|
|
836
852
|
validateFrontendRoutes(config, diagnostics);
|
|
853
|
+
validateResourceDetailRoutes(config, diagnostics);
|
|
854
|
+
validateFrontendUser(config, diagnostics);
|
|
837
855
|
validateAnonymousPublicAccess(config, diagnostics);
|
|
856
|
+
validateAdminAccess(config, diagnostics);
|
|
838
857
|
validateAdminNavigation(config, diagnostics);
|
|
839
858
|
validatePerspectives(config.perspectives, object(config.authz).roles, diagnostics);
|
|
840
859
|
const declaredResources = new Map((Array.isArray(object(config.data).resources)
|
|
@@ -855,7 +874,7 @@ export function validateAppConfig(value) {
|
|
|
855
874
|
const declaredWorkflowCodes = new Set((Array.isArray(object(config.workflows).activations)
|
|
856
875
|
? object(config.workflows).activations
|
|
857
876
|
: []).map(item => string(object(item).workflowCode)));
|
|
858
|
-
validateBackendOperations(backend.operations, declaredResources, declaredWorkflowCodes, diagnostics);
|
|
877
|
+
validateBackendOperations(backend.operations, declaredResources, declaredWorkflowCodes, new Set((Array.isArray(object(config.authz).roles) ? object(config.authz).roles : []).map(role => string(object(role).code))), diagnostics);
|
|
859
878
|
const eventConfig = object(config.events);
|
|
860
879
|
const workflowConfig = object(config.workflows);
|
|
861
880
|
if (backend.enabled === false &&
|
|
@@ -866,10 +885,8 @@ export function validateAppConfig(value) {
|
|
|
866
885
|
Array.isArray(eventConfig.timers) && eventConfig.timers.length > 0 ||
|
|
867
886
|
Array.isArray(eventConfig.dateTriggers) &&
|
|
868
887
|
eventConfig.dateTriggers.length > 0 ||
|
|
869
|
-
Array.isArray(workflowConfig.activations) &&
|
|
870
|
-
workflowConfig.activations.length > 0 ||
|
|
871
888
|
Array.isArray(workflowConfig.providers) && workflowConfig.providers.length > 0)) {
|
|
872
|
-
diagnostics.push(diagnostic('APP_CONFIG_BACKEND_DISABLED_WITH_DECLARATIONS', 'backend.enabled=false 时不能声明后端操作、Secret
|
|
889
|
+
diagnostics.push(diagnostic('APP_CONFIG_BACKEND_DISABLED_WITH_DECLARATIONS', 'backend.enabled=false 时不能声明后端操作、Secret、事件消费者或工作流人员提供器', 'backend.enabled'));
|
|
873
890
|
}
|
|
874
891
|
const secretNames = new Set();
|
|
875
892
|
const secretEnvs = new Set();
|
|
@@ -1041,6 +1058,22 @@ export function validateAppConfig(value) {
|
|
|
1041
1058
|
}
|
|
1042
1059
|
seenCapabilities.add(capability);
|
|
1043
1060
|
});
|
|
1061
|
+
const deniedCapabilities = Array.isArray(role.deniedCapabilities)
|
|
1062
|
+
? role.deniedCapabilities
|
|
1063
|
+
: [];
|
|
1064
|
+
if (role.deniedCapabilities !== undefined &&
|
|
1065
|
+
!Array.isArray(role.deniedCapabilities)) {
|
|
1066
|
+
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLE_DENIED_CAPABILITIES_INVALID', 'deniedCapabilities 必须是 capability 数组', `authz.roles[${index}].deniedCapabilities`));
|
|
1067
|
+
}
|
|
1068
|
+
const seenDeniedCapabilities = new Set();
|
|
1069
|
+
deniedCapabilities.forEach((rawCapability, capabilityIndex) => {
|
|
1070
|
+
const capability = string(rawCapability);
|
|
1071
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9:._*-]{0,254}$/.test(capability) ||
|
|
1072
|
+
seenDeniedCapabilities.has(capability)) {
|
|
1073
|
+
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLE_DENIED_CAPABILITY_INVALID', 'denied capability 必须是非空稳定标识,且同一角色内不能重复', `authz.roles[${index}].deniedCapabilities[${capabilityIndex}]`));
|
|
1074
|
+
}
|
|
1075
|
+
seenDeniedCapabilities.add(capability);
|
|
1076
|
+
});
|
|
1044
1077
|
});
|
|
1045
1078
|
const authenticatedUserRoleCode = authz.authenticatedUserRoleCode === undefined
|
|
1046
1079
|
? ''
|
|
@@ -1972,6 +2005,108 @@ export function validateAppConfig(value) {
|
|
|
1972
2005
|
}
|
|
1973
2006
|
editableParameterCodes.add(code);
|
|
1974
2007
|
});
|
|
2008
|
+
const backendOperationsByCode = new Map((Array.isArray(backend.operations) ? backend.operations : []).map(raw => {
|
|
2009
|
+
const operation = object(raw);
|
|
2010
|
+
return [string(operation.code), operation];
|
|
2011
|
+
}));
|
|
2012
|
+
const dataResourcesByCode = new Map((Array.isArray(data.resources) ? data.resources : []).map(raw => {
|
|
2013
|
+
const resource = object(raw);
|
|
2014
|
+
return [string(resource.code), resource];
|
|
2015
|
+
}));
|
|
2016
|
+
const validateNamedOperationIntent = (rawIntent, intentKind, definition, path) => {
|
|
2017
|
+
const intent = object(rawIntent);
|
|
2018
|
+
const operationCode = string(intent.operationCode);
|
|
2019
|
+
const operation = backendOperationsByCode.get(operationCode);
|
|
2020
|
+
const inputs = object(intent.inputs);
|
|
2021
|
+
const output = object(intent.output);
|
|
2022
|
+
const subjectResource = dataResourcesByCode.get(definition.subject?.resourceCode);
|
|
2023
|
+
const subjectSchema = object(subjectResource?.schema);
|
|
2024
|
+
const subjectFields = new Set((Array.isArray(subjectSchema.fields) ? subjectSchema.fields : []).map(field => string(object(field).code)));
|
|
2025
|
+
const requestSchema = object(operation?.requestSchema);
|
|
2026
|
+
const requestProperties = object(requestSchema.properties);
|
|
2027
|
+
const requestRequired = new Set(Array.isArray(requestSchema.required)
|
|
2028
|
+
? requestSchema.required.map(string)
|
|
2029
|
+
: []);
|
|
2030
|
+
const responseProperties = object(object(operation?.responseSchema).properties);
|
|
2031
|
+
const operationWorkflow = object(object(operation?.platformAccess).workflow);
|
|
2032
|
+
const operationWorkflowCodes = Array.isArray(operationWorkflow.codes)
|
|
2033
|
+
? operationWorkflow.codes.map(string)
|
|
2034
|
+
: [];
|
|
2035
|
+
if (Object.keys(intent).some(key => !['operationCode', 'inputs', 'output'].includes(key)) ||
|
|
2036
|
+
!OPERATION_CODE_PATTERN.test(operationCode) ||
|
|
2037
|
+
!operation ||
|
|
2038
|
+
string(operation.method) !== 'POST' ||
|
|
2039
|
+
!operationWorkflowCodes.includes(definition.code)) {
|
|
2040
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_INVALID', '标准 Workflow named-operation 必须引用同一应用中声明目标 Workflow platformAccess 的 POST operation', path));
|
|
2041
|
+
}
|
|
2042
|
+
const inputEntries = Object.entries(inputs);
|
|
2043
|
+
const propertyCodePattern = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
|
|
2044
|
+
const fieldCodes = new Set();
|
|
2045
|
+
const sourceCounts = new Map();
|
|
2046
|
+
let inputsInvalid = !isRecord(intent.inputs) ||
|
|
2047
|
+
inputEntries.length === 0 ||
|
|
2048
|
+
inputEntries.length > 64;
|
|
2049
|
+
for (const [inputCode, rawBinding] of inputEntries) {
|
|
2050
|
+
const binding = object(rawBinding);
|
|
2051
|
+
const source = string(binding.source);
|
|
2052
|
+
const allowedKeys = source === 'field' ? ['source', 'fieldCode'] : ['source'];
|
|
2053
|
+
inputsInvalid ||=
|
|
2054
|
+
!propertyCodePattern.test(inputCode) ||
|
|
2055
|
+
!(inputCode in requestProperties) ||
|
|
2056
|
+
![
|
|
2057
|
+
'field',
|
|
2058
|
+
'idempotency-key',
|
|
2059
|
+
'current-user-reference',
|
|
2060
|
+
'subject-id',
|
|
2061
|
+
'subject-revision',
|
|
2062
|
+
'requested-at',
|
|
2063
|
+
].includes(source) ||
|
|
2064
|
+
Object.keys(binding).some(key => !allowedKeys.includes(key));
|
|
2065
|
+
sourceCounts.set(source, (sourceCounts.get(source) || 0) + 1);
|
|
2066
|
+
if (source === 'field') {
|
|
2067
|
+
const fieldCode = string(binding.fieldCode);
|
|
2068
|
+
inputsInvalid ||=
|
|
2069
|
+
!propertyCodePattern.test(fieldCode) ||
|
|
2070
|
+
!subjectFields.has(fieldCode) ||
|
|
2071
|
+
fieldCodes.has(fieldCode);
|
|
2072
|
+
fieldCodes.add(fieldCode);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
inputsInvalid ||=
|
|
2076
|
+
[...requestRequired].some(inputCode => !(inputCode in inputs)) ||
|
|
2077
|
+
(sourceCounts.get('idempotency-key') || 0) !== 1 ||
|
|
2078
|
+
![...inputEntries].some(([inputCode, rawBinding]) => string(object(rawBinding).source) === 'idempotency-key' &&
|
|
2079
|
+
requestRequired.has(inputCode));
|
|
2080
|
+
if (intentKind === 'create') {
|
|
2081
|
+
inputsInvalid ||=
|
|
2082
|
+
(sourceCounts.get('subject-id') || 0) > 0 ||
|
|
2083
|
+
(sourceCounts.get('subject-revision') || 0) > 0;
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
inputsInvalid ||=
|
|
2087
|
+
(sourceCounts.get('subject-id') || 0) !== 1 ||
|
|
2088
|
+
(sourceCounts.get('subject-revision') || 0) !== 1;
|
|
2089
|
+
}
|
|
2090
|
+
if (inputsInvalid) {
|
|
2091
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_INPUT_INVALID', 'named-operation 必须以唯一且完整的顶层 input binding 覆盖请求必填字段、平台幂等键及对应 create/existing 身份来源', `${path}.inputs`));
|
|
2092
|
+
}
|
|
2093
|
+
const subjectIdOutput = string(output.subjectId);
|
|
2094
|
+
const subjectRevisionOutput = string(output.subjectRevision);
|
|
2095
|
+
const processCommandOutput = string(output.processCommand);
|
|
2096
|
+
if (!isRecord(intent.output) ||
|
|
2097
|
+
Object.keys(output).some(key => !['subjectId', 'subjectRevision', 'processCommand'].includes(key)) ||
|
|
2098
|
+
!propertyCodePattern.test(subjectIdOutput) ||
|
|
2099
|
+
!(subjectIdOutput in responseProperties) ||
|
|
2100
|
+
(output.subjectRevision !== undefined &&
|
|
2101
|
+
(!propertyCodePattern.test(subjectRevisionOutput) ||
|
|
2102
|
+
!(subjectRevisionOutput in responseProperties))) ||
|
|
2103
|
+
(output.processCommand !== undefined &&
|
|
2104
|
+
(!propertyCodePattern.test(processCommandOutput) ||
|
|
2105
|
+
!(processCommandOutput in responseProperties)))) {
|
|
2106
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_OUTPUT_INVALID', 'named-operation output 必须引用 responseSchema 中的顶层 subjectId、可选 revision 和可选 processCommand 属性', `${path}.output`));
|
|
2107
|
+
}
|
|
2108
|
+
return fieldCodes;
|
|
2109
|
+
};
|
|
1975
2110
|
definitions.forEach((item, index) => {
|
|
1976
2111
|
const declaration = object(item);
|
|
1977
2112
|
const definition = object(declaration.definition);
|
|
@@ -1982,7 +2117,7 @@ export function validateAppConfig(value) {
|
|
|
1982
2117
|
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DEFINITION_DECLARATION_INVALID', 'Workflow definition declaration 只能声明 version、definition、launch 和 detailRouteCode', path));
|
|
1983
2118
|
}
|
|
1984
2119
|
if (declaration.launch !== undefined &&
|
|
1985
|
-
(Object.keys(launch).some(key =>
|
|
2120
|
+
(Object.keys(launch).some(key => !['mode', 'submission'].includes(key)) ||
|
|
1986
2121
|
![
|
|
1987
2122
|
'standalone',
|
|
1988
2123
|
'custom-page',
|
|
@@ -1991,6 +2126,41 @@ export function validateAppConfig(value) {
|
|
|
1991
2126
|
].includes(string(launch.mode)))) {
|
|
1992
2127
|
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_LAUNCH_MODE_INVALID', 'Workflow launch.mode 必须是 standalone、custom-page、hidden-handoff 或 work-center-only', `${path}.launch.mode`));
|
|
1993
2128
|
}
|
|
2129
|
+
if (launch.submission !== undefined) {
|
|
2130
|
+
const submission = object(launch.submission);
|
|
2131
|
+
const createFields = submission.create
|
|
2132
|
+
? validateNamedOperationIntent(submission.create, 'create', definition, `${path}.launch.submission.create`)
|
|
2133
|
+
: new Set();
|
|
2134
|
+
const existingFields = submission.existing
|
|
2135
|
+
? validateNamedOperationIntent(submission.existing, 'existing', definition, `${path}.launch.submission.existing`)
|
|
2136
|
+
: new Set();
|
|
2137
|
+
const contexts = Array.isArray(submission.context)
|
|
2138
|
+
? submission.context
|
|
2139
|
+
: [];
|
|
2140
|
+
const contextKeys = new Set();
|
|
2141
|
+
let contextInvalid = contexts.length > 16;
|
|
2142
|
+
for (const rawContext of contexts) {
|
|
2143
|
+
const context = object(rawContext);
|
|
2144
|
+
const queryParameter = string(context.queryParameter);
|
|
2145
|
+
const fieldCode = string(context.fieldCode);
|
|
2146
|
+
contextInvalid ||=
|
|
2147
|
+
Object.keys(context).some(key => !['queryParameter', 'fieldCode'].includes(key)) ||
|
|
2148
|
+
!/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(queryParameter) ||
|
|
2149
|
+
contextKeys.has(queryParameter) ||
|
|
2150
|
+
(!createFields.has(fieldCode) && !existingFields.has(fieldCode));
|
|
2151
|
+
contextKeys.add(queryParameter);
|
|
2152
|
+
}
|
|
2153
|
+
if (!isRecord(launch.submission) ||
|
|
2154
|
+
submission.kind !== 'named-operation' ||
|
|
2155
|
+
Object.keys(submission).some(key => !['kind', 'create', 'existing', 'context'].includes(key)) ||
|
|
2156
|
+
(!['standalone', 'hidden-handoff'].includes(string(launch.mode))) ||
|
|
2157
|
+
(!submission.create && !submission.existing)) {
|
|
2158
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_SUBMISSION_INVALID', 'named-operation submission 只适用于 standalone/hidden-handoff,且必须声明 create 或 existing intent', `${path}.launch.submission`));
|
|
2159
|
+
}
|
|
2160
|
+
if (contextInvalid) {
|
|
2161
|
+
diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_LAUNCH_CONTEXT_INVALID', 'Workflow launch context 必须唯一、受限并指向已绑定的 subject form field', `${path}.launch.submission.context`));
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
1994
2164
|
if (declaration.detailRouteCode !== undefined) {
|
|
1995
2165
|
const detailRouteCode = object(declaration.detailRouteCode);
|
|
1996
2166
|
const desktopCode = string(detailRouteCode.desktop);
|
|
@@ -2255,7 +2425,7 @@ function validateCapabilityDeclarations(appCode, declarations, diagnostics) {
|
|
|
2255
2425
|
seen.add(code);
|
|
2256
2426
|
});
|
|
2257
2427
|
}
|
|
2258
|
-
function validateBackendOperations(rawOperations, declaredResources, declaredWorkflowCodes, diagnostics) {
|
|
2428
|
+
function validateBackendOperations(rawOperations, declaredResources, declaredWorkflowCodes, declaredRoleCodes, diagnostics) {
|
|
2259
2429
|
const declaredResourceCodes = new Set(declaredResources.keys());
|
|
2260
2430
|
if (rawOperations !== undefined && !Array.isArray(rawOperations)) {
|
|
2261
2431
|
diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATIONS_INVALID', 'backend.operations 必须是数组', 'backend.operations'));
|
|
@@ -2292,10 +2462,18 @@ function validateBackendOperations(rawOperations, declaredResources, declaredWor
|
|
|
2292
2462
|
'managedFileCopies',
|
|
2293
2463
|
'notification',
|
|
2294
2464
|
'workflow',
|
|
2465
|
+
'roleAssertions',
|
|
2295
2466
|
]);
|
|
2296
2467
|
let invalid = !isRecord(operation.platformAccess) ||
|
|
2297
2468
|
Object.keys(access).length === 0 ||
|
|
2298
2469
|
Object.keys(access).some(key => !allowedAccessKeys.has(key));
|
|
2470
|
+
if (access.roleAssertions !== undefined) {
|
|
2471
|
+
const assertion = object(access.roleAssertions);
|
|
2472
|
+
const roles = Array.isArray(assertion.roleCodes) ? assertion.roleCodes : [];
|
|
2473
|
+
invalid ||= !isRecord(access.roleAssertions) || roles.length < 1 || roles.length > 20 ||
|
|
2474
|
+
new Set(roles).size !== roles.length || roles.some(role => !declaredRoleCodes.has(string(role))) ||
|
|
2475
|
+
Object.keys(assertion).some(key => key !== 'roleCodes');
|
|
2476
|
+
}
|
|
2299
2477
|
if (access.directory !== undefined) {
|
|
2300
2478
|
const directory = object(access.directory);
|
|
2301
2479
|
const fields = Array.isArray(directory.fields) ? directory.fields : [];
|
|
@@ -2408,7 +2586,7 @@ function validateBackendOperations(rawOperations, declaredResources, declaredWor
|
|
|
2408
2586
|
Object.keys(workflow).some(key => key !== 'codes');
|
|
2409
2587
|
}
|
|
2410
2588
|
if (invalid) {
|
|
2411
|
-
diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATION_PLATFORM_ACCESS_INVALID', 'operation.platformAccess
|
|
2589
|
+
diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATION_PLATFORM_ACCESS_INVALID', 'operation.platformAccess 必须只声明有界的发起人目录、托管文件、托管文件复制、标准通知、现有工作流或已声明角色条件依赖', `${path}.platformAccess`));
|
|
2412
2590
|
}
|
|
2413
2591
|
}
|
|
2414
2592
|
if (operation.ai !== undefined) {
|
|
@@ -2480,64 +2658,66 @@ function generatedPlatformRouteClaims(config) {
|
|
|
2480
2658
|
const code = string(resource.code);
|
|
2481
2659
|
if (!code)
|
|
2482
2660
|
continue;
|
|
2483
|
-
const
|
|
2484
|
-
const
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
[
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2661
|
+
const resourceSurface = object(resource.surface);
|
|
2662
|
+
for (const selected of [resourceSurface, ...(Array.isArray(resourceSurface.views) ? resourceSurface.views.map(object) : [])]) {
|
|
2663
|
+
const surface = { ...resourceSurface, ...selected };
|
|
2664
|
+
const viewCode = string(selected.code);
|
|
2665
|
+
const generated = object(surface.generated);
|
|
2666
|
+
const native = !surface.mutationOwner || surface.mutationOwner === 'native';
|
|
2667
|
+
const operations = {
|
|
2668
|
+
list: generated.list === undefined ? true : generated.list === true,
|
|
2669
|
+
detail: generated.detail === undefined ? true : generated.detail === true,
|
|
2670
|
+
create: generated.create === undefined ? native : generated.create === true,
|
|
2671
|
+
update: generated.update === undefined ? native : generated.update === true,
|
|
2672
|
+
};
|
|
2673
|
+
const base = `/admin/resources/${code}${viewCode ? `/views/${viewCode}` : ""}`;
|
|
2674
|
+
const candidates = [
|
|
2675
|
+
['list', base],
|
|
2676
|
+
['detail', `${base}/:id`],
|
|
2677
|
+
['create', `${base}/new`],
|
|
2678
|
+
['update', `${base}/:id/edit`],
|
|
2679
|
+
];
|
|
2680
|
+
for (const [operation, path] of candidates) {
|
|
2681
|
+
if (!operations[operation])
|
|
2682
|
+
continue;
|
|
2683
|
+
const owner = `resource:${code}${viewCode ? `:view:${viewCode}` : ''}:${operation}`;
|
|
2684
|
+
claims.push({ owner, path });
|
|
2685
|
+
if (object(surface.mobile).enabled !== false)
|
|
2686
|
+
claims.push({ owner: `${owner}:mobile`, path: `/m${path}` });
|
|
2687
|
+
}
|
|
2503
2688
|
}
|
|
2504
2689
|
}
|
|
2505
2690
|
const frontend = object(config.frontend);
|
|
2506
|
-
const
|
|
2507
|
-
const
|
|
2508
|
-
const hasTodoCenter = navigation.some(rawGroup => {
|
|
2509
|
-
const group = object(rawGroup);
|
|
2510
|
-
const items = Array.isArray(group.items) ? group.items : [];
|
|
2511
|
-
return items.some(rawItem => string(object(object(rawItem).page).kind) === 'application-todo-center');
|
|
2512
|
-
});
|
|
2691
|
+
const user = object(frontend.user);
|
|
2692
|
+
const hasTodoCenter = user.applicationTodoCenter === true;
|
|
2513
2693
|
if (hasTodoCenter) {
|
|
2514
|
-
claims.push({ owner: 'application:todo-center', path: '/
|
|
2694
|
+
claims.push({ owner: 'application:todo-center', path: '/todos' }, { owner: 'application:todo-center:mobile', path: '/m/todos' });
|
|
2515
2695
|
}
|
|
2516
2696
|
const workflows = object(config.workflows);
|
|
2517
2697
|
const definitions = Array.isArray(workflows.definitions)
|
|
2518
2698
|
? workflows.definitions
|
|
2519
2699
|
: [];
|
|
2520
2700
|
if (definitions.length > 0) {
|
|
2521
|
-
claims.push({ owner: 'workflow:work-center', path: '/
|
|
2701
|
+
claims.push({ owner: 'workflow:work-center', path: '/work-center' }, { owner: 'workflow:work-center:mobile', path: '/m/work-center' }, { owner: 'workflow:task', path: '/tasks/:taskId' }, { owner: 'workflow:task:mobile', path: '/m/tasks/:taskId' }, { owner: 'workflow:instance', path: '/workflows/:instanceId' }, {
|
|
2522
2702
|
owner: 'workflow:instance:mobile',
|
|
2523
2703
|
path: '/m/workflows/:instanceId',
|
|
2524
2704
|
});
|
|
2525
|
-
const
|
|
2705
|
+
const launchCodes = new Set();
|
|
2526
2706
|
for (const rawDefinition of definitions) {
|
|
2527
2707
|
const declaration = object(rawDefinition);
|
|
2528
|
-
if (string(object(declaration.launch).mode)
|
|
2708
|
+
if (!['standalone', 'hidden-handoff'].includes(string(object(declaration.launch).mode)))
|
|
2529
2709
|
continue;
|
|
2530
2710
|
const workflowCode = string(object(declaration.definition).code);
|
|
2531
2711
|
if (workflowCode)
|
|
2532
|
-
|
|
2712
|
+
launchCodes.add(workflowCode);
|
|
2533
2713
|
}
|
|
2534
|
-
for (const workflowCode of
|
|
2714
|
+
for (const workflowCode of launchCodes) {
|
|
2535
2715
|
claims.push({
|
|
2536
2716
|
owner: `workflow:${workflowCode}:launch`,
|
|
2537
|
-
path:
|
|
2717
|
+
path: '/workflows/:workflowCode/start',
|
|
2538
2718
|
});
|
|
2539
2719
|
}
|
|
2540
|
-
if (
|
|
2720
|
+
if (launchCodes.size > 0) {
|
|
2541
2721
|
claims.push({
|
|
2542
2722
|
owner: 'workflow:launch:mobile',
|
|
2543
2723
|
path: '/m/workflows/:workflowCode/start',
|
|
@@ -2566,6 +2746,68 @@ function validateFrontendDevicePolicy(config, diagnostics) {
|
|
|
2566
2746
|
diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_DEVICE_POLICY_INVALID', 'frontend.devicePolicy 必须是 viewport-family,移动最大宽度和桌面最小宽度为 320-1600 的相邻整数', 'frontend.devicePolicy'));
|
|
2567
2747
|
}
|
|
2568
2748
|
}
|
|
2749
|
+
function validateResourceDetailRoutes(config, diagnostics) {
|
|
2750
|
+
const frontend = object(config.frontend);
|
|
2751
|
+
const routesByCode = new Map((Array.isArray(frontend.routes) ? frontend.routes : []).map(rawRoute => {
|
|
2752
|
+
const route = object(rawRoute);
|
|
2753
|
+
return [string(route.code), route];
|
|
2754
|
+
}));
|
|
2755
|
+
const data = object(config.data);
|
|
2756
|
+
const resources = Array.isArray(data.resources) ? data.resources : [];
|
|
2757
|
+
resources.forEach((rawResource, resourceIndex) => {
|
|
2758
|
+
const resource = object(rawResource);
|
|
2759
|
+
if (resource.detailRouteCode === undefined)
|
|
2760
|
+
return;
|
|
2761
|
+
const path = `data.resources[${resourceIndex}].detailRouteCode`;
|
|
2762
|
+
const detailRouteCode = object(resource.detailRouteCode);
|
|
2763
|
+
const desktopCode = string(detailRouteCode.desktop);
|
|
2764
|
+
const mobileCode = string(detailRouteCode.mobile);
|
|
2765
|
+
if (Object.keys(detailRouteCode).some(key => !['desktop', 'mobile'].includes(key)) ||
|
|
2766
|
+
!OPERATION_CODE_PATTERN.test(desktopCode) ||
|
|
2767
|
+
!OPERATION_CODE_PATTERN.test(mobileCode) ||
|
|
2768
|
+
desktopCode === mobileCode) {
|
|
2769
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_INVALID', 'detailRouteCode 必须声明两个不同且合法的 desktop/mobile route code', path));
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
const readCapability = string(object(resource.capabilities).read);
|
|
2773
|
+
for (const [device, code] of [
|
|
2774
|
+
['desktop', desktopCode],
|
|
2775
|
+
['mobile', mobileCode],
|
|
2776
|
+
]) {
|
|
2777
|
+
const route = routesByCode.get(code);
|
|
2778
|
+
const routePointer = `${path}.${device}`;
|
|
2779
|
+
if (!route) {
|
|
2780
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_REFERENCE_MISSING', `detailRouteCode.${device} 必须引用已声明 frontend route`, routePointer));
|
|
2781
|
+
continue;
|
|
2782
|
+
}
|
|
2783
|
+
if (string(route.surface) !== 'user') {
|
|
2784
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_SURFACE_INVALID', `detailRouteCode.${device} 必须引用 user surface`, routePointer));
|
|
2785
|
+
}
|
|
2786
|
+
const routePath = string(route.path);
|
|
2787
|
+
const parameters = [
|
|
2788
|
+
...routePath.matchAll(/:([A-Za-z][A-Za-z0-9_]*)/g),
|
|
2789
|
+
].map(match => match[1]);
|
|
2790
|
+
const wrongDeviceFamily = device === 'mobile'
|
|
2791
|
+
? !routePath.startsWith('/m/')
|
|
2792
|
+
: routePath === '/m' || routePath.startsWith('/m/');
|
|
2793
|
+
if (routePath.includes('*') ||
|
|
2794
|
+
routePath.includes('?') ||
|
|
2795
|
+
routePath.includes('#') ||
|
|
2796
|
+
routePath.split('/').includes('..') ||
|
|
2797
|
+
parameters.length !== 1 ||
|
|
2798
|
+
wrongDeviceFamily) {
|
|
2799
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_PATH_INVALID', `detailRouteCode.${device} 路由必须属于对应端且只能声明一个有界动态记录参数`, routePointer));
|
|
2800
|
+
}
|
|
2801
|
+
const access = object(route.access);
|
|
2802
|
+
const allOf = Array.isArray(access.allOf) ? access.allOf.map(string) : [];
|
|
2803
|
+
if (!readCapability ||
|
|
2804
|
+
(string(route.capability) !== readCapability &&
|
|
2805
|
+
!allOf.includes(readCapability))) {
|
|
2806
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_CAPABILITY_INVALID', `detailRouteCode.${device} 路由必须要求当前资源的 read capability`, routePointer));
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2569
2811
|
function validateFrontendRoutes(config, diagnostics) {
|
|
2570
2812
|
const frontend = object(config.frontend);
|
|
2571
2813
|
const rawRoutes = frontend.routes;
|
|
@@ -2642,9 +2884,14 @@ function validateFrontendRoutes(config, diagnostics) {
|
|
|
2642
2884
|
!['admin', 'user'].includes(string(route.surface)) ||
|
|
2643
2885
|
(route.surface === 'admin' &&
|
|
2644
2886
|
routePath !== '/admin' &&
|
|
2645
|
-
!routePath.startsWith('/admin/')
|
|
2887
|
+
!routePath.startsWith('/admin/') &&
|
|
2888
|
+
routePath !== '/m/admin' &&
|
|
2889
|
+
!routePath.startsWith('/m/admin/')) ||
|
|
2646
2890
|
(route.surface === 'user' &&
|
|
2647
|
-
(routePath === '/admin' ||
|
|
2891
|
+
(routePath === '/admin' ||
|
|
2892
|
+
routePath.startsWith('/admin/') ||
|
|
2893
|
+
routePath === '/m/admin' ||
|
|
2894
|
+
routePath.startsWith('/m/admin/'))) ||
|
|
2648
2895
|
(route.capability !== undefined &&
|
|
2649
2896
|
!CAPABILITY_PATTERN.test(string(route.capability))) ||
|
|
2650
2897
|
accessInvalid ||
|
|
@@ -2727,16 +2974,17 @@ function validateAnonymousPublicAccess(config, diagnostics) {
|
|
|
2727
2974
|
const declaration = object(field);
|
|
2728
2975
|
return [string(declaration.code), string(declaration.type)];
|
|
2729
2976
|
}));
|
|
2977
|
+
const surface = object(resource.surface);
|
|
2978
|
+
const surfaceFields = object(surface.fields);
|
|
2730
2979
|
const requiredCreateFields = new Set(fields
|
|
2731
2980
|
.map(object)
|
|
2732
2981
|
.filter(field => {
|
|
2733
2982
|
const type = string(field.type);
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2983
|
+
const fieldSurface = object(surfaceFields[string(field.code)]);
|
|
2984
|
+
return (APP_DATA_FIELD_TYPES.has(type) &&
|
|
2985
|
+
nativeFieldRequiresCreateInputV2({ type, ...(field.nullable === false ? { nullable: false } : {}) }, { system: fieldSurface.system === true, requiredHint: fieldSurface.requiredHint === true }));
|
|
2737
2986
|
})
|
|
2738
2987
|
.map(field => string(field.code)));
|
|
2739
|
-
const surface = object(resource.surface);
|
|
2740
2988
|
const generated = object(surface.generated);
|
|
2741
2989
|
const nativeCreate = (string(surface.mutationOwner) || 'native') === 'native' &&
|
|
2742
2990
|
generated.create !== false;
|
|
@@ -2803,9 +3051,7 @@ function validateAnonymousPublicAccess(config, diagnostics) {
|
|
|
2803
3051
|
fields.some(field => !resourceFields?.has(field)) ||
|
|
2804
3052
|
new Set(requiredFields).size !== requiredFields.length ||
|
|
2805
3053
|
requiredFields.some(field => !fields.includes(field)) ||
|
|
2806
|
-
(operations.includes('create') &&
|
|
2807
|
-
(!resource?.nativeCreate ||
|
|
2808
|
-
[...(resource?.requiredCreateFields || [])].some(field => !fields.includes(field) || !requiredFields.includes(field)))) ||
|
|
3054
|
+
(operations.includes('create') && !resource?.nativeCreate) ||
|
|
2809
3055
|
new Set(ownRecordFields).size !== ownRecordFields.length ||
|
|
2810
3056
|
ownRecordFields.some(field => !fields.includes(field)) ||
|
|
2811
3057
|
(operations.includes('draft.read') !== operations.includes('draft.update')) ||
|
|
@@ -2826,6 +3072,13 @@ function validateAnonymousPublicAccess(config, diagnostics) {
|
|
|
2826
3072
|
if (invalid) {
|
|
2827
3073
|
diagnostics.push(diagnostic('APP_CONFIG_ANONYMOUS_PUBLIC_POLICY_INVALID', '匿名公开策略必须绑定唯一静态 user route、已声明资源/字段和有界操作', path));
|
|
2828
3074
|
}
|
|
3075
|
+
if (operations.includes('create') && resource?.nativeCreate) {
|
|
3076
|
+
const missingFields = [...resource.requiredCreateFields].filter(field => !fields.includes(field));
|
|
3077
|
+
const missingRequired = [...resource.requiredCreateFields].filter(field => !requiredFields.includes(field));
|
|
3078
|
+
if (missingFields.length || missingRequired.length) {
|
|
3079
|
+
diagnostics.push(diagnostic('APP_CONFIG_ANONYMOUS_CREATE_FIELDS_INCOMPLETE', `公开创建必须覆盖资源 ${resourceCode} 的必填字段:${[...new Set([...missingFields, ...missingRequired])].join('、')}`, `${path}.${missingFields.length ? 'fields' : 'requiredFields'}`));
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
2829
3082
|
const validationCodes = new Set();
|
|
2830
3083
|
validations.forEach((rawValidation, validationIndex) => {
|
|
2831
3084
|
const validation = object(rawValidation);
|
|
@@ -2984,14 +3237,50 @@ function validAdminOrder(value) {
|
|
|
2984
3237
|
return (value === undefined ||
|
|
2985
3238
|
(Number.isSafeInteger(value) && Math.abs(Number(value)) <= 10_000));
|
|
2986
3239
|
}
|
|
3240
|
+
function validateFrontendUser(config, diagnostics) {
|
|
3241
|
+
const frontend = object(config.frontend);
|
|
3242
|
+
if (frontend.user === undefined)
|
|
3243
|
+
return;
|
|
3244
|
+
const user = object(frontend.user);
|
|
3245
|
+
if (!frontend.user ||
|
|
3246
|
+
typeof frontend.user !== 'object' ||
|
|
3247
|
+
Array.isArray(frontend.user) ||
|
|
3248
|
+
Object.keys(user).some(key => key !== 'applicationTodoCenter') ||
|
|
3249
|
+
(user.applicationTodoCenter !== undefined &&
|
|
3250
|
+
typeof user.applicationTodoCenter !== 'boolean')) {
|
|
3251
|
+
diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_USER_INVALID', 'frontend.user 只接受 applicationTodoCenter 布尔值', 'frontend.user'));
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
function validateAdminAccess(config, diagnostics) {
|
|
3255
|
+
const accessValue = config.frontend.admin?.access;
|
|
3256
|
+
if (accessValue === undefined)
|
|
3257
|
+
return;
|
|
3258
|
+
const access = object(accessValue);
|
|
3259
|
+
const allOf = Array.isArray(access.allOf) ? access.allOf : [];
|
|
3260
|
+
const anyOf = Array.isArray(access.anyOf) ? access.anyOf : [];
|
|
3261
|
+
const invalid = !accessValue ||
|
|
3262
|
+
typeof accessValue !== 'object' ||
|
|
3263
|
+
Array.isArray(accessValue) ||
|
|
3264
|
+
Object.keys(access).some(key => !['allOf', 'anyOf'].includes(key)) ||
|
|
3265
|
+
(allOf.length === 0 && anyOf.length === 0) ||
|
|
3266
|
+
allOf.length > 50 ||
|
|
3267
|
+
anyOf.length > 50 ||
|
|
3268
|
+
allOf.some(value => !CAPABILITY_PATTERN.test(string(value))) ||
|
|
3269
|
+
anyOf.some(value => !CAPABILITY_PATTERN.test(string(value))) ||
|
|
3270
|
+
new Set(allOf.map(string)).size !== allOf.length ||
|
|
3271
|
+
new Set(anyOf.map(string)).size !== anyOf.length;
|
|
3272
|
+
if (invalid) {
|
|
3273
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_ACCESS_INVALID', 'frontend.admin.access 必须包含非空、无重复且有效的 allOf 或 anyOf capability 数组', 'frontend.admin.access'));
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
2987
3276
|
function validateAdminNavigation(config, diagnostics) {
|
|
2988
3277
|
const frontend = object(config.frontend);
|
|
2989
3278
|
const admin = object(frontend.admin);
|
|
2990
3279
|
const rawNavigation = admin.navigation;
|
|
2991
3280
|
if (frontend.admin !== undefined &&
|
|
2992
|
-
(Object.keys(admin).some(key =>
|
|
3281
|
+
(Object.keys(admin).some(key => !['access', 'navigation'].includes(key)) ||
|
|
2993
3282
|
!Array.isArray(rawNavigation))) {
|
|
2994
|
-
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_NAVIGATION_INVALID', 'frontend.admin 只接受 navigation 数组', 'frontend.admin.navigation'));
|
|
3283
|
+
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_NAVIGATION_INVALID', 'frontend.admin 只接受 access 和 navigation 数组', 'frontend.admin.navigation'));
|
|
2995
3284
|
return;
|
|
2996
3285
|
}
|
|
2997
3286
|
const groups = Array.isArray(rawNavigation) ? rawNavigation : [];
|
|
@@ -3000,24 +3289,6 @@ function validateAdminNavigation(config, diagnostics) {
|
|
|
3000
3289
|
}
|
|
3001
3290
|
const resources = new Map((config.data?.resources || []).map(resource => [resource.code, resource]));
|
|
3002
3291
|
const routes = new Map((config.frontend.routes || []).map(route => [route.code, route]));
|
|
3003
|
-
const workflows = new Map();
|
|
3004
|
-
const workflowDefinitionsByCode = new Map();
|
|
3005
|
-
for (const declaration of config.workflows?.definitions || []) {
|
|
3006
|
-
const declarations = workflowDefinitionsByCode.get(declaration.definition.code) || [];
|
|
3007
|
-
declarations.push(declaration);
|
|
3008
|
-
workflowDefinitionsByCode.set(declaration.definition.code, declarations);
|
|
3009
|
-
}
|
|
3010
|
-
for (const [code, declarations] of workflowDefinitionsByCode) {
|
|
3011
|
-
const activeVersion = config.workflows?.activations.find(activation => activation.workflowCode === code)?.definitionVersion;
|
|
3012
|
-
const selected = declarations.find(declaration => declaration.version === activeVersion) ||
|
|
3013
|
-
[...declarations].sort((left, right) => right.version - left.version)[0];
|
|
3014
|
-
if (selected) {
|
|
3015
|
-
workflows.set(code, {
|
|
3016
|
-
title: selected.definition.title,
|
|
3017
|
-
mode: selected.launch?.mode || 'work-center-only',
|
|
3018
|
-
});
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
3292
|
const groupCodes = new Set();
|
|
3022
3293
|
const pageKeys = new Set();
|
|
3023
3294
|
let itemCount = 0;
|
|
@@ -3051,14 +3322,15 @@ function validateAdminNavigation(config, diagnostics) {
|
|
|
3051
3322
|
let defaultLabel = '';
|
|
3052
3323
|
let internalCode = '';
|
|
3053
3324
|
let exists = false;
|
|
3054
|
-
let standaloneViolation = false;
|
|
3055
3325
|
if (kind === 'resource') {
|
|
3056
3326
|
const resourceCode = string(page.resourceCode);
|
|
3057
3327
|
const resource = resources.get(resourceCode);
|
|
3058
|
-
|
|
3059
|
-
|
|
3328
|
+
const viewCode = string(page.viewCode);
|
|
3329
|
+
const view = resource?.surface?.views?.find((item) => item.code === viewCode);
|
|
3330
|
+
pageKey = `resource:${resourceCode}${viewCode ? `:view:${viewCode}` : ""}:list`;
|
|
3331
|
+
defaultLabel = (viewCode ? view?.name : resource?.name) || '';
|
|
3060
3332
|
internalCode = resourceCode;
|
|
3061
|
-
exists = Boolean(resource?.surface?.generated?.list);
|
|
3333
|
+
exists = Boolean(viewCode ? view?.generated?.list : resource?.surface?.generated?.list);
|
|
3062
3334
|
}
|
|
3063
3335
|
else if (kind === 'operation') {
|
|
3064
3336
|
const routeCode = string(page.routeCode);
|
|
@@ -3066,46 +3338,22 @@ function validateAdminNavigation(config, diagnostics) {
|
|
|
3066
3338
|
pageKey = `operation:${routeCode}`;
|
|
3067
3339
|
defaultLabel = route?.label || '';
|
|
3068
3340
|
internalCode = routeCode;
|
|
3069
|
-
exists = Boolean(route &&
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
defaultLabel = '待办中心';
|
|
3074
|
-
internalCode = pageKey;
|
|
3075
|
-
exists = true;
|
|
3076
|
-
}
|
|
3077
|
-
else if (kind === 'workflow-work-center') {
|
|
3078
|
-
pageKey = 'workflow:work-center';
|
|
3079
|
-
defaultLabel = '我的审批';
|
|
3080
|
-
internalCode = pageKey;
|
|
3081
|
-
exists = workflows.size > 0;
|
|
3082
|
-
}
|
|
3083
|
-
else if (kind === 'workflow-launch') {
|
|
3084
|
-
const workflowCode = string(page.workflowCode);
|
|
3085
|
-
const workflow = workflows.get(workflowCode);
|
|
3086
|
-
pageKey = `workflow:${workflowCode}:launch`;
|
|
3087
|
-
defaultLabel = workflow ? `发起${workflow.title}` : '';
|
|
3088
|
-
internalCode = workflowCode;
|
|
3089
|
-
standaloneViolation = Boolean(workflow && workflow.mode !== 'standalone');
|
|
3090
|
-
exists = Boolean(workflow && workflow.mode === 'standalone');
|
|
3341
|
+
exists = Boolean(route &&
|
|
3342
|
+
route.surface === 'admin' &&
|
|
3343
|
+
!route.path.startsWith('/m/admin') &&
|
|
3344
|
+
!/[:*]/.test(route.path));
|
|
3091
3345
|
}
|
|
3092
3346
|
if (Object.keys(item).some(key => !['page', 'label', 'icon', 'order'].includes(key)) ||
|
|
3093
3347
|
![
|
|
3094
3348
|
'resource',
|
|
3095
3349
|
'operation',
|
|
3096
|
-
'application-todo-center',
|
|
3097
|
-
'workflow-work-center',
|
|
3098
|
-
'workflow-launch',
|
|
3099
3350
|
].includes(kind) ||
|
|
3100
3351
|
(item.icon !== undefined &&
|
|
3101
3352
|
!ADMIN_NAVIGATION_ICONS.has(string(item.icon))) ||
|
|
3102
3353
|
!validAdminOrder(item.order)) {
|
|
3103
3354
|
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_ITEM_INVALID', '导航项必须使用受支持的页面 helper、label、icon 和 order', itemPath));
|
|
3104
3355
|
}
|
|
3105
|
-
if (
|
|
3106
|
-
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_WORKFLOW_NOT_STANDALONE', '只有 self-contained standalone Workflow launch page 可以进入菜单', `${itemPath}.page`));
|
|
3107
|
-
}
|
|
3108
|
-
else if (!exists) {
|
|
3356
|
+
if (!exists) {
|
|
3109
3357
|
diagnostics.push(diagnostic('APP_CONFIG_ADMIN_PAGE_NOT_FOUND', '导航引用必须指向存在且可进入菜单的 page registry 页面', `${itemPath}.page`));
|
|
3110
3358
|
}
|
|
3111
3359
|
if (pageKey && pageKeys.has(pageKey)) {
|
|
@@ -3199,6 +3447,12 @@ function validateCapabilityClosure(appCode, config, declarations, diagnostics) {
|
|
|
3199
3447
|
capability: string(value),
|
|
3200
3448
|
path: `authz.roles[${index}].capabilities[${capabilityIndex}]`,
|
|
3201
3449
|
}));
|
|
3450
|
+
(Array.isArray(role.deniedCapabilities)
|
|
3451
|
+
? role.deniedCapabilities
|
|
3452
|
+
: []).forEach((value, capabilityIndex) => references.push({
|
|
3453
|
+
capability: string(value),
|
|
3454
|
+
path: `authz.roles[${index}].deniedCapabilities[${capabilityIndex}]`,
|
|
3455
|
+
}));
|
|
3202
3456
|
});
|
|
3203
3457
|
const backend = object(config.backend);
|
|
3204
3458
|
(Array.isArray(backend.operations) ? backend.operations : []).forEach((rawOperation, index) => references.push({
|
|
@@ -3224,6 +3478,14 @@ function validateCapabilityClosure(appCode, config, declarations, diagnostics) {
|
|
|
3224
3478
|
}));
|
|
3225
3479
|
}
|
|
3226
3480
|
});
|
|
3481
|
+
const adminAccess = object(object(frontend.admin).access);
|
|
3482
|
+
for (const key of ['allOf', 'anyOf']) {
|
|
3483
|
+
const values = Array.isArray(adminAccess[key]) ? adminAccess[key] : [];
|
|
3484
|
+
values.forEach((value, capabilityIndex) => references.push({
|
|
3485
|
+
capability: string(value),
|
|
3486
|
+
path: `frontend.admin.access.${key}[${capabilityIndex}]`,
|
|
3487
|
+
}));
|
|
3488
|
+
}
|
|
3227
3489
|
for (const reference of references) {
|
|
3228
3490
|
if (!catalog.has(reference.capability) ||
|
|
3229
3491
|
(!reference.capability.startsWith(`app:${appCode}:`) &&
|
|
@@ -3274,6 +3536,21 @@ export function resourceCapabilityCodes(appCode, resourceCode) {
|
|
|
3274
3536
|
delete: `${prefix}:delete`,
|
|
3275
3537
|
};
|
|
3276
3538
|
}
|
|
3539
|
+
function materializeRoleCapabilities(role) {
|
|
3540
|
+
const capabilities = new Set(role.capabilities);
|
|
3541
|
+
for (const denied of role.deniedCapabilities || []) {
|
|
3542
|
+
capabilities.delete(denied);
|
|
3543
|
+
}
|
|
3544
|
+
return [...capabilities].sort();
|
|
3545
|
+
}
|
|
3546
|
+
/** Explicit business permission preset; adding a page never changes grants. */
|
|
3547
|
+
export function resourceRoleCapabilities(appCode, resourceCode, access) {
|
|
3548
|
+
const codes = resourceCapabilityCodes(appCode, resourceCode);
|
|
3549
|
+
const operations = access === 'manage'
|
|
3550
|
+
? ['read', 'create', 'update', 'delete']
|
|
3551
|
+
: access === 'read' ? ['read'] : access;
|
|
3552
|
+
return [...new Set(operations.map(operation => codes[operation]))].sort();
|
|
3553
|
+
}
|
|
3277
3554
|
export function currentUserDataPolicy(input) {
|
|
3278
3555
|
return {
|
|
3279
3556
|
code: input.code,
|
|
@@ -3365,9 +3642,12 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
3365
3642
|
physicalPlanForField(definition, field.searchable === true);
|
|
3366
3643
|
return definition;
|
|
3367
3644
|
});
|
|
3368
|
-
const
|
|
3369
|
-
.filter(field => field.list !== false)
|
|
3370
|
-
|
|
3645
|
+
const listCandidates = declaration.fields
|
|
3646
|
+
.filter(field => field.list !== false && (field.hidden ?? field.system) !== true);
|
|
3647
|
+
const visible = declaration.list?.fields
|
|
3648
|
+
? declaration.list.fields.flatMap(code => listCandidates.filter(field => field.code === code))
|
|
3649
|
+
: listCandidates.slice(0, 8);
|
|
3650
|
+
const defaultSortField = visible.find(field => supportsSort(field.type));
|
|
3371
3651
|
const searchableFields = declaration.fields
|
|
3372
3652
|
.filter(field => field.searchable === true)
|
|
3373
3653
|
.map(field => field.code);
|
|
@@ -3390,6 +3670,7 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
3390
3670
|
return [capabilities[operation]];
|
|
3391
3671
|
};
|
|
3392
3672
|
const surface = {
|
|
3673
|
+
...(declaration.views?.length ? { views: declaration.views } : {}),
|
|
3393
3674
|
mutationOwner,
|
|
3394
3675
|
generated,
|
|
3395
3676
|
fields: Object.fromEntries(declaration.fields.map(field => [
|
|
@@ -3401,6 +3682,9 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
3401
3682
|
...(field.section ? { section: field.section } : {}),
|
|
3402
3683
|
...(field.required === true ? { requiredHint: true } : {}),
|
|
3403
3684
|
...(field.system !== undefined ? { system: field.system } : {}),
|
|
3685
|
+
...(field.hidden !== undefined || field.system
|
|
3686
|
+
? { hidden: field.hidden ?? true }
|
|
3687
|
+
: {}),
|
|
3404
3688
|
readCapabilities: access(field, 'read'),
|
|
3405
3689
|
createCapabilities: access(field, 'create'),
|
|
3406
3690
|
updateCapabilities: access(field, 'update'),
|
|
@@ -3434,22 +3718,23 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
3434
3718
|
},
|
|
3435
3719
|
])),
|
|
3436
3720
|
list: {
|
|
3721
|
+
fieldOrder: visible.map(field => field.code),
|
|
3437
3722
|
defaultPageSize: declaration.list?.defaultPageSize || 20,
|
|
3438
3723
|
...(searchableFields.length ? { searchableFields } : {}),
|
|
3439
3724
|
...(filterFields.length ? { filterFields } : {}),
|
|
3440
3725
|
...(declaration.list?.defaultSort
|
|
3441
3726
|
? { defaultSort: declaration.list.defaultSort }
|
|
3442
|
-
:
|
|
3443
|
-
? { defaultSort: { field:
|
|
3727
|
+
: defaultSortField
|
|
3728
|
+
? { defaultSort: { field: defaultSortField.code, order: 'asc' } }
|
|
3444
3729
|
: {}),
|
|
3445
3730
|
},
|
|
3446
3731
|
form: {
|
|
3447
3732
|
layout: declaration.form?.layout || 'flat',
|
|
3448
|
-
fieldOrder: declaration.fields.map(field => field.code),
|
|
3733
|
+
fieldOrder: declaration.form?.fields || declaration.fields.map(field => field.code),
|
|
3449
3734
|
},
|
|
3450
3735
|
detail: {
|
|
3451
3736
|
layout: declaration.detail?.layout || 'flat',
|
|
3452
|
-
fieldOrder: declaration.fields.map(field => field.code),
|
|
3737
|
+
fieldOrder: declaration.detail?.fields || declaration.fields.map(field => field.code),
|
|
3453
3738
|
},
|
|
3454
3739
|
mobile: declaration.mobile || { enabled: true },
|
|
3455
3740
|
};
|
|
@@ -3465,6 +3750,9 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
3465
3750
|
...(declaration.dataPolicyCode
|
|
3466
3751
|
? { dataPolicyCode: declaration.dataPolicyCode }
|
|
3467
3752
|
: {}),
|
|
3753
|
+
...(declaration.detailRouteCode
|
|
3754
|
+
? { detailRouteCode: { ...declaration.detailRouteCode } }
|
|
3755
|
+
: {}),
|
|
3468
3756
|
fieldPolicies: Object.fromEntries(declaration.fields.map(field => [
|
|
3469
3757
|
field.code,
|
|
3470
3758
|
{
|
|
@@ -3483,6 +3771,7 @@ export function validateAppDeclaration(value) {
|
|
|
3483
3771
|
const resources = Array.isArray(data.resources) ? data.resources : [];
|
|
3484
3772
|
const diagnostics = [];
|
|
3485
3773
|
const resourceKeys = new Set([
|
|
3774
|
+
'views',
|
|
3486
3775
|
'code',
|
|
3487
3776
|
'name',
|
|
3488
3777
|
'mutationOwner',
|
|
@@ -3494,6 +3783,7 @@ export function validateAppDeclaration(value) {
|
|
|
3494
3783
|
'detail',
|
|
3495
3784
|
'mobile',
|
|
3496
3785
|
'dataPolicyCode',
|
|
3786
|
+
'detailRouteCode',
|
|
3497
3787
|
]);
|
|
3498
3788
|
const fieldKeys = new Set([
|
|
3499
3789
|
'code',
|
|
@@ -3516,6 +3806,7 @@ export function validateAppDeclaration(value) {
|
|
|
3516
3806
|
'widget',
|
|
3517
3807
|
'section',
|
|
3518
3808
|
'system',
|
|
3809
|
+
'hidden',
|
|
3519
3810
|
'list',
|
|
3520
3811
|
'filter',
|
|
3521
3812
|
'searchable',
|
|
@@ -3527,7 +3818,7 @@ export function validateAppDeclaration(value) {
|
|
|
3527
3818
|
const resourcePath = `data.resources[${resourceIndex}]`;
|
|
3528
3819
|
for (const key of Object.keys(resource)) {
|
|
3529
3820
|
if (!resourceKeys.has(key)) {
|
|
3530
|
-
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_PROPERTY_UNSUPPORTED', `${resourcePath}.${key} 不属于单次资源声明合同`, `${resourcePath}.${key}`, '只声明 code、name、fields
|
|
3821
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_PROPERTY_UNSUPPORTED', `${resourcePath}.${key} 不属于单次资源声明合同`, `${resourcePath}.${key}`, '只声明 code、name、fields 及可选列表/布局/数据策略/详情路由'));
|
|
3531
3822
|
}
|
|
3532
3823
|
}
|
|
3533
3824
|
const fields = Array.isArray(resource.fields) ? resource.fields : [];
|
|
@@ -3595,12 +3886,6 @@ export function validateAppDeclaration(value) {
|
|
|
3595
3886
|
else if (!rangeType && field.rangeBoundary !== undefined) {
|
|
3596
3887
|
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_RANGE_BOUNDARY_TYPE_INVALID', 'rangeBoundary 只能用于 date-range 或 datetime-range', `${fieldPath}.rangeBoundary`));
|
|
3597
3888
|
}
|
|
3598
|
-
if (field.indexed === false &&
|
|
3599
|
-
(field.filter === true ||
|
|
3600
|
-
field.searchable === true ||
|
|
3601
|
-
field.sortable === true)) {
|
|
3602
|
-
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_INDEX_REQUIRED', '启用筛选、搜索或排序时不能显式关闭索引', `${fieldPath}.indexed`));
|
|
3603
|
-
}
|
|
3604
3889
|
if (['radio', 'checkbox'].includes(widget) &&
|
|
3605
3890
|
object(field.source).loadMode !== 'all' &&
|
|
3606
3891
|
field.source !== undefined) {
|
|
@@ -3614,6 +3899,7 @@ export function validateAppDeclaration(value) {
|
|
|
3614
3899
|
'required',
|
|
3615
3900
|
'indexed',
|
|
3616
3901
|
'system',
|
|
3902
|
+
'hidden',
|
|
3617
3903
|
'list',
|
|
3618
3904
|
'filter',
|
|
3619
3905
|
'searchable',
|
|
@@ -3642,6 +3928,27 @@ export function validateAppDeclaration(value) {
|
|
|
3642
3928
|
if (code)
|
|
3643
3929
|
declaredCodes.add(code);
|
|
3644
3930
|
});
|
|
3931
|
+
for (const view of ['list', 'form', 'detail']) {
|
|
3932
|
+
const selected = object(resource[view]).fields;
|
|
3933
|
+
if (selected === undefined)
|
|
3934
|
+
continue;
|
|
3935
|
+
if (!Array.isArray(selected) || new Set(selected).size !== selected.length ||
|
|
3936
|
+
selected.some(code => typeof code !== 'string' || !declaredCodes.has(code))) {
|
|
3937
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_VIEW_FIELDS_INVALID', '列表/表单/详情 fields 必须是当前资源中不重复的字段列表', `${resourcePath}.${view}.fields`));
|
|
3938
|
+
materializable = false;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
const selectedFormFields = object(resource.form).fields ?? fields.map(raw => object(raw).code);
|
|
3942
|
+
if (Array.isArray(selectedFormFields) && (generated.create ?? mutationOwner === 'native')) {
|
|
3943
|
+
for (const rawField of fields) {
|
|
3944
|
+
const field = object(rawField);
|
|
3945
|
+
if (field.required === true && field.system !== true &&
|
|
3946
|
+
supportsGeneratedMutation({ type: field.type, system: false }) &&
|
|
3947
|
+
object(field.access).create !== false && (field.hidden === true || !selectedFormFields.includes(field.code))) {
|
|
3948
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_FORM_REQUIRED_FIELD_MISSING', `标准新增表单缺少必填字段 ${field.label || field.code};请加入表单或关闭标准新增`, `${resourcePath}.form.fields`));
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3645
3952
|
for (const operation of ['create', 'update']) {
|
|
3646
3953
|
const enabled = generated[operation] ?? mutationOwner === 'native';
|
|
3647
3954
|
const writable = fields.some(rawField => {
|
|
@@ -3663,6 +3970,19 @@ export function validateAppDeclaration(value) {
|
|
|
3663
3970
|
}
|
|
3664
3971
|
if (appCode && fields.length && materializable) {
|
|
3665
3972
|
const canonical = materializeDataResource(appCode, rawResource);
|
|
3973
|
+
for (const [viewIndex, view] of (canonical.surface?.views || []).entries()) {
|
|
3974
|
+
for (const [property, values, supports] of [
|
|
3975
|
+
['searchableFields', view.list?.searchableFields || [], supportsSearch],
|
|
3976
|
+
['filterFields', view.list?.filterFields || [], supportsFilter],
|
|
3977
|
+
['defaultSort', view.list?.defaultSort ? [view.list.defaultSort.field] : [], supportsSort],
|
|
3978
|
+
]) {
|
|
3979
|
+
for (const code of values) {
|
|
3980
|
+
const field = canonical.schema.fields.find(field => field.code === code);
|
|
3981
|
+
if (!field || !supports(field.type))
|
|
3982
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_VIEW_QUERY_FIELD_INVALID', '视图查询字段不存在或不支持此查询类型', `${resourcePath}.views[${viewIndex}].list.${property}`));
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3666
3986
|
diagnostics.push(...validateDataResource(canonical).map(item => ({
|
|
3667
3987
|
...item,
|
|
3668
3988
|
path: `${resourcePath}.${item.path}`,
|
|
@@ -3730,24 +4050,69 @@ export function validateAppDeclaration(value) {
|
|
|
3730
4050
|
return diagnostics;
|
|
3731
4051
|
}
|
|
3732
4052
|
export function defineOpenXiangdaApp(declaration) {
|
|
3733
|
-
const
|
|
4053
|
+
const { modules, ...input } = declaration;
|
|
4054
|
+
const projected = materializeApplicationModules(modules || []);
|
|
4055
|
+
if (projected.diagnostics.length)
|
|
4056
|
+
throw new AppConfigValidationError(projected.diagnostics);
|
|
4057
|
+
const normalizedDeclaration = normalizeLegacyUserSurfaceAuthoring({
|
|
4058
|
+
...input,
|
|
4059
|
+
schemaVersion: declaration.schemaVersion ?? 3,
|
|
4060
|
+
frontend: { root: 'apps/web', ...declaration.frontend },
|
|
4061
|
+
backend: {
|
|
4062
|
+
root: 'apps/server', runtime: 'node', framework: 'nestjs',
|
|
4063
|
+
enabled: Boolean(declaration.backend?.operations?.length || declaration.backend?.secrets?.length ||
|
|
4064
|
+
declaration.events?.subscriptions?.length || declaration.events?.timers?.length ||
|
|
4065
|
+
declaration.events?.dateTriggers?.length ||
|
|
4066
|
+
declaration.workflows?.providers?.length),
|
|
4067
|
+
...declaration.backend,
|
|
4068
|
+
},
|
|
4069
|
+
platform: { root: 'platform', ...declaration.platform },
|
|
4070
|
+
...(modules || declaration.data ? {
|
|
4071
|
+
data: { resources: [...(declaration.data?.resources || []), ...projected.resources] },
|
|
4072
|
+
} : {}),
|
|
4073
|
+
});
|
|
4074
|
+
const declarationDiagnostics = validateAppDeclaration(normalizedDeclaration);
|
|
3734
4075
|
if (declarationDiagnostics.length > 0) {
|
|
3735
4076
|
throw new AppConfigValidationError(declarationDiagnostics);
|
|
3736
4077
|
}
|
|
3737
|
-
const { data, ...application } =
|
|
3738
|
-
const
|
|
4078
|
+
const { data, ...application } = normalizedDeclaration;
|
|
4079
|
+
const materialized = {
|
|
3739
4080
|
...application,
|
|
3740
4081
|
...(data
|
|
3741
4082
|
? {
|
|
3742
4083
|
data: {
|
|
3743
|
-
resources: data.resources.map(resource => materializeDataResource(
|
|
4084
|
+
resources: data.resources.map(resource => materializeDataResource(normalizedDeclaration.app.code, resource)),
|
|
3744
4085
|
},
|
|
3745
4086
|
}
|
|
3746
4087
|
: {}),
|
|
3747
4088
|
};
|
|
4089
|
+
const config = materialized.authz
|
|
4090
|
+
? {
|
|
4091
|
+
...materialized,
|
|
4092
|
+
authz: {
|
|
4093
|
+
...materialized.authz,
|
|
4094
|
+
roles: materialized.authz.roles.map(role => ({
|
|
4095
|
+
...role,
|
|
4096
|
+
capabilities: materializeRoleCapabilities(role),
|
|
4097
|
+
})),
|
|
4098
|
+
},
|
|
4099
|
+
}
|
|
4100
|
+
: materialized;
|
|
3748
4101
|
const diagnostics = validateAppConfig(config);
|
|
3749
4102
|
if (diagnostics.length > 0)
|
|
3750
4103
|
throw new AppConfigValidationError(diagnostics);
|
|
3751
|
-
|
|
4104
|
+
const sealed = config.authz
|
|
4105
|
+
? {
|
|
4106
|
+
...config,
|
|
4107
|
+
authz: {
|
|
4108
|
+
...config.authz,
|
|
4109
|
+
roles: config.authz.roles.map(role => {
|
|
4110
|
+
const { deniedCapabilities: _deniedCapabilities, ...materializedRole } = role;
|
|
4111
|
+
return materializedRole;
|
|
4112
|
+
}),
|
|
4113
|
+
},
|
|
4114
|
+
}
|
|
4115
|
+
: config;
|
|
4116
|
+
return Object.freeze(sealed);
|
|
3752
4117
|
}
|
|
3753
4118
|
//# sourceMappingURL=config.js.map
|