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.
Files changed (53) hide show
  1. package/dist/application-services.d.ts +181 -1
  2. package/dist/application-services.d.ts.map +1 -1
  3. package/dist/application-services.js +33 -0
  4. package/dist/application-services.js.map +1 -1
  5. package/dist/application-ui-contract.d.ts +5 -0
  6. package/dist/application-ui-contract.d.ts.map +1 -0
  7. package/dist/application-ui-contract.js +149 -0
  8. package/dist/application-ui-contract.js.map +1 -0
  9. package/dist/command-registry.d.ts +2 -2
  10. package/dist/command-registry.js +1 -1
  11. package/dist/command-registry.js.map +1 -1
  12. package/dist/compiler/application-model.d.ts +60 -0
  13. package/dist/compiler/application-model.d.ts.map +1 -0
  14. package/dist/compiler/application-model.js +141 -0
  15. package/dist/compiler/application-model.js.map +1 -0
  16. package/dist/compiler/bundle.d.ts.map +1 -1
  17. package/dist/compiler/bundle.js +50 -12
  18. package/dist/compiler/bundle.js.map +1 -1
  19. package/dist/compiler/config.d.ts +27 -8
  20. package/dist/compiler/config.d.ts.map +1 -1
  21. package/dist/compiler/config.js +205 -61
  22. package/dist/compiler/config.js.map +1 -1
  23. package/dist/compiler/field-surface.js +1 -1
  24. package/dist/compiler/field-surface.js.map +1 -1
  25. package/dist/compiler/permission-review.d.ts +139 -0
  26. package/dist/compiler/permission-review.d.ts.map +1 -0
  27. package/dist/compiler/permission-review.js +131 -0
  28. package/dist/compiler/permission-review.js.map +1 -0
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +4 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/optional-backend.d.ts +14 -0
  34. package/dist/optional-backend.d.ts.map +1 -0
  35. package/dist/optional-backend.js +145 -0
  36. package/dist/optional-backend.js.map +1 -0
  37. package/dist/workspace-loader.d.ts.map +1 -1
  38. package/dist/workspace-loader.js +16 -1
  39. package/dist/workspace-loader.js.map +1 -1
  40. package/package.json +3 -2
  41. package/templates/backend/.turbo/turbo-build.log +4 -0
  42. package/templates/backend/.turbo/turbo-check.log +4 -0
  43. package/templates/backend/.turbo/turbo-test.log +13 -0
  44. package/templates/backend/Dockerfile +20 -0
  45. package/templates/backend/node_modules/.bin/openxiangda +21 -0
  46. package/templates/backend/node_modules/.bin/tsc +21 -0
  47. package/templates/backend/node_modules/.bin/tsserver +21 -0
  48. package/templates/backend/node_modules/.bin/tsx +21 -0
  49. package/templates/backend/package.json +27 -0
  50. package/templates/backend/src/app.module.ts +23 -0
  51. package/templates/backend/src/main.ts +5 -0
  52. package/templates/backend/test/smoke.test.ts +17 -0
  53. package/templates/backend/tsconfig.json +9 -0
@@ -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
- return adminNavigationItem({ kind: 'resource', resourceCode }, options);
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
- if (resources.length > 0) {
98
- groups.push(adminNavigationGroup('data-management', '数据管理', resources.map(resource => adminResourcePage(resource.code, { label: resource.name })), { icon: 'database', order: 100 }));
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、事件或工作流运行时', 'backend.enabled'));
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 surface = object(resource.surface);
2640
- const generated = object(surface.generated);
2641
- const native = !surface.mutationOwner || surface.mutationOwner === 'native';
2642
- const operations = {
2643
- list: generated.list === undefined ? true : generated.list === true,
2644
- detail: generated.detail === undefined ? true : generated.detail === true,
2645
- create: generated.create === undefined ? native : generated.create === true,
2646
- update: generated.update === undefined ? native : generated.update === true,
2647
- };
2648
- const base = `/admin/resources/${code}`;
2649
- const candidates = [
2650
- ['list', base],
2651
- ['detail', `${base}/:id`],
2652
- ['create', `${base}/new`],
2653
- ['update', `${base}/:id/edit`],
2654
- ];
2655
- for (const [operation, path] of candidates) {
2656
- if (!operations[operation])
2657
- continue;
2658
- claims.push({ owner: `resource:${code}:${operation}`, path }, { owner: `resource:${code}:${operation}:mobile`, path: `/m${path}` });
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
- pageKey = `resource:${resourceCode}:list`;
3232
- defaultLabel = resource?.name || '';
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(config, role) {
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 visible = declaration.fields
3550
- .filter(field => field.list !== false)
3551
- .slice(0, 8);
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
- : visible[0]
3624
- ? { defaultSort: { field: visible[0].code, order: 'asc' } }
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 normalizedDeclaration = normalizeLegacyUserSurfaceAuthoring(declaration);
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(materialized, role),
4081
+ capabilities: materializeRoleCapabilities(role),
3938
4082
  })),
3939
4083
  },
3940
4084
  }