openxiangda-devkit-core 2.0.0-alpha.106 → 2.0.0-alpha.108
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 +181 -1
- package/dist/application-services.d.ts.map +1 -1
- package/dist/application-services.js +33 -0
- 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/command-registry.d.ts +2 -2
- package/dist/command-registry.js +1 -1
- package/dist/command-registry.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 +50 -12
- package/dist/compiler/bundle.js.map +1 -1
- package/dist/compiler/config.d.ts +27 -8
- package/dist/compiler/config.d.ts.map +1 -1
- package/dist/compiler/config.js +205 -61
- 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/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/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- 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/workspace-loader.d.ts.map +1 -1
- package/dist/workspace-loader.js +16 -1
- package/dist/workspace-loader.js.map +1 -1
- package/package.json +3 -2
- package/templates/backend/.turbo/turbo-build.log +4 -0
- package/templates/backend/.turbo/turbo-check.log +4 -0
- package/templates/backend/.turbo/turbo-test.log +13 -0
- package/templates/backend/Dockerfile +20 -0
- package/templates/backend/node_modules/.bin/openxiangda +21 -0
- package/templates/backend/node_modules/.bin/tsc +21 -0
- package/templates/backend/node_modules/.bin/tsserver +21 -0
- package/templates/backend/node_modules/.bin/tsx +21 -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/test/smoke.test.ts +17 -0
- package/templates/backend/tsconfig.json +9 -0
package/dist/compiler/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { materializeApplicationModules } from './application-model.js';
|
|
1
2
|
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
3
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
3
4
|
import { CronExpressionParser } from 'cron-parser';
|
|
@@ -33,8 +34,9 @@ export function defineAdminNavigation(groups) {
|
|
|
33
34
|
export function adminNavigationGroup(code, label, items, options = {}) {
|
|
34
35
|
return Object.freeze({ code, label, items: Object.freeze([...items]), ...options });
|
|
35
36
|
}
|
|
36
|
-
export function adminResourcePage(resourceCode, options) {
|
|
37
|
-
|
|
37
|
+
export function adminResourcePage(resourceCode, options = {}) {
|
|
38
|
+
const { viewCode, ...display } = options;
|
|
39
|
+
return adminNavigationItem({ kind: 'resource', resourceCode, ...(viewCode ? { viewCode } : {}) }, display);
|
|
38
40
|
}
|
|
39
41
|
export function adminOperationPage(routeCode, options) {
|
|
40
42
|
return adminNavigationItem({ kind: 'operation', routeCode }, options);
|
|
@@ -92,10 +94,18 @@ function normalizeLegacyUserSurfaceAuthoring(declaration) {
|
|
|
92
94
|
export function suggestAdminNavigation(config) {
|
|
93
95
|
const groups = [];
|
|
94
96
|
const resources = [...(config.data?.resources || [])]
|
|
95
|
-
.filter(resource => resource.surface?.generated?.list !== false)
|
|
96
97
|
.sort((left, right) => left.code < right.code ? -1 : left.code > right.code ? 1 : 0);
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
const resourceItems = resources.flatMap(resource => [
|
|
99
|
+
...(resource.surface?.generated?.list !== false
|
|
100
|
+
? [adminResourcePage(resource.code, { label: resource.name })]
|
|
101
|
+
: []),
|
|
102
|
+
...[...(resource.surface?.views || [])]
|
|
103
|
+
.filter(view => view.generated.list !== false)
|
|
104
|
+
.sort((left, right) => left.code.localeCompare(right.code, 'en'))
|
|
105
|
+
.map(view => adminResourcePage(resource.code, { viewCode: view.code, label: view.name })),
|
|
106
|
+
]);
|
|
107
|
+
if (resourceItems.length > 0) {
|
|
108
|
+
groups.push(adminNavigationGroup('data-management', '数据管理', resourceItems, { icon: 'database', order: 100 }));
|
|
99
109
|
}
|
|
100
110
|
const routes = [...(config.frontend.routes || [])]
|
|
101
111
|
.filter(route => route.surface === 'admin' && !/[:*]/.test(route.path))
|
|
@@ -107,6 +117,9 @@ export function suggestAdminNavigation(config) {
|
|
|
107
117
|
}
|
|
108
118
|
function renderAdminNavigationOptions(options) {
|
|
109
119
|
const entries = [
|
|
120
|
+
...(options.viewCode !== undefined
|
|
121
|
+
? [`viewCode: ${JSON.stringify(options.viewCode)}`]
|
|
122
|
+
: []),
|
|
110
123
|
...(options.label !== undefined
|
|
111
124
|
? [`label: ${JSON.stringify(options.label)}`]
|
|
112
125
|
: []),
|
|
@@ -127,7 +140,9 @@ export function renderAdminNavigationSuggestion(config) {
|
|
|
127
140
|
const groups = navigation.map(group => {
|
|
128
141
|
imports.add('adminNavigationGroup');
|
|
129
142
|
const items = group.items.map(item => {
|
|
130
|
-
const options = renderAdminNavigationOptions(item
|
|
143
|
+
const options = renderAdminNavigationOptions({ ...item,
|
|
144
|
+
...(item.page.kind === 'resource' && item.page.viewCode ? { viewCode: item.page.viewCode } : {}),
|
|
145
|
+
});
|
|
131
146
|
let expression;
|
|
132
147
|
switch (item.page.kind) {
|
|
133
148
|
case 'resource':
|
|
@@ -605,7 +620,6 @@ export function backendRuntimeRequired(config) {
|
|
|
605
620
|
config.events?.subscriptions?.length ||
|
|
606
621
|
config.events?.timers?.length ||
|
|
607
622
|
config.events?.dateTriggers?.length ||
|
|
608
|
-
config.workflows?.activations?.length ||
|
|
609
623
|
config.workflows?.providers?.length)));
|
|
610
624
|
}
|
|
611
625
|
function object(value) {
|
|
@@ -835,6 +849,7 @@ export function validateAppConfig(value) {
|
|
|
835
849
|
validateApplicationAuthentication(config, diagnostics);
|
|
836
850
|
validateFrontendDevicePolicy(config, diagnostics);
|
|
837
851
|
validateFrontendRoutes(config, diagnostics);
|
|
852
|
+
validateResourceDetailRoutes(config, diagnostics);
|
|
838
853
|
validateFrontendUser(config, diagnostics);
|
|
839
854
|
validateAnonymousPublicAccess(config, diagnostics);
|
|
840
855
|
validateAdminAccess(config, diagnostics);
|
|
@@ -869,10 +884,8 @@ export function validateAppConfig(value) {
|
|
|
869
884
|
Array.isArray(eventConfig.timers) && eventConfig.timers.length > 0 ||
|
|
870
885
|
Array.isArray(eventConfig.dateTriggers) &&
|
|
871
886
|
eventConfig.dateTriggers.length > 0 ||
|
|
872
|
-
Array.isArray(workflowConfig.activations) &&
|
|
873
|
-
workflowConfig.activations.length > 0 ||
|
|
874
887
|
Array.isArray(workflowConfig.providers) && workflowConfig.providers.length > 0)) {
|
|
875
|
-
diagnostics.push(diagnostic('APP_CONFIG_BACKEND_DISABLED_WITH_DECLARATIONS', 'backend.enabled=false 时不能声明后端操作、Secret
|
|
888
|
+
diagnostics.push(diagnostic('APP_CONFIG_BACKEND_DISABLED_WITH_DECLARATIONS', 'backend.enabled=false 时不能声明后端操作、Secret、事件消费者或工作流人员提供器', 'backend.enabled'));
|
|
876
889
|
}
|
|
877
890
|
const secretNames = new Set();
|
|
878
891
|
const secretEnvs = new Set();
|
|
@@ -2636,26 +2649,33 @@ function generatedPlatformRouteClaims(config) {
|
|
|
2636
2649
|
const code = string(resource.code);
|
|
2637
2650
|
if (!code)
|
|
2638
2651
|
continue;
|
|
2639
|
-
const
|
|
2640
|
-
const
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
[
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2652
|
+
const resourceSurface = object(resource.surface);
|
|
2653
|
+
for (const selected of [resourceSurface, ...(Array.isArray(resourceSurface.views) ? resourceSurface.views.map(object) : [])]) {
|
|
2654
|
+
const surface = { ...resourceSurface, ...selected };
|
|
2655
|
+
const viewCode = string(selected.code);
|
|
2656
|
+
const generated = object(surface.generated);
|
|
2657
|
+
const native = !surface.mutationOwner || surface.mutationOwner === 'native';
|
|
2658
|
+
const operations = {
|
|
2659
|
+
list: generated.list === undefined ? true : generated.list === true,
|
|
2660
|
+
detail: generated.detail === undefined ? true : generated.detail === true,
|
|
2661
|
+
create: generated.create === undefined ? native : generated.create === true,
|
|
2662
|
+
update: generated.update === undefined ? native : generated.update === true,
|
|
2663
|
+
};
|
|
2664
|
+
const base = `/admin/resources/${code}${viewCode ? `/views/${viewCode}` : ""}`;
|
|
2665
|
+
const candidates = [
|
|
2666
|
+
['list', base],
|
|
2667
|
+
['detail', `${base}/:id`],
|
|
2668
|
+
['create', `${base}/new`],
|
|
2669
|
+
['update', `${base}/:id/edit`],
|
|
2670
|
+
];
|
|
2671
|
+
for (const [operation, path] of candidates) {
|
|
2672
|
+
if (!operations[operation])
|
|
2673
|
+
continue;
|
|
2674
|
+
const owner = `resource:${code}${viewCode ? `:view:${viewCode}` : ''}:${operation}`;
|
|
2675
|
+
claims.push({ owner, path });
|
|
2676
|
+
if (object(surface.mobile).enabled !== false)
|
|
2677
|
+
claims.push({ owner: `${owner}:mobile`, path: `/m${path}` });
|
|
2678
|
+
}
|
|
2659
2679
|
}
|
|
2660
2680
|
}
|
|
2661
2681
|
const frontend = object(config.frontend);
|
|
@@ -2717,6 +2737,68 @@ function validateFrontendDevicePolicy(config, diagnostics) {
|
|
|
2717
2737
|
diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_DEVICE_POLICY_INVALID', 'frontend.devicePolicy 必须是 viewport-family,移动最大宽度和桌面最小宽度为 320-1600 的相邻整数', 'frontend.devicePolicy'));
|
|
2718
2738
|
}
|
|
2719
2739
|
}
|
|
2740
|
+
function validateResourceDetailRoutes(config, diagnostics) {
|
|
2741
|
+
const frontend = object(config.frontend);
|
|
2742
|
+
const routesByCode = new Map((Array.isArray(frontend.routes) ? frontend.routes : []).map(rawRoute => {
|
|
2743
|
+
const route = object(rawRoute);
|
|
2744
|
+
return [string(route.code), route];
|
|
2745
|
+
}));
|
|
2746
|
+
const data = object(config.data);
|
|
2747
|
+
const resources = Array.isArray(data.resources) ? data.resources : [];
|
|
2748
|
+
resources.forEach((rawResource, resourceIndex) => {
|
|
2749
|
+
const resource = object(rawResource);
|
|
2750
|
+
if (resource.detailRouteCode === undefined)
|
|
2751
|
+
return;
|
|
2752
|
+
const path = `data.resources[${resourceIndex}].detailRouteCode`;
|
|
2753
|
+
const detailRouteCode = object(resource.detailRouteCode);
|
|
2754
|
+
const desktopCode = string(detailRouteCode.desktop);
|
|
2755
|
+
const mobileCode = string(detailRouteCode.mobile);
|
|
2756
|
+
if (Object.keys(detailRouteCode).some(key => !['desktop', 'mobile'].includes(key)) ||
|
|
2757
|
+
!OPERATION_CODE_PATTERN.test(desktopCode) ||
|
|
2758
|
+
!OPERATION_CODE_PATTERN.test(mobileCode) ||
|
|
2759
|
+
desktopCode === mobileCode) {
|
|
2760
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_INVALID', 'detailRouteCode 必须声明两个不同且合法的 desktop/mobile route code', path));
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
const readCapability = string(object(resource.capabilities).read);
|
|
2764
|
+
for (const [device, code] of [
|
|
2765
|
+
['desktop', desktopCode],
|
|
2766
|
+
['mobile', mobileCode],
|
|
2767
|
+
]) {
|
|
2768
|
+
const route = routesByCode.get(code);
|
|
2769
|
+
const routePointer = `${path}.${device}`;
|
|
2770
|
+
if (!route) {
|
|
2771
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_REFERENCE_MISSING', `detailRouteCode.${device} 必须引用已声明 frontend route`, routePointer));
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
if (string(route.surface) !== 'user') {
|
|
2775
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_SURFACE_INVALID', `detailRouteCode.${device} 必须引用 user surface`, routePointer));
|
|
2776
|
+
}
|
|
2777
|
+
const routePath = string(route.path);
|
|
2778
|
+
const parameters = [
|
|
2779
|
+
...routePath.matchAll(/:([A-Za-z][A-Za-z0-9_]*)/g),
|
|
2780
|
+
].map(match => match[1]);
|
|
2781
|
+
const wrongDeviceFamily = device === 'mobile'
|
|
2782
|
+
? !routePath.startsWith('/m/')
|
|
2783
|
+
: routePath === '/m' || routePath.startsWith('/m/');
|
|
2784
|
+
if (routePath.includes('*') ||
|
|
2785
|
+
routePath.includes('?') ||
|
|
2786
|
+
routePath.includes('#') ||
|
|
2787
|
+
routePath.split('/').includes('..') ||
|
|
2788
|
+
parameters.length !== 1 ||
|
|
2789
|
+
wrongDeviceFamily) {
|
|
2790
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_PATH_INVALID', `detailRouteCode.${device} 路由必须属于对应端且只能声明一个有界动态记录参数`, routePointer));
|
|
2791
|
+
}
|
|
2792
|
+
const access = object(route.access);
|
|
2793
|
+
const allOf = Array.isArray(access.allOf) ? access.allOf.map(string) : [];
|
|
2794
|
+
if (!readCapability ||
|
|
2795
|
+
(string(route.capability) !== readCapability &&
|
|
2796
|
+
!allOf.includes(readCapability))) {
|
|
2797
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_CAPABILITY_INVALID', `detailRouteCode.${device} 路由必须要求当前资源的 read capability`, routePointer));
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2720
2802
|
function validateFrontendRoutes(config, diagnostics) {
|
|
2721
2803
|
const frontend = object(config.frontend);
|
|
2722
2804
|
const rawRoutes = frontend.routes;
|
|
@@ -3228,10 +3310,12 @@ function validateAdminNavigation(config, diagnostics) {
|
|
|
3228
3310
|
if (kind === 'resource') {
|
|
3229
3311
|
const resourceCode = string(page.resourceCode);
|
|
3230
3312
|
const resource = resources.get(resourceCode);
|
|
3231
|
-
|
|
3232
|
-
|
|
3313
|
+
const viewCode = string(page.viewCode);
|
|
3314
|
+
const view = resource?.surface?.views?.find((item) => item.code === viewCode);
|
|
3315
|
+
pageKey = `resource:${resourceCode}${viewCode ? `:view:${viewCode}` : ""}:list`;
|
|
3316
|
+
defaultLabel = (viewCode ? view?.name : resource?.name) || '';
|
|
3233
3317
|
internalCode = resourceCode;
|
|
3234
|
-
exists = Boolean(resource?.surface?.generated?.list);
|
|
3318
|
+
exists = Boolean(viewCode ? view?.generated?.list : resource?.surface?.generated?.list);
|
|
3235
3319
|
}
|
|
3236
3320
|
else if (kind === 'operation') {
|
|
3237
3321
|
const routeCode = string(page.routeCode);
|
|
@@ -3437,24 +3521,21 @@ export function resourceCapabilityCodes(appCode, resourceCode) {
|
|
|
3437
3521
|
delete: `${prefix}:delete`,
|
|
3438
3522
|
};
|
|
3439
3523
|
}
|
|
3440
|
-
function materializeRoleCapabilities(
|
|
3524
|
+
function materializeRoleCapabilities(role) {
|
|
3441
3525
|
const capabilities = new Set(role.capabilities);
|
|
3442
|
-
for (const resource of config.data?.resources || []) {
|
|
3443
|
-
if (resource.surface?.mutationOwner !== 'native' ||
|
|
3444
|
-
!capabilities.has(resource.capabilities.read)) {
|
|
3445
|
-
continue;
|
|
3446
|
-
}
|
|
3447
|
-
for (const operation of ['create', 'update', 'delete']) {
|
|
3448
|
-
if (resource.surface?.generated?.[operation] === true) {
|
|
3449
|
-
capabilities.add(resource.capabilities[operation]);
|
|
3450
|
-
}
|
|
3451
|
-
}
|
|
3452
|
-
}
|
|
3453
3526
|
for (const denied of role.deniedCapabilities || []) {
|
|
3454
3527
|
capabilities.delete(denied);
|
|
3455
3528
|
}
|
|
3456
3529
|
return [...capabilities].sort();
|
|
3457
3530
|
}
|
|
3531
|
+
/** Explicit business permission preset; adding a page never changes grants. */
|
|
3532
|
+
export function resourceRoleCapabilities(appCode, resourceCode, access) {
|
|
3533
|
+
const codes = resourceCapabilityCodes(appCode, resourceCode);
|
|
3534
|
+
const operations = access === 'manage'
|
|
3535
|
+
? ['read', 'create', 'update', 'delete']
|
|
3536
|
+
: access === 'read' ? ['read'] : access;
|
|
3537
|
+
return [...new Set(operations.map(operation => codes[operation]))].sort();
|
|
3538
|
+
}
|
|
3458
3539
|
export function currentUserDataPolicy(input) {
|
|
3459
3540
|
return {
|
|
3460
3541
|
code: input.code,
|
|
@@ -3546,9 +3627,12 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
3546
3627
|
physicalPlanForField(definition, field.searchable === true);
|
|
3547
3628
|
return definition;
|
|
3548
3629
|
});
|
|
3549
|
-
const
|
|
3550
|
-
.filter(field => field.list !== false)
|
|
3551
|
-
|
|
3630
|
+
const listCandidates = declaration.fields
|
|
3631
|
+
.filter(field => field.list !== false && (field.hidden ?? field.system) !== true);
|
|
3632
|
+
const visible = declaration.list?.fields
|
|
3633
|
+
? declaration.list.fields.flatMap(code => listCandidates.filter(field => field.code === code))
|
|
3634
|
+
: listCandidates.slice(0, 8);
|
|
3635
|
+
const defaultSortField = visible.find(field => supportsSort(field.type));
|
|
3552
3636
|
const searchableFields = declaration.fields
|
|
3553
3637
|
.filter(field => field.searchable === true)
|
|
3554
3638
|
.map(field => field.code);
|
|
@@ -3571,6 +3655,7 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
3571
3655
|
return [capabilities[operation]];
|
|
3572
3656
|
};
|
|
3573
3657
|
const surface = {
|
|
3658
|
+
...(declaration.views?.length ? { views: declaration.views } : {}),
|
|
3574
3659
|
mutationOwner,
|
|
3575
3660
|
generated,
|
|
3576
3661
|
fields: Object.fromEntries(declaration.fields.map(field => [
|
|
@@ -3582,6 +3667,9 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
3582
3667
|
...(field.section ? { section: field.section } : {}),
|
|
3583
3668
|
...(field.required === true ? { requiredHint: true } : {}),
|
|
3584
3669
|
...(field.system !== undefined ? { system: field.system } : {}),
|
|
3670
|
+
...(field.hidden !== undefined || field.system
|
|
3671
|
+
? { hidden: field.hidden ?? true }
|
|
3672
|
+
: {}),
|
|
3585
3673
|
readCapabilities: access(field, 'read'),
|
|
3586
3674
|
createCapabilities: access(field, 'create'),
|
|
3587
3675
|
updateCapabilities: access(field, 'update'),
|
|
@@ -3615,22 +3703,23 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
3615
3703
|
},
|
|
3616
3704
|
])),
|
|
3617
3705
|
list: {
|
|
3706
|
+
fieldOrder: visible.map(field => field.code),
|
|
3618
3707
|
defaultPageSize: declaration.list?.defaultPageSize || 20,
|
|
3619
3708
|
...(searchableFields.length ? { searchableFields } : {}),
|
|
3620
3709
|
...(filterFields.length ? { filterFields } : {}),
|
|
3621
3710
|
...(declaration.list?.defaultSort
|
|
3622
3711
|
? { defaultSort: declaration.list.defaultSort }
|
|
3623
|
-
:
|
|
3624
|
-
? { defaultSort: { field:
|
|
3712
|
+
: defaultSortField
|
|
3713
|
+
? { defaultSort: { field: defaultSortField.code, order: 'asc' } }
|
|
3625
3714
|
: {}),
|
|
3626
3715
|
},
|
|
3627
3716
|
form: {
|
|
3628
3717
|
layout: declaration.form?.layout || 'flat',
|
|
3629
|
-
fieldOrder: declaration.fields.map(field => field.code),
|
|
3718
|
+
fieldOrder: declaration.form?.fields || declaration.fields.map(field => field.code),
|
|
3630
3719
|
},
|
|
3631
3720
|
detail: {
|
|
3632
3721
|
layout: declaration.detail?.layout || 'flat',
|
|
3633
|
-
fieldOrder: declaration.fields.map(field => field.code),
|
|
3722
|
+
fieldOrder: declaration.detail?.fields || declaration.fields.map(field => field.code),
|
|
3634
3723
|
},
|
|
3635
3724
|
mobile: declaration.mobile || { enabled: true },
|
|
3636
3725
|
};
|
|
@@ -3646,6 +3735,9 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
3646
3735
|
...(declaration.dataPolicyCode
|
|
3647
3736
|
? { dataPolicyCode: declaration.dataPolicyCode }
|
|
3648
3737
|
: {}),
|
|
3738
|
+
...(declaration.detailRouteCode
|
|
3739
|
+
? { detailRouteCode: { ...declaration.detailRouteCode } }
|
|
3740
|
+
: {}),
|
|
3649
3741
|
fieldPolicies: Object.fromEntries(declaration.fields.map(field => [
|
|
3650
3742
|
field.code,
|
|
3651
3743
|
{
|
|
@@ -3664,6 +3756,7 @@ export function validateAppDeclaration(value) {
|
|
|
3664
3756
|
const resources = Array.isArray(data.resources) ? data.resources : [];
|
|
3665
3757
|
const diagnostics = [];
|
|
3666
3758
|
const resourceKeys = new Set([
|
|
3759
|
+
'views',
|
|
3667
3760
|
'code',
|
|
3668
3761
|
'name',
|
|
3669
3762
|
'mutationOwner',
|
|
@@ -3675,6 +3768,7 @@ export function validateAppDeclaration(value) {
|
|
|
3675
3768
|
'detail',
|
|
3676
3769
|
'mobile',
|
|
3677
3770
|
'dataPolicyCode',
|
|
3771
|
+
'detailRouteCode',
|
|
3678
3772
|
]);
|
|
3679
3773
|
const fieldKeys = new Set([
|
|
3680
3774
|
'code',
|
|
@@ -3697,6 +3791,7 @@ export function validateAppDeclaration(value) {
|
|
|
3697
3791
|
'widget',
|
|
3698
3792
|
'section',
|
|
3699
3793
|
'system',
|
|
3794
|
+
'hidden',
|
|
3700
3795
|
'list',
|
|
3701
3796
|
'filter',
|
|
3702
3797
|
'searchable',
|
|
@@ -3708,7 +3803,7 @@ export function validateAppDeclaration(value) {
|
|
|
3708
3803
|
const resourcePath = `data.resources[${resourceIndex}]`;
|
|
3709
3804
|
for (const key of Object.keys(resource)) {
|
|
3710
3805
|
if (!resourceKeys.has(key)) {
|
|
3711
|
-
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_PROPERTY_UNSUPPORTED', `${resourcePath}.${key} 不属于单次资源声明合同`, `${resourcePath}.${key}`, '只声明 code、name、fields
|
|
3806
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_PROPERTY_UNSUPPORTED', `${resourcePath}.${key} 不属于单次资源声明合同`, `${resourcePath}.${key}`, '只声明 code、name、fields 及可选列表/布局/数据策略/详情路由'));
|
|
3712
3807
|
}
|
|
3713
3808
|
}
|
|
3714
3809
|
const fields = Array.isArray(resource.fields) ? resource.fields : [];
|
|
@@ -3776,12 +3871,6 @@ export function validateAppDeclaration(value) {
|
|
|
3776
3871
|
else if (!rangeType && field.rangeBoundary !== undefined) {
|
|
3777
3872
|
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_RANGE_BOUNDARY_TYPE_INVALID', 'rangeBoundary 只能用于 date-range 或 datetime-range', `${fieldPath}.rangeBoundary`));
|
|
3778
3873
|
}
|
|
3779
|
-
if (field.indexed === false &&
|
|
3780
|
-
(field.filter === true ||
|
|
3781
|
-
field.searchable === true ||
|
|
3782
|
-
field.sortable === true)) {
|
|
3783
|
-
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_INDEX_REQUIRED', '启用筛选、搜索或排序时不能显式关闭索引', `${fieldPath}.indexed`));
|
|
3784
|
-
}
|
|
3785
3874
|
if (['radio', 'checkbox'].includes(widget) &&
|
|
3786
3875
|
object(field.source).loadMode !== 'all' &&
|
|
3787
3876
|
field.source !== undefined) {
|
|
@@ -3795,6 +3884,7 @@ export function validateAppDeclaration(value) {
|
|
|
3795
3884
|
'required',
|
|
3796
3885
|
'indexed',
|
|
3797
3886
|
'system',
|
|
3887
|
+
'hidden',
|
|
3798
3888
|
'list',
|
|
3799
3889
|
'filter',
|
|
3800
3890
|
'searchable',
|
|
@@ -3823,6 +3913,27 @@ export function validateAppDeclaration(value) {
|
|
|
3823
3913
|
if (code)
|
|
3824
3914
|
declaredCodes.add(code);
|
|
3825
3915
|
});
|
|
3916
|
+
for (const view of ['list', 'form', 'detail']) {
|
|
3917
|
+
const selected = object(resource[view]).fields;
|
|
3918
|
+
if (selected === undefined)
|
|
3919
|
+
continue;
|
|
3920
|
+
if (!Array.isArray(selected) || new Set(selected).size !== selected.length ||
|
|
3921
|
+
selected.some(code => typeof code !== 'string' || !declaredCodes.has(code))) {
|
|
3922
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_VIEW_FIELDS_INVALID', '列表/表单/详情 fields 必须是当前资源中不重复的字段列表', `${resourcePath}.${view}.fields`));
|
|
3923
|
+
materializable = false;
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
const selectedFormFields = object(resource.form).fields ?? fields.map(raw => object(raw).code);
|
|
3927
|
+
if (Array.isArray(selectedFormFields) && (generated.create ?? mutationOwner === 'native')) {
|
|
3928
|
+
for (const rawField of fields) {
|
|
3929
|
+
const field = object(rawField);
|
|
3930
|
+
if (field.required === true && field.system !== true &&
|
|
3931
|
+
supportsGeneratedMutation({ type: field.type, system: false }) &&
|
|
3932
|
+
object(field.access).create !== false && (field.hidden === true || !selectedFormFields.includes(field.code))) {
|
|
3933
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_FORM_REQUIRED_FIELD_MISSING', `标准新增表单缺少必填字段 ${field.label || field.code};请加入表单或关闭标准新增`, `${resourcePath}.form.fields`));
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3826
3937
|
for (const operation of ['create', 'update']) {
|
|
3827
3938
|
const enabled = generated[operation] ?? mutationOwner === 'native';
|
|
3828
3939
|
const writable = fields.some(rawField => {
|
|
@@ -3844,6 +3955,19 @@ export function validateAppDeclaration(value) {
|
|
|
3844
3955
|
}
|
|
3845
3956
|
if (appCode && fields.length && materializable) {
|
|
3846
3957
|
const canonical = materializeDataResource(appCode, rawResource);
|
|
3958
|
+
for (const [viewIndex, view] of (canonical.surface?.views || []).entries()) {
|
|
3959
|
+
for (const [property, values, supports] of [
|
|
3960
|
+
['searchableFields', view.list?.searchableFields || [], supportsSearch],
|
|
3961
|
+
['filterFields', view.list?.filterFields || [], supportsFilter],
|
|
3962
|
+
['defaultSort', view.list?.defaultSort ? [view.list.defaultSort.field] : [], supportsSort],
|
|
3963
|
+
]) {
|
|
3964
|
+
for (const code of values) {
|
|
3965
|
+
const field = canonical.schema.fields.find(field => field.code === code);
|
|
3966
|
+
if (!field || !supports(field.type))
|
|
3967
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_VIEW_QUERY_FIELD_INVALID', '视图查询字段不存在或不支持此查询类型', `${resourcePath}.views[${viewIndex}].list.${property}`));
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3847
3971
|
diagnostics.push(...validateDataResource(canonical).map(item => ({
|
|
3848
3972
|
...item,
|
|
3849
3973
|
path: `${resourcePath}.${item.path}`,
|
|
@@ -3911,7 +4035,27 @@ export function validateAppDeclaration(value) {
|
|
|
3911
4035
|
return diagnostics;
|
|
3912
4036
|
}
|
|
3913
4037
|
export function defineOpenXiangdaApp(declaration) {
|
|
3914
|
-
const
|
|
4038
|
+
const { modules, ...input } = declaration;
|
|
4039
|
+
const projected = materializeApplicationModules(modules || []);
|
|
4040
|
+
if (projected.diagnostics.length)
|
|
4041
|
+
throw new AppConfigValidationError(projected.diagnostics);
|
|
4042
|
+
const normalizedDeclaration = normalizeLegacyUserSurfaceAuthoring({
|
|
4043
|
+
...input,
|
|
4044
|
+
schemaVersion: declaration.schemaVersion ?? 3,
|
|
4045
|
+
frontend: { root: 'apps/web', ...declaration.frontend },
|
|
4046
|
+
backend: {
|
|
4047
|
+
root: 'apps/server', runtime: 'node', framework: 'nestjs',
|
|
4048
|
+
enabled: Boolean(declaration.backend?.operations?.length || declaration.backend?.secrets?.length ||
|
|
4049
|
+
declaration.events?.subscriptions?.length || declaration.events?.timers?.length ||
|
|
4050
|
+
declaration.events?.dateTriggers?.length ||
|
|
4051
|
+
declaration.workflows?.providers?.length),
|
|
4052
|
+
...declaration.backend,
|
|
4053
|
+
},
|
|
4054
|
+
platform: { root: 'platform', ...declaration.platform },
|
|
4055
|
+
...(modules || declaration.data ? {
|
|
4056
|
+
data: { resources: [...(declaration.data?.resources || []), ...projected.resources] },
|
|
4057
|
+
} : {}),
|
|
4058
|
+
});
|
|
3915
4059
|
const declarationDiagnostics = validateAppDeclaration(normalizedDeclaration);
|
|
3916
4060
|
if (declarationDiagnostics.length > 0) {
|
|
3917
4061
|
throw new AppConfigValidationError(declarationDiagnostics);
|
|
@@ -3934,7 +4078,7 @@ export function defineOpenXiangdaApp(declaration) {
|
|
|
3934
4078
|
...materialized.authz,
|
|
3935
4079
|
roles: materialized.authz.roles.map(role => ({
|
|
3936
4080
|
...role,
|
|
3937
|
-
capabilities: materializeRoleCapabilities(
|
|
4081
|
+
capabilities: materializeRoleCapabilities(role),
|
|
3938
4082
|
})),
|
|
3939
4083
|
},
|
|
3940
4084
|
}
|