openxiangda-devkit-core 2.41.6 → 2.51.1

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 (51) hide show
  1. package/dist/application-services.d.ts +1 -0
  2. package/dist/application-services.d.ts.map +1 -1
  3. package/dist/application-services.js +32 -6
  4. package/dist/application-services.js.map +1 -1
  5. package/dist/compiler/ai-catalog.d.ts.map +1 -1
  6. package/dist/compiler/ai-catalog.js +2 -10
  7. package/dist/compiler/ai-catalog.js.map +1 -1
  8. package/dist/compiler/application-model.d.ts +5 -1
  9. package/dist/compiler/application-model.d.ts.map +1 -1
  10. package/dist/compiler/application-model.js +5 -1
  11. package/dist/compiler/application-model.js.map +1 -1
  12. package/dist/compiler/bundle.d.ts.map +1 -1
  13. package/dist/compiler/bundle.js +107 -32
  14. package/dist/compiler/bundle.js.map +1 -1
  15. package/dist/compiler/config.d.ts +16 -1
  16. package/dist/compiler/config.d.ts.map +1 -1
  17. package/dist/compiler/config.js +231 -24
  18. package/dist/compiler/config.js.map +1 -1
  19. package/dist/compiler/permission-review.d.ts.map +1 -1
  20. package/dist/compiler/schema-composition.js +3 -0
  21. package/dist/compiler/schema-composition.js.map +1 -1
  22. package/dist/connected-development-vite.d.ts +15 -0
  23. package/dist/connected-development-vite.d.ts.map +1 -0
  24. package/dist/connected-development-vite.js +27 -0
  25. package/dist/connected-development-vite.js.map +1 -0
  26. package/dist/connected-development.d.ts +5 -0
  27. package/dist/connected-development.d.ts.map +1 -1
  28. package/dist/connected-development.js +105 -27
  29. package/dist/connected-development.js.map +1 -1
  30. package/dist/control-plane-client.d.ts +8 -0
  31. package/dist/control-plane-client.d.ts.map +1 -1
  32. package/dist/control-plane-client.js +36 -0
  33. package/dist/control-plane-client.js.map +1 -1
  34. package/dist/deployment.d.ts.map +1 -1
  35. package/dist/deployment.js +6 -1
  36. package/dist/deployment.js.map +1 -1
  37. package/dist/development-backend-relay.d.ts +28 -0
  38. package/dist/development-backend-relay.d.ts.map +1 -0
  39. package/dist/development-backend-relay.js +184 -0
  40. package/dist/development-backend-relay.js.map +1 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/internal/workflow.d.ts.map +1 -1
  46. package/dist/internal/workflow.js +22 -7
  47. package/dist/internal/workflow.js.map +1 -1
  48. package/dist/workspace-loader.js +1 -1
  49. package/dist/workspace-loader.js.map +1 -1
  50. package/package.json +2 -2
  51. package/templates/backend/Dockerfile +1 -0
@@ -1,10 +1,16 @@
1
+ import { assertWorkflowNativeStagePolicy } from 'openxiangda-contracts';
2
+ import { validateAuthenticatedPublicRead, AuthenticatedPublicReadContractError } from 'openxiangda-contracts/native-compiler';
3
+ import { DATE_EVENT_RESERVED_DATA_FIELDS_V2, dateEventProjectionFieldsV2, dateEventProjectionEnvelopeSchemaV2 } from 'openxiangda-contracts/native-compiler';
4
+ import { validateUserCandidateReferences, validateWorkflowUserCandidateBindings, UserCandidateContractError } from 'openxiangda-contracts';
1
5
  import { parseNativeUniqueKeys, DATA_SUBTABLE_MAX_TOTAL_ROWS } from 'openxiangda-contracts';
6
+ import { assertWorkflowOwnedSubjectCreate } from 'openxiangda-contracts';
7
+ export { workflowBusinessCommandInvocationSchema } from 'openxiangda-contracts';
2
8
  import { validateManagedConcurrency } from 'openxiangda-contracts/native-compiler';
3
9
  import { normalizeDecimalReservationEventDeclaration, decimalReservationEventContext } from 'openxiangda-contracts';
4
10
  import { materializeApplicationModules } from './application-model.js';
5
11
  import { nativeFieldRequiresCreateInputV2 } from 'openxiangda-contracts/native-compiler';
6
12
  import { NativeDecimalLifecycleContractError, validateDecimalReservationLifecycles } from 'openxiangda-contracts/native-compiler';
7
- import { SCHEMA_VERSIONS, DATA_AUDIT_METADATA_FIELDS, isDataAuditMetadataField, isDataSystemSortField, DATA_FIELD_TYPES, PLATFORM_EVENT_TYPES_V2, nativePlatformCapabilityCatalog, validateDataResource, validateWorkflowInstanceCommandPolicies, validateWorkflowTaskPages, validateWorkflowAssignmentRoutingBindings, WORKFLOW_SUMMARY_MAX_FIELDS, WORKFLOW_SUMMARY_TEXT_LONG_MAX_BYTES, isWorkflowSummaryFieldType, } from 'openxiangda-contracts';
13
+ import { SCHEMA_VERSIONS, DATA_AUDIT_METADATA_FIELDS, isDataAuditMetadataField, isDataSystemSortField, DATA_FIELD_TYPES, PLATFORM_EVENT_TYPES_V2, nativePlatformCapabilityCatalog, validateDataResource, validateWorkflowInstanceCommandPolicies, validateWorkflowTaskPages, validateWorkflowCommandHandlers, validateWorkflowAssignmentRoutingBindings, WORKFLOW_SUMMARY_MAX_FIELDS, WORKFLOW_SUMMARY_TEXT_LONG_MAX_BYTES, isWorkflowSummaryFieldType, } from 'openxiangda-contracts';
8
14
  import { Ajv2020 } from 'ajv/dist/2020.js';
9
15
  import { CronExpressionParser } from 'cron-parser';
10
16
  import { fieldNullable, supportsGeneratedMutation, } from './field-codec.js';
@@ -17,14 +23,7 @@ import { AUTHORIZATION_TRANSITION_CAPABILITY_CODE_PATTERN, AUTHORIZATION_TRANSIT
17
23
  // The compiler owns the application source contract. Devkit and every adapter
18
24
  // re-export this module instead of maintaining separate configuration models.
19
25
  import { validateWorkflowBinding, validateWorkflowDefinition, } from '../internal/workflow.js';
20
- const DATE_TRIGGER_RESERVED_DATA_FIELDS = new Set([
21
- 'triggerCode',
22
- 'resourceCode',
23
- 'recordId',
24
- 'recordRevision',
25
- 'field',
26
- 'dueAt',
27
- ]);
26
+ const DATE_TRIGGER_RESERVED_DATA_FIELDS = new Set(DATE_EVENT_RESERVED_DATA_FIELDS_V2);
28
27
  const APP_DATA_FIELD_TYPES = new Set(DATA_FIELD_TYPES);
29
28
  const WORKFLOW_RUNTIME_CODE_PATTERN = /^[a-z][a-z0-9-]{2,63}$/;
30
29
  function adminNavigationItem(page, options = {}) {
@@ -966,6 +965,7 @@ export function validateAppConfig(value) {
966
965
  secretNames.add(name);
967
966
  secretEnvs.add(env);
968
967
  });
968
+ const authz = object(config.authz);
969
969
  const data = object(config.data);
970
970
  if (config.data !== undefined && !Array.isArray(data.resources)) {
971
971
  diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCES_REQUIRED', 'data.resources 必须是数组', 'data.resources'));
@@ -995,6 +995,15 @@ export function validateAppConfig(value) {
995
995
  resources.forEach((resource, index) => {
996
996
  const resourceRecord = object(resource);
997
997
  const fields = object(resourceRecord.schema).fields;
998
+ try {
999
+ validateUserCandidateReferences((Array.isArray(fields) ? fields : []), new Set((Array.isArray(authz.roles) ? authz.roles : []).map(item => string(object(item).code))), new Set((Array.isArray(authz.scopeDimensions) ? authz.scopeDimensions : []).map(item => string(object(item).code))), `data.resources[${index}].schema.fields`);
1000
+ }
1001
+ catch (error) {
1002
+ if (!(error instanceof UserCandidateContractError))
1003
+ throw error;
1004
+ const issue = error;
1005
+ diagnostics.push(diagnostic(issue.code, '人员候选必须引用同一应用声明的职责、范围和兼容字段', issue.pointer));
1006
+ }
998
1007
  (Array.isArray(fields) ? fields : []).forEach((rawField, fieldIndex) => {
999
1008
  const field = object(rawField);
1000
1009
  const source = object(field.source);
@@ -1048,7 +1057,6 @@ export function validateAppConfig(value) {
1048
1057
  });
1049
1058
  });
1050
1059
  }
1051
- const authz = object(config.authz);
1052
1060
  const declaredCapabilities = Array.isArray(authz.capabilities)
1053
1061
  ? authz.capabilities
1054
1062
  : [];
@@ -1069,7 +1077,13 @@ export function validateAppConfig(value) {
1069
1077
  const mutationOwner = string(object(resource.surface).mutationOwner) || 'native';
1070
1078
  if (['native', 'queued-command'].includes(mutationOwner))
1071
1079
  continue;
1080
+ const recordDeletionCapabilities = object(resource.recordDeletion).delete;
1072
1081
  for (const operation of ['create', 'update', 'delete']) {
1082
+ // This capability authorizes the server's controlled deletion entry;
1083
+ // ordinary Native mutations still belong to the declared owner.
1084
+ if (operation === 'delete' && Array.isArray(recordDeletionCapabilities) &&
1085
+ recordDeletionCapabilities.length > 0)
1086
+ continue;
1073
1087
  const capability = string(object(resource.capabilities)[operation]);
1074
1088
  if (capability) {
1075
1089
  forbiddenNativeMutationCapabilities.set(capability, {
@@ -1436,6 +1450,7 @@ export function validateAppConfig(value) {
1436
1450
  'name',
1437
1451
  'resourceCode',
1438
1452
  'unrestrictedRoleCodes',
1453
+ 'publicRead',
1439
1454
  'operations',
1440
1455
  'matchMode',
1441
1456
  'rules',
@@ -1478,6 +1493,20 @@ export function validateAppConfig(value) {
1478
1493
  if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(string(policy.resourceCode))) {
1479
1494
  diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_RESOURCE_REQUIRED', 'data policy 必须声明唯一目标 resourceCode', `authz.dataPolicies[${index}].resourceCode`));
1480
1495
  }
1496
+ if (policy.publicRead !== undefined) {
1497
+ const resource = dataResources.map(object).find(resource => resource.code === policy.resourceCode);
1498
+ const baseline = roles.map(object).find(role => role.code === authenticatedUserRoleCode);
1499
+ try {
1500
+ validateAuthenticatedPublicRead({ policy, resource, baselineRoleCode: authenticatedUserRoleCode,
1501
+ baselineCapabilities: Array.isArray(baseline?.capabilities) ? baseline.capabilities : [],
1502
+ baselineDeniedCapabilities: Array.isArray(baseline?.deniedCapabilities) ? baseline.deniedCapabilities : [], pointer: policyPath });
1503
+ }
1504
+ catch (error) {
1505
+ if (!(error instanceof AuthenticatedPublicReadContractError))
1506
+ throw error;
1507
+ diagnostics.push(diagnostic(error.code, '认证公共投影必须闭合公共字段,所有非公开字段与历史继续拒绝基线成员', error.pointer));
1508
+ }
1509
+ }
1481
1510
  if (policy.unrestrictedRoleCodes !== undefined) {
1482
1511
  const unrestrictedRoleCodes = Array.isArray(policy.unrestrictedRoleCodes)
1483
1512
  ? policy.unrestrictedRoleCodes
@@ -1996,6 +2025,16 @@ export function validateAppConfig(value) {
1996
2025
  const field = string(trigger.field);
1997
2026
  const fieldType = eventResourceFields.get(resourceCode)?.get(field);
1998
2027
  const payload = object(trigger.payload);
2028
+ let projectionFields = [];
2029
+ let generatedSchema = applicationEventSchemas.get(string(trigger.eventType));
2030
+ try {
2031
+ projectionFields = dateEventProjectionFieldsV2(resourceCode, (Array.isArray(events.subscriptions) ? events.subscriptions : []).map(object)
2032
+ .filter(subscription => Array.isArray(subscription.eventTypes) && subscription.eventTypes.includes(trigger.eventType)), eventDataResources.map(object).find(resource => resource.code === resourceCode));
2033
+ generatedSchema = dateEventProjectionEnvelopeSchemaV2(generatedSchema, projectionFields);
2034
+ }
2035
+ catch (error) {
2036
+ diagnostics.push(diagnostic('APP_CONFIG_DATE_TRIGGER_PROJECTION_INVALID', `日期事件资料投影无效:${error instanceof Error ? error.message : 'invalid'}`, path));
2037
+ }
1999
2038
  let payloadBytes = Number.POSITIVE_INFINITY;
2000
2039
  try {
2001
2040
  payloadBytes = Buffer.byteLength(JSON.stringify(trigger.payload || {}), 'utf8');
@@ -2014,7 +2053,7 @@ export function validateAppConfig(value) {
2014
2053
  Array.isArray(trigger.payload) ||
2015
2054
  payloadBytes > 64 * 1024 ||
2016
2055
  Object.keys(payload).some(key => DATE_TRIGGER_RESERVED_DATA_FIELDS.has(key)) ||
2017
- !eventDataMatchesSchema(applicationEventSchemas.get(string(trigger.eventType)), {
2056
+ !eventDataMatchesSchema(generatedSchema, {
2018
2057
  ...payload,
2019
2058
  triggerCode: code,
2020
2059
  resourceCode,
@@ -2022,6 +2061,7 @@ export function validateAppConfig(value) {
2022
2061
  recordRevision: 1,
2023
2062
  field,
2024
2063
  dueAt: '2026-01-01T00:00:00.000Z',
2064
+ ...(projectionFields.length ? { projection: {} } : {}),
2025
2065
  })) {
2026
2066
  diagnostics.push(diagnostic('APP_CONFIG_DATE_TRIGGER_INVALID', 'dateTrigger 必须使用唯一 code、date/datetime 字段、十年内 ISO-8601 offset、已注册应用事件、无保留字段且完整生成数据满足 JSON Schema', path));
2027
2067
  }
@@ -2372,6 +2412,11 @@ export function validateAppConfig(value) {
2372
2412
  const property = workflowInputProperties[factKey];
2373
2413
  const propertyRecord = isRecord(property) ? property : {};
2374
2414
  const propertyType = string(propertyRecord.type);
2415
+ const nullableObject = Array.isArray(propertyRecord.type) &&
2416
+ propertyRecord.type.length === 2 &&
2417
+ new Set(propertyRecord.type).size === 2 &&
2418
+ propertyRecord.type.includes('object') &&
2419
+ propertyRecord.type.includes('null');
2375
2420
  const itemRecord = isRecord(propertyRecord.items)
2376
2421
  ? propertyRecord.items
2377
2422
  : {};
@@ -2384,20 +2429,20 @@ export function validateAppConfig(value) {
2384
2429
  ? 'array(items.type=array,items.items.type=object)'
2385
2430
  : fieldType.endsWith('.multiple')
2386
2431
  ? 'array(items.type=object)'
2387
- : 'object';
2432
+ : 'object 或 [object, null]';
2388
2433
  const shapeValid = fieldType === 'cascade.multiple'
2389
2434
  ? propertyType === 'array' &&
2390
2435
  itemType === 'array' &&
2391
2436
  nestedItemType === 'object'
2392
2437
  : fieldType.endsWith('.multiple')
2393
2438
  ? propertyType === 'array' && itemType === 'object'
2394
- : propertyType === 'object';
2439
+ : propertyType === 'object' || nullableObject;
2395
2440
  if (!shapeValid) {
2396
2441
  diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_FACT_PROJECTION_SHAPE_INVALID', `fact "${factKey}" 投影的字段 ${fieldCode} 是 ${fieldType} 快照字段;inputSchema.properties.${factKey} 必须声明 ${expected}`, `${path}.definition.inputSchema.properties.${factKey}`));
2397
2442
  }
2398
2443
  }
2399
2444
  }
2400
- for (const error of validateWorkflowDefinition(definition)) {
2445
+ for (const error of [...validateWorkflowDefinition(definition), ...validateWorkflowCommandHandlers(definition, object(config.backend).operations)]) {
2401
2446
  diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DEFINITION_INVALID', error, path));
2402
2447
  }
2403
2448
  const policyResource = object((Array.isArray(data.resources) ? data.resources : []).find(resource => object(resource).code === definition.subject?.resourceCode));
@@ -2474,6 +2519,18 @@ export function validateAppConfig(value) {
2474
2519
  for (const error of validateWorkflowBinding(definition, binding)) {
2475
2520
  diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_BINDING_INVALID', error, path));
2476
2521
  }
2522
+ try {
2523
+ const resource = (Array.isArray(data.resources) ? data.resources : []).map(object).find(item => item.code === definition.subject.resourceCode);
2524
+ const fields = object(resource?.schema).fields;
2525
+ validateWorkflowUserCandidateBindings(definition, binding, new Map((Array.isArray(fields) ? fields : []).map(item => [String(object(item).code), item])), `${path}.bindings`, new Map((Array.isArray(data.resources) ? data.resources : []).map(object).map(resource => [String(resource.code),
2526
+ new Map((Array.isArray(object(resource.schema).fields) ? object(resource.schema).fields : []).map(field => [field.code, field]))])));
2527
+ }
2528
+ catch (error) {
2529
+ if (!(error instanceof UserCandidateContractError))
2530
+ throw error;
2531
+ const issue = error;
2532
+ diagnostics.push(diagnostic(issue.code, '流程选人必须引用原人员字段及固定范围事实,办理页不能修改选人范围', issue.pointer));
2533
+ }
2477
2534
  activeRoutingBindings.push(binding);
2478
2535
  for (const entry of Object.values(binding.bindings || {})) {
2479
2536
  if (entry.provider === 'application_provider' &&
@@ -2771,16 +2828,54 @@ function validateBackendOperations(rawOperations, declaredResources, declaredRes
2771
2828
  'workflow',
2772
2829
  'roleAssertions',
2773
2830
  'decimalReservation',
2831
+ 'dataCommands',
2832
+ 'workflowStage',
2833
+ 'recordEdit',
2834
+ 'ownedSubject',
2774
2835
  ]);
2775
2836
  let invalid = !isRecord(operation.platformAccess) ||
2776
2837
  Object.keys(access).length === 0 ||
2777
2838
  Object.keys(access).some(key => !allowedAccessKeys.has(key));
2839
+ if (access.ownedSubject !== undefined) {
2840
+ try {
2841
+ assertWorkflowOwnedSubjectCreate(access.ownedSubject);
2842
+ }
2843
+ catch {
2844
+ invalid = true;
2845
+ }
2846
+ invalid ||= access.workflow === undefined || access.decimalReservation !== undefined;
2847
+ }
2848
+ if (access.dataCommands !== undefined) {
2849
+ const commands = object(access.dataCommands);
2850
+ invalid ||= commands.mode !== 'recoverable-native' || Object.keys(commands).some(key => key !== 'mode');
2851
+ }
2852
+ if (access.workflowStage !== undefined) {
2853
+ try {
2854
+ assertWorkflowNativeStagePolicy(access.workflowStage);
2855
+ }
2856
+ catch {
2857
+ invalid = true;
2858
+ }
2859
+ const stage = object(access.workflowStage);
2860
+ invalid ||= object(access.dataCommands).mode !== 'recoverable-native' || access.decimalReservation !== undefined ||
2861
+ !declaredResources.has(string(stage.resourceCode)) || !declaredWorkflowCodes.has(string(stage.workflowCode)) ||
2862
+ !Array.isArray(object(access.workflow).codes) || !object(access.workflow).codes.includes(stage.workflowCode);
2863
+ }
2864
+ if (access.recordEdit !== undefined) {
2865
+ const edit = object(access.recordEdit);
2866
+ const fields = Array.isArray(edit.fieldCodes) ? edit.fieldCodes : [];
2867
+ invalid ||= !isRecord(access.recordEdit) || !string(edit.resourceCode) || fields.length < 1 || fields.length > 200 ||
2868
+ new Set(fields).size !== fields.length || fields.some(field => !string(field)) ||
2869
+ Object.keys(edit).some(key => !['resourceCode', 'fieldCodes'].includes(key)) ||
2870
+ object(access.dataCommands).mode !== 'recoverable-native';
2871
+ }
2778
2872
  if (access.roleAssertions !== undefined) {
2779
2873
  const assertion = object(access.roleAssertions);
2780
2874
  const roles = Array.isArray(assertion.roleCodes) ? assertion.roleCodes : [];
2781
2875
  invalid ||= !isRecord(access.roleAssertions) || roles.length < 1 || roles.length > 20 ||
2782
2876
  new Set(roles).size !== roles.length || roles.some(role => !declaredRoleCodes.has(string(role))) ||
2783
- Object.keys(assertion).some(key => key !== 'roleCodes');
2877
+ Object.keys(assertion).some(key => !['roleCodes', 'actorAuthority'].includes(key)) ||
2878
+ (Object.hasOwn(assertion, 'actorAuthority') && assertion.actorAuthority !== true);
2784
2879
  }
2785
2880
  if (access.directory !== undefined) {
2786
2881
  const directory = object(access.directory);
@@ -2790,6 +2885,7 @@ function validateBackendOperations(rawOperations, declaredResources, declaredRes
2790
2885
  'employeeNumber',
2791
2886
  'primaryDepartment',
2792
2887
  'departments',
2888
+ ...(directory.mode === 'current-initiator' ? ['phone'] : []),
2793
2889
  ]);
2794
2890
  invalid ||=
2795
2891
  !['current-initiator', 'selected-user'].includes(String(directory.mode)) ||
@@ -2891,7 +2987,12 @@ function validateBackendOperations(rawOperations, declaredResources, declaredRes
2891
2987
  workflowCodes.length > 16 ||
2892
2988
  new Set(workflowCodes).size !== workflowCodes.length ||
2893
2989
  workflowCodes.some(code => !declaredWorkflowCodes.has(code)) ||
2894
- Object.keys(workflow).some(key => key !== 'codes');
2990
+ Object.keys(workflow).some(key => !['codes', 'businessCommands'].includes(key));
2991
+ if (workflow.businessCommands !== undefined) {
2992
+ const commands = workflow.businessCommands;
2993
+ invalid ||= !Array.isArray(commands) || commands.length === 0 || commands.length > 4 ||
2994
+ new Set(commands).size !== commands.length || commands.some(command => !['approve', 'reject', 'withdraw', 'resubmit'].includes(command));
2995
+ }
2895
2996
  }
2896
2997
  if (access.decimalReservation !== undefined) {
2897
2998
  const reservation = object(access.decimalReservation);
@@ -4226,7 +4327,10 @@ function validateCapabilityClosure(appCode, config, declarations, diagnostics) {
4226
4327
  if (code)
4227
4328
  catalogOwners.set(code, 'authz.capabilities');
4228
4329
  }
4229
- for (const [resourceIndex, rawResource] of (Array.isArray(data.resources) ? data.resources : []).entries()) {
4330
+ const resources = Array.isArray(data.resources) ? data.resources : [];
4331
+ // Register every real definition before inspecting field references. A field
4332
+ // may reference a later resource without claiming that resource's catalog.
4333
+ for (const rawResource of resources) {
4230
4334
  const resource = object(rawResource);
4231
4335
  const resourceOwner = `data.resources.${string(resource.code)}`;
4232
4336
  Object.values(object(resource.capabilities)).forEach(value => {
@@ -4234,18 +4338,42 @@ function validateCapabilityClosure(appCode, config, declarations, diagnostics) {
4234
4338
  registerCapabilityOwner(code, resourceOwner, catalogOwners, diagnostics);
4235
4339
  catalog.add(code);
4236
4340
  });
4341
+ }
4342
+ const definedCapabilities = new Set(catalog);
4343
+ for (const [resourceIndex, rawResource] of resources.entries()) {
4344
+ const resource = object(rawResource);
4345
+ const resourceOwner = `data.resources.${string(resource.code)}`;
4346
+ for (const mode of ['read', 'create']) {
4347
+ const list = object(resource.recordComments)[mode];
4348
+ for (const capability of Array.isArray(list) ? list : []) {
4349
+ references.push({ capability: string(capability), path: `data.resources[${resourceIndex}].recordComments.${mode}` });
4350
+ }
4351
+ }
4352
+ const printRead = object(resource.recordPrint).read;
4353
+ const recordDelete = object(resource.recordDeletion).delete;
4354
+ for (const capability of Array.isArray(recordDelete) ? recordDelete : []) {
4355
+ references.push({ capability: string(capability), path: `data.resources[${resourceIndex}].recordDeletion.delete` });
4356
+ }
4357
+ for (const capability of Array.isArray(printRead) ? printRead : []) {
4358
+ references.push({ capability: string(capability), path: `data.resources[${resourceIndex}].recordPrint.read` });
4359
+ }
4360
+ const historyRead = object(resource.workflowHistory).read;
4361
+ for (const capability of Array.isArray(historyRead) ? historyRead : []) {
4362
+ references.push({ capability: string(capability), path: `data.resources[${resourceIndex}].workflowHistory.read` });
4363
+ }
4237
4364
  Object.entries(object(resource.fieldPolicies)).forEach(([fieldCode, rawPolicy]) => {
4238
4365
  const policy = object(rawPolicy);
4239
4366
  for (const key of ['read', 'create', 'update']) {
4240
4367
  for (const value of Array.isArray(policy[key]) ? policy[key] : []) {
4241
4368
  const code = string(value);
4242
- if (isDataAuditMetadataField(fieldCode)) {
4243
- references.push({
4244
- capability: code,
4245
- path: `data.resources[${resourceIndex}].fieldPolicies.${fieldCode}.${key}`,
4246
- });
4369
+ references.push({
4370
+ capability: code,
4371
+ path: `data.resources[${resourceIndex}].fieldPolicies.${fieldCode}.${key}`,
4372
+ });
4373
+ if (isDataAuditMetadataField(fieldCode) || definedCapabilities.has(code))
4247
4374
  continue;
4248
- }
4375
+ // Preserve resource-owned implicit field capabilities. Reusing an
4376
+ // explicit/CRUD definition is different from two implicit owners.
4249
4377
  registerCapabilityOwner(code, resourceOwner, catalogOwners, diagnostics);
4250
4378
  catalog.add(code);
4251
4379
  }
@@ -4444,6 +4572,7 @@ export function materializeDataResource(appCode, declaration) {
4444
4572
  Boolean(field.filter || field.searchable || field.sortable),
4445
4573
  ...(field.options ? { options: field.options } : {}),
4446
4574
  ...(field.source ? { source: field.source } : {}),
4575
+ ...(field.userCandidates !== undefined ? { userCandidates: field.userCandidates } : {}),
4447
4576
  ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),
4448
4577
  ...(field.precision !== undefined ? { precision: field.precision } : {}),
4449
4578
  ...(field.scale !== undefined ? { scale: field.scale } : {}),
@@ -4525,6 +4654,7 @@ export function materializeDataResource(appCode, declaration) {
4525
4654
  ...(field.file?.accept ? { accept: field.file.accept } : {}),
4526
4655
  ...(field.options ? { options: field.options } : {}),
4527
4656
  ...(field.source ? { source: field.source } : {}),
4657
+ ...(field.userCandidates !== undefined ? { userCandidates: field.userCandidates } : {}),
4528
4658
  ...(field.timePrecision ? { timePrecision: field.timePrecision } : {}),
4529
4659
  ...(field.serial ? { serial: field.serial } : {}),
4530
4660
  ...(field.subtable ? { subtable: field.subtable } : {}),
@@ -4572,6 +4702,24 @@ export function materializeDataResource(appCode, declaration) {
4572
4702
  ...(declaration.invariants ? { invariants: declaration.invariants } : {}),
4573
4703
  ...(declaration.decimalReservationLifecycle !== undefined ? { decimalReservationLifecycle: declaration.decimalReservationLifecycle } : {}),
4574
4704
  ...(uniqueKeys !== undefined ? { uniqueKeys } : {}),
4705
+ ...(declaration.recordComments === undefined ? {} : { recordComments: {
4706
+ read: declaration.recordComments.read === false ? [] : declaration.recordComments.read === true
4707
+ ? [capabilities.read] : [...declaration.recordComments.read],
4708
+ create: declaration.recordComments.create === false ? [] : declaration.recordComments.create === true
4709
+ ? [capabilities.read] : [...declaration.recordComments.create],
4710
+ } }),
4711
+ ...(declaration.recordPrint === undefined ? {} : { recordPrint: {
4712
+ read: declaration.recordPrint.read === false ? [] : declaration.recordPrint.read === true
4713
+ ? [capabilities.read] : [...declaration.recordPrint.read],
4714
+ } }),
4715
+ ...(declaration.recordDeletion === undefined ? {} : { recordDeletion: {
4716
+ delete: declaration.recordDeletion.delete === false ? [] : declaration.recordDeletion.delete === true
4717
+ ? [capabilities.delete] : [...declaration.recordDeletion.delete],
4718
+ } }),
4719
+ ...(declaration.workflowHistory === undefined ? {} : { workflowHistory: {
4720
+ read: declaration.workflowHistory.read === false ? [] : declaration.workflowHistory.read === true
4721
+ ? [capabilities.read] : [...declaration.workflowHistory.read],
4722
+ } }),
4575
4723
  surface,
4576
4724
  capabilities,
4577
4725
  ...(declaration.dataPolicyCode
@@ -4621,6 +4769,10 @@ export function validateAppDeclaration(value) {
4621
4769
  'detailRouteCode',
4622
4770
  'userSurface',
4623
4771
  'audit',
4772
+ 'workflowHistory',
4773
+ 'recordPrint',
4774
+ 'recordComments',
4775
+ 'recordDeletion',
4624
4776
  ]);
4625
4777
  const fieldKeys = new Set([
4626
4778
  'code',
@@ -4630,6 +4782,7 @@ export function validateAppDeclaration(value) {
4630
4782
  'indexed',
4631
4783
  'options',
4632
4784
  'source',
4785
+ 'userCandidates',
4633
4786
  'maxLength',
4634
4787
  'precision',
4635
4788
  'scale',
@@ -4672,6 +4825,55 @@ export function validateAppDeclaration(value) {
4672
4825
  diagnostics.push(diagnostic('APP_CONFIG_DATA_AUDIT_READ_INVALID', 'audit.read 必须为 true(绑定资源读能力)、false 或不重复的非空 capability 数组(最多 20 项)', `${resourcePath}.audit`, `audit: { read: true } 会把审计读取绑定到本资源的 read 能力;需要更细粒度时写能力数组,例如 audit: { read: ['app:${appCode}:data:<resource>:read'] }`));
4673
4826
  }
4674
4827
  }
4828
+ let recordCommentsValid = true;
4829
+ if (resource.recordComments !== undefined) {
4830
+ const rule = object(resource.recordComments);
4831
+ if (!resource.recordComments || typeof resource.recordComments !== 'object' || Array.isArray(resource.recordComments) ||
4832
+ Object.keys(rule).some(key => !['read', 'create'].includes(key)) || ['read', 'create'].some(mode => {
4833
+ const list = rule[mode];
4834
+ return list !== false && list !== true && (!Array.isArray(list) || list.length < 1 || list.length > 20 ||
4835
+ list.some(value => typeof value !== 'string' || !value.trim()) || new Set(list).size !== list.length);
4836
+ })) {
4837
+ recordCommentsValid = false;
4838
+ diagnostics.push(diagnostic('APP_CONFIG_RECORD_COMMENTS_POLICY_INVALID', 'recordComments.read/create 必须为 true、false 或不重复的非空 capability 数组(最多20项)', `${resourcePath}.recordComments`, 'true绑定本资源read;每项操作的能力需由同一成员全部持有。'));
4839
+ }
4840
+ }
4841
+ let recordDeletionValid = true;
4842
+ if (resource.recordDeletion !== undefined) {
4843
+ const rule = object(resource.recordDeletion);
4844
+ const remove = rule.delete;
4845
+ if (!resource.recordDeletion || typeof resource.recordDeletion !== 'object' || Array.isArray(resource.recordDeletion) ||
4846
+ Object.keys(rule).some(key => key !== 'delete') ||
4847
+ (remove !== true && remove !== false && (!Array.isArray(remove) || remove.length < 1 || remove.length > 20 ||
4848
+ remove.some(value => typeof value !== 'string' || !value.trim()) || new Set(remove).size !== remove.length))) {
4849
+ recordDeletionValid = false;
4850
+ diagnostics.push(diagnostic('APP_CONFIG_RECORD_DELETION_DELETE_INVALID', 'recordDeletion.delete 必须为 true、false 或不重复的非空 capability 数组(最多20项)', `${resourcePath}.recordDeletion`, 'true绑定本资源delete;同一成员需同时有资料read、delete及全部维护能力。'));
4851
+ }
4852
+ }
4853
+ let recordPrintValid = true;
4854
+ if (resource.recordPrint !== undefined) {
4855
+ const rule = object(resource.recordPrint);
4856
+ const read = rule.read;
4857
+ if (!resource.recordPrint || typeof resource.recordPrint !== 'object' || Array.isArray(resource.recordPrint) ||
4858
+ Object.keys(rule).some(key => key !== 'read') ||
4859
+ (read !== true && read !== false && (!Array.isArray(read) || read.length < 1 || read.length > 20 ||
4860
+ read.some(value => typeof value !== 'string' || !value.trim()) || new Set(read).size !== read.length))) {
4861
+ recordPrintValid = false;
4862
+ diagnostics.push(diagnostic('APP_CONFIG_RECORD_PRINT_READ_INVALID', 'recordPrint.read 必须为 true、false 或不重复的非空 capability 数组(最多 20 项)', `${resourcePath}.recordPrint`, 'true 绑定本资源 read;独立能力数组按同一成员全部持有校验。'));
4863
+ }
4864
+ }
4865
+ let workflowHistoryValid = true;
4866
+ if (resource.workflowHistory !== undefined) {
4867
+ const rule = object(resource.workflowHistory);
4868
+ const read = rule.read;
4869
+ if (!resource.workflowHistory || typeof resource.workflowHistory !== 'object' || Array.isArray(resource.workflowHistory) ||
4870
+ Object.keys(rule).some(key => key !== 'read') ||
4871
+ (read !== true && read !== false && (!Array.isArray(read) || read.length < 1 || read.length > 20 ||
4872
+ read.some(value => typeof value !== 'string' || !value.trim()) || new Set(read).size !== read.length))) {
4873
+ workflowHistoryValid = false;
4874
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_HISTORY_READ_INVALID', 'workflowHistory.read 必须为 true、false 或不重复的非空 capability 数组(最多 20 项)', `${resourcePath}.workflowHistory`, 'true 绑定本资源 read;独立能力数组按同一成员全部持有校验。'));
4875
+ }
4876
+ }
4675
4877
  const userSurface = resource.userSurface;
4676
4878
  if (userSurface !== undefined) {
4677
4879
  const surface = object(userSurface);
@@ -4708,7 +4910,7 @@ export function validateAppDeclaration(value) {
4708
4910
  return;
4709
4911
  }
4710
4912
  const declaredCodes = new Set();
4711
- let materializable = auditValid;
4913
+ let materializable = auditValid && workflowHistoryValid && recordPrintValid && recordCommentsValid && recordDeletionValid;
4712
4914
  fields.forEach((rawField, fieldIndex) => {
4713
4915
  const field = object(rawField);
4714
4916
  const fieldPath = `${resourcePath}.fields[${fieldIndex}]`;
@@ -4894,6 +5096,11 @@ export function validateAppDeclaration(value) {
4894
5096
  return;
4895
5097
  const path = `data.resources[${declaration.resourceIndex}].fields[${fieldIndex}].subtable`;
4896
5098
  const subtable = object(field.subtable);
5099
+ if (subtable.minRows !== undefined &&
5100
+ (!Number.isSafeInteger(subtable.minRows) || Number(subtable.minRows) < 0 ||
5101
+ Number(subtable.minRows) > Number(subtable.maxRows ?? 20))) {
5102
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_SUBTABLE_MIN_ROWS_INVALID', '子表 minRows 必须是非负整数且不能超过 maxRows(默认20)', `${path}.minRows`));
5103
+ }
4897
5104
  const targetCode = string(subtable.resourceCode);
4898
5105
  const target = declaredResources.get(targetCode);
4899
5106
  if (!target || targetCode === resourceCode) {