openxiangda-devkit-core 2.0.0-alpha.52 → 2.0.0-alpha.56
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 +0 -5
- package/dist/application-services.d.ts.map +1 -1
- package/dist/application-services.js +0 -5
- package/dist/application-services.js.map +1 -1
- package/dist/compiler/ai-catalog.d.ts.map +1 -1
- package/dist/compiler/ai-catalog.js +76 -20
- package/dist/compiler/ai-catalog.js.map +1 -1
- package/dist/compiler/bundle.d.ts.map +1 -1
- package/dist/compiler/bundle.js +259 -60
- package/dist/compiler/bundle.js.map +1 -1
- package/dist/compiler/config.d.ts +32 -5
- package/dist/compiler/config.d.ts.map +1 -1
- package/dist/compiler/config.js +555 -46
- package/dist/compiler/config.js.map +1 -1
- package/dist/compiler/field-codec.d.ts +7 -0
- package/dist/compiler/field-codec.d.ts.map +1 -0
- package/dist/compiler/field-codec.js +124 -0
- package/dist/compiler/field-codec.js.map +1 -0
- package/dist/compiler/field-physical-plan.d.ts +9 -0
- package/dist/compiler/field-physical-plan.d.ts.map +1 -0
- package/dist/compiler/field-physical-plan.js +59 -0
- package/dist/compiler/field-physical-plan.js.map +1 -0
- package/dist/compiler/field-policy-path.d.ts +10 -0
- package/dist/compiler/field-policy-path.d.ts.map +1 -0
- package/dist/compiler/field-policy-path.js +264 -0
- package/dist/compiler/field-policy-path.js.map +1 -0
- package/dist/compiler/field-query-plan.d.ts +13 -0
- package/dist/compiler/field-query-plan.d.ts.map +1 -0
- package/dist/compiler/field-query-plan.js +122 -0
- package/dist/compiler/field-query-plan.js.map +1 -0
- package/dist/compiler/field-surface.d.ts +5 -5
- package/dist/compiler/field-surface.d.ts.map +1 -1
- package/dist/compiler/field-surface.js +41 -24
- package/dist/compiler/field-surface.js.map +1 -1
- package/dist/compiler/package-compiler.d.ts.map +1 -1
- package/dist/compiler/package-compiler.js +4 -1
- package/dist/compiler/package-compiler.js.map +1 -1
- package/dist/control-plane-client.d.ts +8 -30
- package/dist/control-plane-client.d.ts.map +1 -1
- package/dist/control-plane-client.js +6 -11
- package/dist/control-plane-client.js.map +1 -1
- package/dist/testing.d.ts +39 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +169 -0
- package/dist/testing.js.map +1 -1
- package/package.json +3 -2
package/dist/compiler/config.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import { SCHEMA_VERSIONS, DATA_FIELD_TYPES, nativePlatformCapabilityCatalog, validateDataResource, } from 'openxiangda-contracts';
|
|
1
|
+
import { SCHEMA_VERSIONS, DATA_FIELD_TYPES, PLATFORM_EVENT_TYPES_V2, nativePlatformCapabilityCatalog, validateDataResource, } from 'openxiangda-contracts';
|
|
2
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
2
3
|
import { CronExpressionParser } from 'cron-parser';
|
|
3
|
-
import {
|
|
4
|
+
import { fieldNullable, isGeneratedField } from './field-codec.js';
|
|
5
|
+
import { supportsFilter, supportsSearch, supportsSort, } from './field-query-plan.js';
|
|
6
|
+
import { physicalPlanForField } from './field-physical-plan.js';
|
|
7
|
+
import { SEMANTIC_FIELD_PATH_PATTERN, semanticFieldPathRoot, validateAuthzSemanticBindings, } from './field-policy-path.js';
|
|
8
|
+
import { compatibleWidgets, isCompatibleFieldWidget, resolveDataFieldSurfaceWidget, } from './field-surface.js';
|
|
4
9
|
// The compiler owns the application source contract. Devkit and every adapter
|
|
5
10
|
// re-export this module instead of maintaining separate configuration models.
|
|
6
11
|
import { validateWorkflowBinding, validateWorkflowDefinition, } from '../internal/workflow.js';
|
|
12
|
+
const DATE_TRIGGER_RESERVED_DATA_FIELDS = new Set([
|
|
13
|
+
'triggerCode',
|
|
14
|
+
'resourceCode',
|
|
15
|
+
'recordId',
|
|
16
|
+
'recordRevision',
|
|
17
|
+
'field',
|
|
18
|
+
'dueAt',
|
|
19
|
+
]);
|
|
7
20
|
const APP_DATA_FIELD_TYPES = new Set(DATA_FIELD_TYPES);
|
|
8
21
|
export const openXiangdaAppConfigSchema = {
|
|
9
22
|
$id: 'openxiangda.app-config/v3',
|
|
@@ -150,6 +163,11 @@ export const openXiangdaAppConfigSchema = {
|
|
|
150
163
|
additionalProperties: false,
|
|
151
164
|
required: ['subscriptions'],
|
|
152
165
|
properties: {
|
|
166
|
+
schemas: {
|
|
167
|
+
type: 'array',
|
|
168
|
+
maxItems: 100,
|
|
169
|
+
items: { type: 'object' },
|
|
170
|
+
},
|
|
153
171
|
subscriptions: {
|
|
154
172
|
type: 'array',
|
|
155
173
|
maxItems: 100,
|
|
@@ -160,6 +178,11 @@ export const openXiangdaAppConfigSchema = {
|
|
|
160
178
|
maxItems: 100,
|
|
161
179
|
items: { type: 'object' },
|
|
162
180
|
},
|
|
181
|
+
dateTriggers: {
|
|
182
|
+
type: 'array',
|
|
183
|
+
maxItems: 100,
|
|
184
|
+
items: { type: 'object' },
|
|
185
|
+
},
|
|
163
186
|
},
|
|
164
187
|
},
|
|
165
188
|
workflows: {
|
|
@@ -235,6 +258,7 @@ export function backendRuntimeRequired(config) {
|
|
|
235
258
|
backend.secrets?.length ||
|
|
236
259
|
config.events?.subscriptions?.length ||
|
|
237
260
|
config.events?.timers?.length ||
|
|
261
|
+
config.events?.dateTriggers?.length ||
|
|
238
262
|
config.workflows?.activations?.length ||
|
|
239
263
|
config.workflows?.providers?.length)));
|
|
240
264
|
}
|
|
@@ -246,6 +270,109 @@ function object(value) {
|
|
|
246
270
|
function string(value) {
|
|
247
271
|
return typeof value === 'string' ? value.trim() : '';
|
|
248
272
|
}
|
|
273
|
+
function eventSchemaDeclaresPath(schema, path) {
|
|
274
|
+
let current = schema;
|
|
275
|
+
for (const segment of path.split('.')) {
|
|
276
|
+
if (current.type === 'array')
|
|
277
|
+
current = object(current.items);
|
|
278
|
+
const properties = object(current.properties);
|
|
279
|
+
if (!Object.prototype.hasOwnProperty.call(properties, segment)) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
current = object(properties[segment]);
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
function eventDataMatchesSchema(schema, data) {
|
|
287
|
+
if (!schema)
|
|
288
|
+
return false;
|
|
289
|
+
try {
|
|
290
|
+
const validator = new Ajv2020({
|
|
291
|
+
allErrors: true,
|
|
292
|
+
strict: false,
|
|
293
|
+
validateFormats: false,
|
|
294
|
+
}).compile(schema);
|
|
295
|
+
return Boolean(validator(data));
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function validDateTriggerOffset(value) {
|
|
302
|
+
const match = /^([+-])?P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/.exec(string(value));
|
|
303
|
+
if (!match || !match.slice(2).some(part => part !== undefined))
|
|
304
|
+
return false;
|
|
305
|
+
const milliseconds = Number(match[2] || 0) * 24 * 60 * 60 * 1000 +
|
|
306
|
+
Number(match[3] || 0) * 60 * 60 * 1000 +
|
|
307
|
+
Number(match[4] || 0) * 60 * 1000 +
|
|
308
|
+
Number(match[5] || 0) * 1000;
|
|
309
|
+
return (Number.isFinite(milliseconds) &&
|
|
310
|
+
milliseconds <= 10 * 365 * 24 * 60 * 60 * 1000);
|
|
311
|
+
}
|
|
312
|
+
const EVENT_TYPE_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9_-]*)+\.v[1-9][0-9]*$/;
|
|
313
|
+
const EVENT_DATA_SCHEMA_VERSION_PATTERN = /^[1-9][0-9]*\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/;
|
|
314
|
+
const EVENT_FIELD_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
315
|
+
function eventPredicateValid(value) {
|
|
316
|
+
const predicate = object(value);
|
|
317
|
+
const keys = Object.keys(predicate);
|
|
318
|
+
if (keys.length !== 1 ||
|
|
319
|
+
!['eq', 'ne', 'in', 'notIn', 'exists'].includes(keys[0] || '')) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
const operand = predicate[keys[0]];
|
|
323
|
+
if (keys[0] === 'exists' && typeof operand !== 'boolean')
|
|
324
|
+
return false;
|
|
325
|
+
if ((keys[0] === 'in' || keys[0] === 'notIn') &&
|
|
326
|
+
(!Array.isArray(operand) || operand.length < 1 || operand.length > 20))
|
|
327
|
+
return false;
|
|
328
|
+
try {
|
|
329
|
+
const serialized = JSON.stringify(predicate);
|
|
330
|
+
return (typeof serialized === 'string' &&
|
|
331
|
+
Buffer.byteLength(serialized, 'utf8') <= 4096);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function validateEventChangeFilter(value, path, allowedFields, diagnostics) {
|
|
338
|
+
const change = object(value);
|
|
339
|
+
const field = string(change.field);
|
|
340
|
+
const keys = Object.keys(change);
|
|
341
|
+
if (!EVENT_FIELD_PATTERN.test(field) ||
|
|
342
|
+
!allowedFields.has(field) ||
|
|
343
|
+
keys.some(key => !['field', 'before', 'after'].includes(key)) ||
|
|
344
|
+
(!('before' in change) && !('after' in change)) ||
|
|
345
|
+
('before' in change && !eventPredicateValid(change.before)) ||
|
|
346
|
+
('after' in change && !eventPredicateValid(change.after))) {
|
|
347
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_CHANGE_FILTER_INVALID', '字段变化条件必须引用已声明字段,并且 before/after 各只使用一个受支持操作符', path));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function validateEventFilterCondition(value, path, allowedFields, diagnostics, state, depth = 1) {
|
|
351
|
+
const condition = object(value);
|
|
352
|
+
const keys = Object.keys(condition);
|
|
353
|
+
if (depth > 3 || keys.length !== 1) {
|
|
354
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_COMPLEXITY_EXCEEDED', '事件过滤条件最多嵌套 3 层,且每层只能声明 change/all/any/not 之一', path));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const key = keys[0];
|
|
358
|
+
if (key === 'change') {
|
|
359
|
+
state.atoms += 1;
|
|
360
|
+
validateEventChangeFilter(condition.change, `${path}.change`, allowedFields, diagnostics);
|
|
361
|
+
}
|
|
362
|
+
else if (key === 'all' || key === 'any') {
|
|
363
|
+
const items = Array.isArray(condition[key]) ? condition[key] : [];
|
|
364
|
+
if (items.length < 1 || items.length > 16) {
|
|
365
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_GROUP_INVALID', '事件过滤 all/any 必须包含 1 到 16 个条件', `${path}.${key}`));
|
|
366
|
+
}
|
|
367
|
+
items.forEach((item, index) => validateEventFilterCondition(item, `${path}.${key}[${index}]`, allowedFields, diagnostics, state, depth + 1));
|
|
368
|
+
}
|
|
369
|
+
else if (key === 'not') {
|
|
370
|
+
validateEventFilterCondition(condition.not, `${path}.not`, allowedFields, diagnostics, state, depth + 1);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_OPERATOR_INVALID', '事件过滤条件只支持 change/all/any/not', path));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
249
376
|
export function validateAppConfig(value) {
|
|
250
377
|
const diagnostics = [];
|
|
251
378
|
const config = object(value);
|
|
@@ -318,6 +445,8 @@ export function validateAppConfig(value) {
|
|
|
318
445
|
Array.isArray(eventConfig.subscriptions) &&
|
|
319
446
|
eventConfig.subscriptions.length > 0 ||
|
|
320
447
|
Array.isArray(eventConfig.timers) && eventConfig.timers.length > 0 ||
|
|
448
|
+
Array.isArray(eventConfig.dateTriggers) &&
|
|
449
|
+
eventConfig.dateTriggers.length > 0 ||
|
|
321
450
|
Array.isArray(workflowConfig.activations) &&
|
|
322
451
|
workflowConfig.activations.length > 0 ||
|
|
323
452
|
Array.isArray(workflowConfig.providers) && workflowConfig.providers.length > 0)) {
|
|
@@ -379,22 +508,54 @@ export function validateAppConfig(value) {
|
|
|
379
508
|
const fields = object(resourceRecord.schema).fields;
|
|
380
509
|
(Array.isArray(fields) ? fields : []).forEach((rawField, fieldIndex) => {
|
|
381
510
|
const field = object(rawField);
|
|
382
|
-
const
|
|
383
|
-
if (string(
|
|
511
|
+
const source = object(field.source);
|
|
512
|
+
if (string(source.kind) !== 'resource')
|
|
384
513
|
return;
|
|
385
|
-
const targetCode = string(
|
|
514
|
+
const targetCode = string(source.resourceCode);
|
|
386
515
|
const target = resources.find(candidate => string(object(candidate).code) === targetCode);
|
|
387
|
-
const
|
|
516
|
+
const sourcePath = `data.resources[${index}].schema.fields[${fieldIndex}].source`;
|
|
388
517
|
if (!target) {
|
|
389
|
-
diagnostics.push(diagnostic('
|
|
518
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_TARGET_NOT_FOUND', `动态选项来源 ${targetCode} 必须在同一应用中声明`, `${sourcePath}.resourceCode`));
|
|
390
519
|
return;
|
|
391
520
|
}
|
|
392
521
|
const targetFields = object(object(target).schema).fields;
|
|
393
|
-
const
|
|
522
|
+
const targetFieldList = Array.isArray(targetFields) ? targetFields : [];
|
|
523
|
+
const labelField = string(source.labelField);
|
|
394
524
|
const label = (Array.isArray(targetFields) ? targetFields : []).find(candidate => string(object(candidate).code) === labelField);
|
|
395
|
-
if (!label ||
|
|
396
|
-
|
|
525
|
+
if (!label ||
|
|
526
|
+
!['text.short', 'text.long', 'serial-number'].includes(string(object(label).type))) {
|
|
527
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_LABEL_INVALID', `动态选项标签字段 ${labelField} 必须是文本或流水号`, `${sourcePath}.labelField`));
|
|
397
528
|
}
|
|
529
|
+
for (const key of [
|
|
530
|
+
'searchFields',
|
|
531
|
+
'descriptionFields',
|
|
532
|
+
'snapshotFields',
|
|
533
|
+
]) {
|
|
534
|
+
const values = Array.isArray(source[key]) ? source[key] : [];
|
|
535
|
+
values.forEach((value, valueIndex) => {
|
|
536
|
+
const targetField = targetFieldList.find(candidate => string(object(candidate).code) === string(value));
|
|
537
|
+
if (!targetField) {
|
|
538
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_FIELD_NOT_FOUND', `动态来源字段 ${String(value)} 未在 ${targetCode} 声明`, `${sourcePath}.${key}[${valueIndex}]`));
|
|
539
|
+
}
|
|
540
|
+
else if (key === 'searchFields' &&
|
|
541
|
+
!supportsSearch(string(object(targetField).type))) {
|
|
542
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_SEARCH_UNSUPPORTED', `动态来源字段 ${String(value)} 不支持搜索`, `${sourcePath}.${key}[${valueIndex}]`));
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
const sourceFilters = Array.isArray(source.filters) ? source.filters : [];
|
|
547
|
+
sourceFilters.forEach((rawFilter, filterIndex) => {
|
|
548
|
+
const filter = object(rawFilter);
|
|
549
|
+
const targetFieldCode = string(filter.field);
|
|
550
|
+
if (!targetFieldList.some(candidate => string(object(candidate).code) === targetFieldCode)) {
|
|
551
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_FILTER_FIELD_NOT_FOUND', `动态来源筛选字段 ${targetFieldCode} 未在 ${targetCode} 声明`, `${sourcePath}.filters[${filterIndex}].field`));
|
|
552
|
+
}
|
|
553
|
+
const bindingField = string(object(filter.binding).field);
|
|
554
|
+
if (bindingField &&
|
|
555
|
+
!(Array.isArray(fields) ? fields : []).some(candidate => string(object(candidate).code) === bindingField)) {
|
|
556
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_BINDING_FIELD_NOT_FOUND', `动态来源绑定字段 ${bindingField} 未在当前资源声明`, `${sourcePath}.filters[${filterIndex}].binding.field`));
|
|
557
|
+
}
|
|
558
|
+
});
|
|
398
559
|
});
|
|
399
560
|
});
|
|
400
561
|
}
|
|
@@ -514,10 +675,10 @@ export function validateAppConfig(value) {
|
|
|
514
675
|
if (!declaredFieldTypes) {
|
|
515
676
|
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_DIMENSION_VALUE_SOURCE_RESOURCE_INVALID', 'native_resource valueSource 必须引用同一应用内已声明的 Data Resource', `${valueSourcePath}.resourceCode`));
|
|
516
677
|
}
|
|
517
|
-
else if (!['
|
|
678
|
+
else if (!['text.short', 'text.long', 'serial-number'].includes(declaredFieldTypes.get(labelField) || '') ||
|
|
518
679
|
(enabledField !== undefined &&
|
|
519
680
|
declaredFieldTypes.get(enabledField) !== 'boolean')) {
|
|
520
|
-
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_DIMENSION_VALUE_SOURCE_FIELD_INVALID', 'valueSource
|
|
681
|
+
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_DIMENSION_VALUE_SOURCE_FIELD_INVALID', 'valueSource 标签字段必须为文本/流水号,启用字段必须为 boolean', valueSourcePath));
|
|
521
682
|
}
|
|
522
683
|
}
|
|
523
684
|
dimensionCodes.add(code);
|
|
@@ -541,7 +702,7 @@ export function validateAppConfig(value) {
|
|
|
541
702
|
!string(source.name) ||
|
|
542
703
|
!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(resourceCode) ||
|
|
543
704
|
!['user', 'role_membership'].includes(subjectType) ||
|
|
544
|
-
|
|
705
|
+
!SEMANTIC_FIELD_PATH_PATTERN.test(string(subject.userIdField)) ||
|
|
545
706
|
(subjectType === 'role_membership' && !roleCodes.has(roleCode)) ||
|
|
546
707
|
(subjectType === 'user' && Boolean(roleCode)) ||
|
|
547
708
|
grants.length === 0 ||
|
|
@@ -554,9 +715,9 @@ export function validateAppConfig(value) {
|
|
|
554
715
|
const dimensionCode = string(grant.dimensionCode);
|
|
555
716
|
if (!dimensionCodes.has(dimensionCode) ||
|
|
556
717
|
grantDimensions.has(dimensionCode) ||
|
|
557
|
-
|
|
718
|
+
!SEMANTIC_FIELD_PATH_PATTERN.test(string(grant.valueField)) ||
|
|
558
719
|
(grant.parentValueField !== undefined &&
|
|
559
|
-
|
|
720
|
+
!SEMANTIC_FIELD_PATH_PATTERN.test(string(grant.parentValueField)))) {
|
|
560
721
|
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_SCOPE_SOURCE_GRANT_INVALID', 'scope source grant 必须引用唯一已声明维度及有效字段', `${sourcePath}.grants[${grantIndex}]`));
|
|
561
722
|
}
|
|
562
723
|
grantDimensions.add(dimensionCode);
|
|
@@ -593,7 +754,7 @@ export function validateAppConfig(value) {
|
|
|
593
754
|
.map(string),
|
|
594
755
|
];
|
|
595
756
|
if (!declaredFields ||
|
|
596
|
-
referencedFields.some(field => !declaredFields.has(field))) {
|
|
757
|
+
referencedFields.some(field => !declaredFields.has(semanticFieldPathRoot(field)))) {
|
|
597
758
|
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_SCOPE_SOURCE_RESOURCE_INVALID', 'scope source 必须引用已声明 Data Resource 及其真实字段', sourcePath));
|
|
598
759
|
}
|
|
599
760
|
sourceCodes.add(code);
|
|
@@ -611,6 +772,9 @@ export function validateAppConfig(value) {
|
|
|
611
772
|
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_INVALID', 'data policy 必须声明唯一 code、name、matchMode 和至少一条规则', `authz.dataPolicies[${index}]`));
|
|
612
773
|
}
|
|
613
774
|
policyCodes.add(code);
|
|
775
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(string(policy.resourceCode))) {
|
|
776
|
+
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_RESOURCE_REQUIRED', 'data policy 必须声明唯一目标 resourceCode', `authz.dataPolicies[${index}].resourceCode`));
|
|
777
|
+
}
|
|
614
778
|
if (policy.unrestrictedRoleCodes !== undefined) {
|
|
615
779
|
const unrestrictedRoleCodes = Array.isArray(policy.unrestrictedRoleCodes)
|
|
616
780
|
? policy.unrestrictedRoleCodes
|
|
@@ -683,10 +847,80 @@ export function validateAppConfig(value) {
|
|
|
683
847
|
diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_DATA_POLICY_NOT_FOUND', `DataResource 引用了未声明的数据策略: ${policyCode}`, `data.resources[${index}].dataPolicyCode`));
|
|
684
848
|
}
|
|
685
849
|
});
|
|
850
|
+
diagnostics.push(...validateAuthzSemanticBindings({
|
|
851
|
+
resources: dataResources,
|
|
852
|
+
dimensions,
|
|
853
|
+
scopeSources,
|
|
854
|
+
policies,
|
|
855
|
+
}));
|
|
686
856
|
validateAuthorizationTransitions(authz.authorizationTransitions, roleCodes, diagnostics);
|
|
687
857
|
}
|
|
688
858
|
validateCapabilityClosure(appCode, config, declaredCapabilities, diagnostics);
|
|
689
859
|
const events = object(config.events);
|
|
860
|
+
const eventDataResources = Array.isArray(object(config.data).resources)
|
|
861
|
+
? object(config.data).resources
|
|
862
|
+
: [];
|
|
863
|
+
const eventResourceFields = new Map(eventDataResources.map(rawResource => {
|
|
864
|
+
const resource = object(rawResource);
|
|
865
|
+
const fields = Array.isArray(object(resource.schema).fields)
|
|
866
|
+
? object(resource.schema).fields
|
|
867
|
+
: [];
|
|
868
|
+
return [
|
|
869
|
+
string(resource.code),
|
|
870
|
+
new Map(fields.map(rawField => {
|
|
871
|
+
const field = object(rawField);
|
|
872
|
+
return [string(field.code), string(field.type)];
|
|
873
|
+
})),
|
|
874
|
+
];
|
|
875
|
+
}));
|
|
876
|
+
const platformEventTypes = new Set(PLATFORM_EVENT_TYPES_V2);
|
|
877
|
+
const declaredEventTypes = new Set();
|
|
878
|
+
const applicationEventSchemas = new Map();
|
|
879
|
+
const schemaKeys = new Set();
|
|
880
|
+
(Array.isArray(events.schemas) ? events.schemas : []).forEach((item, index) => {
|
|
881
|
+
const schema = object(item);
|
|
882
|
+
const path = `events.schemas[${index}]`;
|
|
883
|
+
const eventType = string(schema.eventType);
|
|
884
|
+
const dataSchemaVersion = string(schema.dataSchemaVersion);
|
|
885
|
+
const jsonSchema = object(schema.jsonSchema);
|
|
886
|
+
const sensitiveFields = Array.isArray(schema.sensitiveFields)
|
|
887
|
+
? schema.sensitiveFields.map(value => string(value))
|
|
888
|
+
: [];
|
|
889
|
+
const sensitiveFieldsValid = (schema.sensitiveFields === undefined ||
|
|
890
|
+
Array.isArray(schema.sensitiveFields)) &&
|
|
891
|
+
sensitiveFields.length <= 100 &&
|
|
892
|
+
new Set(sensitiveFields).size === sensitiveFields.length &&
|
|
893
|
+
sensitiveFields.every(field => /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*){0,7}$/.test(field) && eventSchemaDeclaresPath(jsonSchema, field));
|
|
894
|
+
const schemaKey = `${eventType}:${dataSchemaVersion}`;
|
|
895
|
+
let schemaBytes = Number.POSITIVE_INFINITY;
|
|
896
|
+
try {
|
|
897
|
+
schemaBytes = Buffer.byteLength(JSON.stringify(schema.jsonSchema), 'utf8');
|
|
898
|
+
}
|
|
899
|
+
catch {
|
|
900
|
+
// Reported by the combined validation below.
|
|
901
|
+
}
|
|
902
|
+
const schemaValid = !(!EVENT_TYPE_PATTERN.test(eventType) ||
|
|
903
|
+
eventType.startsWith('openxiangda.') ||
|
|
904
|
+
!eventType.startsWith(`${appCode}.`) ||
|
|
905
|
+
!EVENT_DATA_SCHEMA_VERSION_PATTERN.test(dataSchemaVersion) ||
|
|
906
|
+
object(schema.jsonSchema) !== schema.jsonSchema ||
|
|
907
|
+
jsonSchema.type !== 'object' ||
|
|
908
|
+
schemaBytes > 256 * 1024 ||
|
|
909
|
+
!sensitiveFieldsValid ||
|
|
910
|
+
schemaKeys.has(schemaKey) ||
|
|
911
|
+
declaredEventTypes.has(eventType));
|
|
912
|
+
if (!schemaValid) {
|
|
913
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_SCHEMA_INVALID', '应用事件 Schema 必须使用 appCode 命名空间、major 事件类型、SemVer dataSchemaVersion、不超过 256 KiB 的 object JSON Schema;sensitiveFields 最多 100 个、路径规范且必须指向已声明字段,且版本不可重复', path));
|
|
914
|
+
}
|
|
915
|
+
if (schemaValid) {
|
|
916
|
+
declaredEventTypes.add(eventType);
|
|
917
|
+
applicationEventSchemas.set(eventType, jsonSchema);
|
|
918
|
+
}
|
|
919
|
+
schemaKeys.add(schemaKey);
|
|
920
|
+
});
|
|
921
|
+
const allowedEventTypes = new Set([...platformEventTypes, ...declaredEventTypes]);
|
|
922
|
+
const captureFieldsByResource = new Map();
|
|
923
|
+
const subscriptionCodes = new Set();
|
|
690
924
|
if (config.events !== undefined && !Array.isArray(events.subscriptions)) {
|
|
691
925
|
diagnostics.push(diagnostic('APP_CONFIG_EVENT_SUBSCRIPTIONS_REQUIRED', 'events.subscriptions 必须是数组', 'events.subscriptions'));
|
|
692
926
|
}
|
|
@@ -697,34 +931,154 @@ export function validateAppConfig(value) {
|
|
|
697
931
|
if ('environmentKey' in subscription) {
|
|
698
932
|
diagnostics.push(diagnostic('APP_CONFIG_ENVIRONMENT_OVERRIDE_FORBIDDEN', '事件声明属于环境中立 AppVersion,不能声明 environmentKey', `${path}.environmentKey`));
|
|
699
933
|
}
|
|
700
|
-
|
|
701
|
-
|
|
934
|
+
const subscriptionCode = string(subscription.code);
|
|
935
|
+
if (!/^[a-z][a-z0-9-]*$/.test(subscriptionCode) ||
|
|
936
|
+
subscriptionCodes.has(subscriptionCode)) {
|
|
937
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_SUBSCRIPTION_CODE_INVALID', '事件订阅 code 必须是唯一的 kebab-case', `${path}.code`));
|
|
702
938
|
}
|
|
703
939
|
if (!Array.isArray(subscription.eventTypes) ||
|
|
704
|
-
subscription.eventTypes.length === 0
|
|
705
|
-
|
|
940
|
+
subscription.eventTypes.length === 0 ||
|
|
941
|
+
subscription.eventTypes.length > 20 ||
|
|
942
|
+
new Set(subscription.eventTypes.map(string)).size !==
|
|
943
|
+
subscription.eventTypes.length ||
|
|
944
|
+
subscription.eventTypes.some(eventType => !allowedEventTypes.has(string(eventType)))) {
|
|
945
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_TYPES_INVALID', '事件订阅必须声明 1 到 20 个不重复且已注册的 v2 eventTypes', `${path}.eventTypes`));
|
|
946
|
+
}
|
|
947
|
+
if ('endpointPath' in subscription) {
|
|
948
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_ENDPOINT_DECLARATION_FORBIDDEN', '事件订阅 endpointPath 由 code 生成,应用不能重复声明', `${path}.endpointPath`));
|
|
706
949
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
950
|
+
subscriptionCodes.add(subscriptionCode);
|
|
951
|
+
const filter = object(subscription.filter);
|
|
952
|
+
const filterKeys = Object.keys(filter);
|
|
953
|
+
const resourceCodes = Array.isArray(filter.resourceCodes)
|
|
954
|
+
? filter.resourceCodes.map(string)
|
|
955
|
+
: [];
|
|
956
|
+
if (filterKeys.some(key => !['resourceCodes', 'subject', 'changedFields', 'changes', 'where'].includes(key)) ||
|
|
957
|
+
resourceCodes.length > 20 ||
|
|
958
|
+
new Set(resourceCodes).size !== resourceCodes.length ||
|
|
959
|
+
resourceCodes.some(code => !eventResourceFields.has(code))) {
|
|
960
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_RESOURCE_INVALID', '事件过滤 resourceCodes 必须引用最多 20 个不重复的已声明资源', `${path}.filter`));
|
|
961
|
+
}
|
|
962
|
+
const allowedFields = new Set();
|
|
963
|
+
if (resourceCodes.length > 0) {
|
|
964
|
+
const fieldSets = resourceCodes
|
|
965
|
+
.map(code => eventResourceFields.get(code))
|
|
966
|
+
.filter(Boolean);
|
|
967
|
+
for (const field of fieldSets[0]?.keys() || []) {
|
|
968
|
+
if (fieldSets.every(fields => fields.has(field)))
|
|
969
|
+
allowedFields.add(field);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const subject = object(filter.subject);
|
|
973
|
+
if (Object.keys(subject).length > 1 ||
|
|
974
|
+
Object.keys(subject).some(key => !['equals', 'prefix'].includes(key)) ||
|
|
975
|
+
Object.values(subject).some(value => typeof value !== 'string')) {
|
|
976
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_SUBJECT_INVALID', 'subject 过滤只能声明 equals 或 prefix 之一', `${path}.filter.subject`));
|
|
977
|
+
}
|
|
978
|
+
const referencedFields = new Set();
|
|
979
|
+
const changedFields = object(filter.changedFields);
|
|
980
|
+
const filterComplexity = {
|
|
981
|
+
atoms: (resourceCodes.length > 0 ? 1 : 0) +
|
|
982
|
+
(Object.keys(subject).length > 0 ? 1 : 0),
|
|
983
|
+
};
|
|
984
|
+
for (const [group, rawFields] of Object.entries(changedFields)) {
|
|
985
|
+
const fields = Array.isArray(rawFields) ? rawFields.map(string) : [];
|
|
986
|
+
fields.forEach(field => referencedFields.add(field));
|
|
987
|
+
if (['anyOf', 'allOf', 'noneOf'].includes(group)) {
|
|
988
|
+
filterComplexity.atoms += 1;
|
|
989
|
+
}
|
|
990
|
+
if (!['anyOf', 'allOf', 'noneOf'].includes(group) ||
|
|
991
|
+
fields.length < 1 ||
|
|
992
|
+
fields.length > 32 ||
|
|
993
|
+
new Set(fields).size !== fields.length) {
|
|
994
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_CHANGED_FIELDS_INVALID', 'changedFields 只支持 anyOf/allOf/noneOf,每组 1 到 32 个不重复字段', `${path}.filter.changedFields.${group}`));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const changes = Array.isArray(filter.changes) ? filter.changes : [];
|
|
998
|
+
filterComplexity.atoms += changes.length;
|
|
999
|
+
if (changes.length > 16) {
|
|
1000
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_CHANGE_FILTERS_EXCEEDED', '单个订阅最多声明 16 个字段变化条件', `${path}.filter.changes`));
|
|
1001
|
+
}
|
|
1002
|
+
changes.forEach((change, changeIndex) => {
|
|
1003
|
+
referencedFields.add(string(object(change).field));
|
|
1004
|
+
validateEventChangeFilter(change, `${path}.filter.changes[${changeIndex}]`, allowedFields, diagnostics);
|
|
1005
|
+
});
|
|
1006
|
+
if (filter.where !== undefined) {
|
|
1007
|
+
validateEventFilterCondition(filter.where, `${path}.filter.where`, allowedFields, diagnostics, filterComplexity);
|
|
1008
|
+
}
|
|
1009
|
+
if (filterComplexity.atoms > 16) {
|
|
1010
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_ATOMS_EXCEEDED', '单个订阅最多包含 16 个原子过滤条件', `${path}.filter`));
|
|
1011
|
+
}
|
|
1012
|
+
const payload = object(subscription.payload);
|
|
1013
|
+
const projectionFields = Array.isArray(payload.fields)
|
|
1014
|
+
? payload.fields.map(string)
|
|
1015
|
+
: [];
|
|
1016
|
+
projectionFields.forEach(field => referencedFields.add(field));
|
|
1017
|
+
if (Object.keys(payload).some(key => !['includeChanges', 'fields'].includes(key)) ||
|
|
1018
|
+
(payload.includeChanges !== undefined &&
|
|
1019
|
+
typeof payload.includeChanges !== 'boolean') ||
|
|
1020
|
+
projectionFields.length > 32 ||
|
|
1021
|
+
new Set(projectionFields).size !== projectionFields.length ||
|
|
1022
|
+
referencedFields.size > 0 && resourceCodes.length === 0 ||
|
|
1023
|
+
[...referencedFields].some(field => !allowedFields.has(field))) {
|
|
1024
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_PROJECTION_INVALID', '字段过滤和 payload.fields 必须绑定 resourceCodes、引用各目标资源共有字段,且单订阅投影最多 32 个字段', `${path}.payload`));
|
|
1025
|
+
}
|
|
1026
|
+
for (const resourceCode of resourceCodes) {
|
|
1027
|
+
const captured = captureFieldsByResource.get(resourceCode) || new Set();
|
|
1028
|
+
referencedFields.forEach(field => captured.add(field));
|
|
1029
|
+
captureFieldsByResource.set(resourceCode, captured);
|
|
1030
|
+
}
|
|
1031
|
+
const delivery = {
|
|
1032
|
+
timeoutMs: 10_000,
|
|
1033
|
+
maxAttempts: 8,
|
|
1034
|
+
initialBackoffMs: 1_000,
|
|
1035
|
+
maxBackoffMs: 300_000,
|
|
1036
|
+
ordering: 'none',
|
|
1037
|
+
concurrency: 10,
|
|
1038
|
+
...object(subscription.delivery),
|
|
1039
|
+
};
|
|
1040
|
+
if (!Number.isInteger(delivery.timeoutMs) ||
|
|
1041
|
+
Number(delivery.timeoutMs) < 1000 ||
|
|
1042
|
+
Number(delivery.timeoutMs) > 30_000 ||
|
|
1043
|
+
!Number.isInteger(delivery.maxAttempts) ||
|
|
1044
|
+
Number(delivery.maxAttempts) < 1 ||
|
|
1045
|
+
Number(delivery.maxAttempts) > 12 ||
|
|
1046
|
+
!Number.isInteger(delivery.initialBackoffMs) ||
|
|
1047
|
+
Number(delivery.initialBackoffMs) < 1000 ||
|
|
1048
|
+
!Number.isInteger(delivery.maxBackoffMs) ||
|
|
1049
|
+
Number(delivery.maxBackoffMs) < Number(delivery.initialBackoffMs) ||
|
|
1050
|
+
Number(delivery.maxBackoffMs) > 1_800_000 ||
|
|
1051
|
+
!['none', 'record'].includes(string(delivery.ordering)) ||
|
|
1052
|
+
!Number.isInteger(delivery.concurrency) ||
|
|
1053
|
+
Number(delivery.concurrency) < 1 ||
|
|
1054
|
+
Number(delivery.concurrency) > 50) {
|
|
1055
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_DELIVERY_POLICY_INVALID', 'delivery 超出 timeout、attempt、backoff、ordering 或 concurrency 的平台上限', `${path}.delivery`));
|
|
712
1056
|
}
|
|
713
1057
|
});
|
|
714
1058
|
}
|
|
1059
|
+
for (const [resourceCode, fields] of captureFieldsByResource) {
|
|
1060
|
+
if (fields.size > 64) {
|
|
1061
|
+
diagnostics.push(diagnostic('APP_CONFIG_EVENT_CAPTURE_PLAN_EXCEEDED', `资源 ${resourceCode} 的订阅捕获字段并集超过 64 个`, 'events.subscriptions'));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const timerCodes = new Set();
|
|
715
1065
|
(Array.isArray(events.timers) ? events.timers : []).forEach((item, index) => {
|
|
716
1066
|
const timer = object(item);
|
|
717
1067
|
const path = `events.timers[${index}]`;
|
|
718
1068
|
if ('environmentKey' in timer) {
|
|
719
1069
|
diagnostics.push(diagnostic('APP_CONFIG_ENVIRONMENT_OVERRIDE_FORBIDDEN', '定时声明属于环境中立 AppVersion,不能声明 environmentKey', `${path}.environmentKey`));
|
|
720
1070
|
}
|
|
721
|
-
if (!/^[a-z][a-z0-9-]*$/.test(string(timer.code))
|
|
1071
|
+
if (!/^[a-z][a-z0-9-]*$/.test(string(timer.code)) ||
|
|
1072
|
+
timerCodes.has(string(timer.code))) {
|
|
722
1073
|
diagnostics.push(diagnostic('APP_CONFIG_TIMER_CODE_INVALID', '定时订阅 code 必须是 kebab-case', `${path}.code`));
|
|
723
1074
|
}
|
|
724
|
-
|
|
1075
|
+
timerCodes.add(string(timer.code));
|
|
1076
|
+
if (!declaredEventTypes.has(string(timer.eventType)) ||
|
|
725
1077
|
!string(timer.cronExpression) ||
|
|
726
|
-
!string(timer.timezone)
|
|
727
|
-
|
|
1078
|
+
!string(timer.timezone) ||
|
|
1079
|
+
(timer.misfirePolicy !== undefined &&
|
|
1080
|
+
timer.misfirePolicy !== 'coalesce_one')) {
|
|
1081
|
+
diagnostics.push(diagnostic('APP_CONFIG_TIMER_SCHEDULE_REQUIRED', '定时订阅必须引用应用事件 Schema,并声明六段 cronExpression、IANA timezone 和 coalesce_one misfire policy', path));
|
|
728
1082
|
}
|
|
729
1083
|
const cronExpression = string(timer.cronExpression);
|
|
730
1084
|
const timezone = string(timer.timezone);
|
|
@@ -754,6 +1108,49 @@ export function validateAppConfig(value) {
|
|
|
754
1108
|
else if (Buffer.byteLength(JSON.stringify(timer.payload || {}), 'utf8') > 64 * 1024) {
|
|
755
1109
|
diagnostics.push(diagnostic('APP_CONFIG_TIMER_PAYLOAD_TOO_LARGE', '定时事件 payload 不能超过 64 KiB', `${path}.payload`));
|
|
756
1110
|
}
|
|
1111
|
+
else if (!eventDataMatchesSchema(applicationEventSchemas.get(string(timer.eventType)), timer.payload)) {
|
|
1112
|
+
diagnostics.push(diagnostic('APP_CONFIG_TIMER_PAYLOAD_SCHEMA_INVALID', '定时事件 payload 必须完整满足所引用的应用事件 JSON Schema', `${path}.payload`));
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
const dateTriggerCodes = new Set();
|
|
1116
|
+
(Array.isArray(events.dateTriggers) ? events.dateTriggers : []).forEach((item, index) => {
|
|
1117
|
+
const trigger = object(item);
|
|
1118
|
+
const path = `events.dateTriggers[${index}]`;
|
|
1119
|
+
const code = string(trigger.code);
|
|
1120
|
+
const resourceCode = string(trigger.resourceCode);
|
|
1121
|
+
const field = string(trigger.field);
|
|
1122
|
+
const fieldType = eventResourceFields.get(resourceCode)?.get(field);
|
|
1123
|
+
const payload = object(trigger.payload);
|
|
1124
|
+
let payloadBytes = Number.POSITIVE_INFINITY;
|
|
1125
|
+
try {
|
|
1126
|
+
payloadBytes = Buffer.byteLength(JSON.stringify(trigger.payload || {}), 'utf8');
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
// Reported by the combined validation below.
|
|
1130
|
+
}
|
|
1131
|
+
if (!/^[a-z][a-z0-9-]*$/.test(code) ||
|
|
1132
|
+
dateTriggerCodes.has(code) ||
|
|
1133
|
+
!eventResourceFields.has(resourceCode) ||
|
|
1134
|
+
!['date', 'datetime'].includes(fieldType || '') ||
|
|
1135
|
+
!validDateTriggerOffset(trigger.offset) ||
|
|
1136
|
+
!declaredEventTypes.has(string(trigger.eventType)) ||
|
|
1137
|
+
trigger.payload === null ||
|
|
1138
|
+
typeof trigger.payload !== 'object' ||
|
|
1139
|
+
Array.isArray(trigger.payload) ||
|
|
1140
|
+
payloadBytes > 64 * 1024 ||
|
|
1141
|
+
Object.keys(payload).some(key => DATE_TRIGGER_RESERVED_DATA_FIELDS.has(key)) ||
|
|
1142
|
+
!eventDataMatchesSchema(applicationEventSchemas.get(string(trigger.eventType)), {
|
|
1143
|
+
...payload,
|
|
1144
|
+
triggerCode: code,
|
|
1145
|
+
resourceCode,
|
|
1146
|
+
recordId: '00000000-0000-4000-8000-000000000000',
|
|
1147
|
+
recordRevision: 1,
|
|
1148
|
+
field,
|
|
1149
|
+
dueAt: '2026-01-01T00:00:00.000Z',
|
|
1150
|
+
})) {
|
|
1151
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATE_TRIGGER_INVALID', 'dateTrigger 必须使用唯一 code、date/datetime 字段、十年内 ISO-8601 offset、已注册应用事件、无保留字段且完整生成数据满足 JSON Schema', path));
|
|
1152
|
+
}
|
|
1153
|
+
dateTriggerCodes.add(code);
|
|
757
1154
|
});
|
|
758
1155
|
const workflows = object(config.workflows);
|
|
759
1156
|
if (config.workflows !== undefined &&
|
|
@@ -1288,14 +1685,29 @@ export function currentUserDataPolicy(input) {
|
|
|
1288
1685
|
}
|
|
1289
1686
|
export function materializeDataResource(appCode, declaration) {
|
|
1290
1687
|
const capabilities = resourceCapabilityCodes(appCode, declaration.code);
|
|
1291
|
-
const fields = declaration.fields.map(field =>
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1688
|
+
const fields = declaration.fields.map(field => {
|
|
1689
|
+
const definition = {
|
|
1690
|
+
code: field.code,
|
|
1691
|
+
type: field.type,
|
|
1692
|
+
nullable: fieldNullable({
|
|
1693
|
+
type: field.type,
|
|
1694
|
+
nullable: field.required !== true,
|
|
1695
|
+
}),
|
|
1696
|
+
indexed: field.indexed ??
|
|
1697
|
+
Boolean(field.filter || field.searchable || field.sortable),
|
|
1698
|
+
...(field.options ? { options: field.options } : {}),
|
|
1699
|
+
...(field.source ? { source: field.source } : {}),
|
|
1700
|
+
...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),
|
|
1701
|
+
...(field.precision !== undefined ? { precision: field.precision } : {}),
|
|
1702
|
+
...(field.scale !== undefined ? { scale: field.scale } : {}),
|
|
1703
|
+
...(field.timePrecision ? { timePrecision: field.timePrecision } : {}),
|
|
1704
|
+
...(field.file ? { file: field.file } : {}),
|
|
1705
|
+
...(field.serial ? { serial: field.serial } : {}),
|
|
1706
|
+
...(field.subtable ? { subtable: field.subtable } : {}),
|
|
1707
|
+
};
|
|
1708
|
+
physicalPlanForField(definition, field.searchable === true);
|
|
1709
|
+
return definition;
|
|
1710
|
+
});
|
|
1299
1711
|
const visible = declaration.fields
|
|
1300
1712
|
.filter(field => field.list !== false)
|
|
1301
1713
|
.slice(0, 8);
|
|
@@ -1306,6 +1718,8 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
1306
1718
|
.filter(field => field.filter === true)
|
|
1307
1719
|
.map(field => field.code);
|
|
1308
1720
|
const access = (field, operation) => {
|
|
1721
|
+
if (isGeneratedField(field.type) && operation !== 'read')
|
|
1722
|
+
return [];
|
|
1309
1723
|
const override = field.access?.[operation];
|
|
1310
1724
|
if (override === false)
|
|
1311
1725
|
return [];
|
|
@@ -1319,6 +1733,7 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
1319
1733
|
field.code,
|
|
1320
1734
|
{
|
|
1321
1735
|
label: field.label,
|
|
1736
|
+
type: field.type,
|
|
1322
1737
|
widget: resolveDataFieldSurfaceWidget(field),
|
|
1323
1738
|
...(field.section ? { section: field.section } : {}),
|
|
1324
1739
|
...(field.required === true ? { requiredHint: true } : {}),
|
|
@@ -1326,16 +1741,21 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
1326
1741
|
readCapabilities: access(field, 'read'),
|
|
1327
1742
|
createCapabilities: access(field, 'create'),
|
|
1328
1743
|
updateCapabilities: access(field, 'update'),
|
|
1329
|
-
...(field.
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
? { multiple: field.file.multiple }
|
|
1333
|
-
: {}),
|
|
1744
|
+
...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),
|
|
1745
|
+
...(field.precision !== undefined ? { precision: field.precision } : {}),
|
|
1746
|
+
...(field.scale !== undefined ? { scale: field.scale } : {}),
|
|
1334
1747
|
...(field.file?.maxCount !== undefined
|
|
1335
1748
|
? { maxCount: field.file.maxCount }
|
|
1336
1749
|
: {}),
|
|
1750
|
+
...(field.file?.maxSizeMb !== undefined
|
|
1751
|
+
? { maxSizeMb: field.file.maxSizeMb }
|
|
1752
|
+
: {}),
|
|
1337
1753
|
...(field.file?.accept ? { accept: field.file.accept } : {}),
|
|
1338
1754
|
...(field.options ? { options: field.options } : {}),
|
|
1755
|
+
...(field.source ? { source: field.source } : {}),
|
|
1756
|
+
...(field.timePrecision ? { timePrecision: field.timePrecision } : {}),
|
|
1757
|
+
...(field.serial ? { serial: field.serial } : {}),
|
|
1758
|
+
...(field.subtable ? { subtable: field.subtable } : {}),
|
|
1339
1759
|
...(visible.some(item => item.code === field.code)
|
|
1340
1760
|
? { list: true }
|
|
1341
1761
|
: { list: false }),
|
|
@@ -1343,7 +1763,6 @@ export function materializeDataResource(appCode, declaration) {
|
|
|
1343
1763
|
? { searchable: field.searchable }
|
|
1344
1764
|
: {}),
|
|
1345
1765
|
...(field.sortable !== undefined ? { sortable: field.sortable } : {}),
|
|
1346
|
-
...(field.reference ? { reference: field.reference } : {}),
|
|
1347
1766
|
},
|
|
1348
1767
|
])),
|
|
1349
1768
|
list: {
|
|
@@ -1404,8 +1823,15 @@ export function validateAppDeclaration(value) {
|
|
|
1404
1823
|
'label',
|
|
1405
1824
|
'required',
|
|
1406
1825
|
'indexed',
|
|
1407
|
-
'
|
|
1826
|
+
'options',
|
|
1827
|
+
'source',
|
|
1828
|
+
'maxLength',
|
|
1829
|
+
'precision',
|
|
1830
|
+
'scale',
|
|
1831
|
+
'timePrecision',
|
|
1408
1832
|
'file',
|
|
1833
|
+
'serial',
|
|
1834
|
+
'subtable',
|
|
1409
1835
|
'widget',
|
|
1410
1836
|
'section',
|
|
1411
1837
|
'system',
|
|
@@ -1413,7 +1839,6 @@ export function validateAppDeclaration(value) {
|
|
|
1413
1839
|
'filter',
|
|
1414
1840
|
'searchable',
|
|
1415
1841
|
'sortable',
|
|
1416
|
-
'options',
|
|
1417
1842
|
'access',
|
|
1418
1843
|
]);
|
|
1419
1844
|
resources.forEach((rawResource, resourceIndex) => {
|
|
@@ -1443,7 +1868,34 @@ export function validateAppDeclaration(value) {
|
|
|
1443
1868
|
const fieldType = string(field.type);
|
|
1444
1869
|
if (!APP_DATA_FIELD_TYPES.has(fieldType)) {
|
|
1445
1870
|
materializable = false;
|
|
1446
|
-
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_TYPE_INVALID', `${fieldPath}.type 必须是
|
|
1871
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_TYPE_INVALID', `${fieldPath}.type 必须是 OpenXiangda 2.0 语义字段类型之一: ${DATA_FIELD_TYPES.join('、')}`, `${fieldPath}.type`, '直接声明业务语义类型,不声明 PostgreSQL 物理类型'));
|
|
1872
|
+
}
|
|
1873
|
+
else {
|
|
1874
|
+
const semanticType = fieldType;
|
|
1875
|
+
const widget = string(field.widget);
|
|
1876
|
+
if (widget && !isCompatibleFieldWidget({ type: semanticType, widget })) {
|
|
1877
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_WIDGET_INCOMPATIBLE', `${fieldPath}.widget 与 ${semanticType} 不兼容;允许 ${compatibleWidgets(semanticType).join('、')}`, `${fieldPath}.widget`));
|
|
1878
|
+
}
|
|
1879
|
+
if (field.filter === true && !supportsFilter(semanticType)) {
|
|
1880
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_FILTER_UNSUPPORTED', `${semanticType} 不支持作为父资源筛选字段`, `${fieldPath}.filter`));
|
|
1881
|
+
}
|
|
1882
|
+
if (field.searchable === true && !supportsSearch(semanticType)) {
|
|
1883
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_SEARCH_UNSUPPORTED', `${semanticType} 没有全文/模糊搜索和 trigram 索引计划`, `${fieldPath}.searchable`));
|
|
1884
|
+
}
|
|
1885
|
+
if (field.sortable === true && !supportsSort(semanticType)) {
|
|
1886
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_SORT_UNSUPPORTED', `${semanticType} 不支持稳定排序`, `${fieldPath}.sortable`));
|
|
1887
|
+
}
|
|
1888
|
+
if (field.indexed === false &&
|
|
1889
|
+
(field.filter === true ||
|
|
1890
|
+
field.searchable === true ||
|
|
1891
|
+
field.sortable === true)) {
|
|
1892
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_INDEX_REQUIRED', '启用筛选、搜索或排序时不能显式关闭索引', `${fieldPath}.indexed`));
|
|
1893
|
+
}
|
|
1894
|
+
if (['radio', 'checkbox'].includes(widget) &&
|
|
1895
|
+
object(field.source).loadMode !== 'all' &&
|
|
1896
|
+
field.source !== undefined) {
|
|
1897
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_SOURCE_LOAD_MODE_INVALID', '动态 Radio/Checkbox 必须声明 source.loadMode = all', `${fieldPath}.source.loadMode`));
|
|
1898
|
+
}
|
|
1447
1899
|
}
|
|
1448
1900
|
if (!string(field.label)) {
|
|
1449
1901
|
diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_LABEL_REQUIRED', '字段必须同时声明中文或业务 label', `${fieldPath}.label`));
|
|
@@ -1493,6 +1945,63 @@ export function validateAppDeclaration(value) {
|
|
|
1493
1945
|
})));
|
|
1494
1946
|
}
|
|
1495
1947
|
});
|
|
1948
|
+
const declaredResources = new Map(resources.map((rawResource, resourceIndex) => {
|
|
1949
|
+
const resource = object(rawResource);
|
|
1950
|
+
return [
|
|
1951
|
+
string(resource.code),
|
|
1952
|
+
{
|
|
1953
|
+
resource,
|
|
1954
|
+
resourceIndex,
|
|
1955
|
+
fields: new Map((Array.isArray(resource.fields) ? resource.fields : []).map(rawField => {
|
|
1956
|
+
const field = object(rawField);
|
|
1957
|
+
return [string(field.code), field];
|
|
1958
|
+
})),
|
|
1959
|
+
},
|
|
1960
|
+
];
|
|
1961
|
+
}));
|
|
1962
|
+
for (const [resourceCode, declaration] of declaredResources) {
|
|
1963
|
+
const fields = Array.isArray(declaration.resource.fields)
|
|
1964
|
+
? declaration.resource.fields
|
|
1965
|
+
: [];
|
|
1966
|
+
const aggregateMaxRows = fields.reduce((total, rawField) => {
|
|
1967
|
+
const field = object(rawField);
|
|
1968
|
+
if (field.type !== 'subtable')
|
|
1969
|
+
return total;
|
|
1970
|
+
const configured = Number(object(field.subtable).maxRows);
|
|
1971
|
+
return total + (Number.isSafeInteger(configured) ? configured : 20);
|
|
1972
|
+
}, 0);
|
|
1973
|
+
if (aggregateMaxRows > 49) {
|
|
1974
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_SUBTABLE_AGGREGATE_MAX_ROWS_EXCEEDED', '同一父资源的所有子表 maxRows 总和不能超过 49', `data.resources[${declaration.resourceIndex}].fields`));
|
|
1975
|
+
}
|
|
1976
|
+
fields.forEach((rawField, fieldIndex) => {
|
|
1977
|
+
const field = object(rawField);
|
|
1978
|
+
if (field.type !== 'subtable')
|
|
1979
|
+
return;
|
|
1980
|
+
const path = `data.resources[${declaration.resourceIndex}].fields[${fieldIndex}].subtable`;
|
|
1981
|
+
const subtable = object(field.subtable);
|
|
1982
|
+
const targetCode = string(subtable.resourceCode);
|
|
1983
|
+
const target = declaredResources.get(targetCode);
|
|
1984
|
+
if (!target || targetCode === resourceCode) {
|
|
1985
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_SUBTABLE_TARGET_INVALID', '子表必须引用同一应用内不同的子资源', `${path}.resourceCode`));
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
const foreignKey = target.fields.get(string(subtable.foreignKey));
|
|
1989
|
+
if (!foreignKey ||
|
|
1990
|
+
foreignKey.type !== 'uuid' ||
|
|
1991
|
+
foreignKey.required !== true ||
|
|
1992
|
+
object(foreignKey.access).create === false) {
|
|
1993
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_SUBTABLE_FOREIGN_KEY_INVALID', '子表外键必须是子资源中可创建写入的必填 uuid 字段', `${path}.foreignKey`));
|
|
1994
|
+
}
|
|
1995
|
+
const orderField = target.fields.get(string(subtable.orderField));
|
|
1996
|
+
if (!orderField ||
|
|
1997
|
+
orderField.type !== 'number.integer' ||
|
|
1998
|
+
orderField.required !== true ||
|
|
1999
|
+
object(orderField.access).create === false ||
|
|
2000
|
+
object(orderField.access).update === false) {
|
|
2001
|
+
diagnostics.push(diagnostic('APP_CONFIG_DATA_SUBTABLE_ORDER_FIELD_INVALID', '子表排序字段必须是子资源中可创建和更新的必填 number.integer 字段', `${path}.orderField`));
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
1496
2005
|
return diagnostics;
|
|
1497
2006
|
}
|
|
1498
2007
|
export function defineOpenXiangdaApp(declaration) {
|