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.
- package/dist/application-services.d.ts +1 -0
- package/dist/application-services.d.ts.map +1 -1
- package/dist/application-services.js +32 -6
- package/dist/application-services.js.map +1 -1
- package/dist/compiler/ai-catalog.d.ts.map +1 -1
- package/dist/compiler/ai-catalog.js +2 -10
- package/dist/compiler/ai-catalog.js.map +1 -1
- package/dist/compiler/application-model.d.ts +5 -1
- package/dist/compiler/application-model.d.ts.map +1 -1
- package/dist/compiler/application-model.js +5 -1
- package/dist/compiler/application-model.js.map +1 -1
- package/dist/compiler/bundle.d.ts.map +1 -1
- package/dist/compiler/bundle.js +107 -32
- package/dist/compiler/bundle.js.map +1 -1
- package/dist/compiler/config.d.ts +16 -1
- package/dist/compiler/config.d.ts.map +1 -1
- package/dist/compiler/config.js +231 -24
- package/dist/compiler/config.js.map +1 -1
- package/dist/compiler/permission-review.d.ts.map +1 -1
- package/dist/compiler/schema-composition.js +3 -0
- package/dist/compiler/schema-composition.js.map +1 -1
- package/dist/connected-development-vite.d.ts +15 -0
- package/dist/connected-development-vite.d.ts.map +1 -0
- package/dist/connected-development-vite.js +27 -0
- package/dist/connected-development-vite.js.map +1 -0
- package/dist/connected-development.d.ts +5 -0
- package/dist/connected-development.d.ts.map +1 -1
- package/dist/connected-development.js +105 -27
- package/dist/connected-development.js.map +1 -1
- package/dist/control-plane-client.d.ts +8 -0
- package/dist/control-plane-client.d.ts.map +1 -1
- package/dist/control-plane-client.js +36 -0
- package/dist/control-plane-client.js.map +1 -1
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +6 -1
- package/dist/deployment.js.map +1 -1
- package/dist/development-backend-relay.d.ts +28 -0
- package/dist/development-backend-relay.d.ts.map +1 -0
- package/dist/development-backend-relay.js +184 -0
- package/dist/development-backend-relay.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/workflow.d.ts.map +1 -1
- package/dist/internal/workflow.js +22 -7
- package/dist/internal/workflow.js.map +1 -1
- package/dist/workspace-loader.js +1 -1
- package/dist/workspace-loader.js.map +1 -1
- package/package.json +2 -2
- package/templates/backend/Dockerfile +1 -0
package/dist/compiler/config.js
CHANGED
|
@@ -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(
|
|
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 =>
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
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) {
|