openxiangda-devkit-core 2.0.0-alpha.53 → 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/bundle.d.ts.map +1 -1
- package/dist/compiler/bundle.js +171 -23
- package/dist/compiler/bundle.js.map +1 -1
- package/dist/compiler/config.d.ts +24 -3
- package/dist/compiler/config.d.ts.map +1 -1
- package/dist/compiler/config.js +366 -14
- package/dist/compiler/config.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,4 +1,5 @@
|
|
|
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
4
|
import { fieldNullable, isGeneratedField } from './field-codec.js';
|
|
4
5
|
import { supportsFilter, supportsSearch, supportsSort, } from './field-query-plan.js';
|
|
@@ -8,6 +9,14 @@ import { compatibleWidgets, isCompatibleFieldWidget, resolveDataFieldSurfaceWidg
|
|
|
8
9
|
// The compiler owns the application source contract. Devkit and every adapter
|
|
9
10
|
// re-export this module instead of maintaining separate configuration models.
|
|
10
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
|
+
]);
|
|
11
20
|
const APP_DATA_FIELD_TYPES = new Set(DATA_FIELD_TYPES);
|
|
12
21
|
export const openXiangdaAppConfigSchema = {
|
|
13
22
|
$id: 'openxiangda.app-config/v3',
|
|
@@ -154,6 +163,11 @@ export const openXiangdaAppConfigSchema = {
|
|
|
154
163
|
additionalProperties: false,
|
|
155
164
|
required: ['subscriptions'],
|
|
156
165
|
properties: {
|
|
166
|
+
schemas: {
|
|
167
|
+
type: 'array',
|
|
168
|
+
maxItems: 100,
|
|
169
|
+
items: { type: 'object' },
|
|
170
|
+
},
|
|
157
171
|
subscriptions: {
|
|
158
172
|
type: 'array',
|
|
159
173
|
maxItems: 100,
|
|
@@ -164,6 +178,11 @@ export const openXiangdaAppConfigSchema = {
|
|
|
164
178
|
maxItems: 100,
|
|
165
179
|
items: { type: 'object' },
|
|
166
180
|
},
|
|
181
|
+
dateTriggers: {
|
|
182
|
+
type: 'array',
|
|
183
|
+
maxItems: 100,
|
|
184
|
+
items: { type: 'object' },
|
|
185
|
+
},
|
|
167
186
|
},
|
|
168
187
|
},
|
|
169
188
|
workflows: {
|
|
@@ -239,6 +258,7 @@ export function backendRuntimeRequired(config) {
|
|
|
239
258
|
backend.secrets?.length ||
|
|
240
259
|
config.events?.subscriptions?.length ||
|
|
241
260
|
config.events?.timers?.length ||
|
|
261
|
+
config.events?.dateTriggers?.length ||
|
|
242
262
|
config.workflows?.activations?.length ||
|
|
243
263
|
config.workflows?.providers?.length)));
|
|
244
264
|
}
|
|
@@ -250,6 +270,109 @@ function object(value) {
|
|
|
250
270
|
function string(value) {
|
|
251
271
|
return typeof value === 'string' ? value.trim() : '';
|
|
252
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
|
+
}
|
|
253
376
|
export function validateAppConfig(value) {
|
|
254
377
|
const diagnostics = [];
|
|
255
378
|
const config = object(value);
|
|
@@ -322,6 +445,8 @@ export function validateAppConfig(value) {
|
|
|
322
445
|
Array.isArray(eventConfig.subscriptions) &&
|
|
323
446
|
eventConfig.subscriptions.length > 0 ||
|
|
324
447
|
Array.isArray(eventConfig.timers) && eventConfig.timers.length > 0 ||
|
|
448
|
+
Array.isArray(eventConfig.dateTriggers) &&
|
|
449
|
+
eventConfig.dateTriggers.length > 0 ||
|
|
325
450
|
Array.isArray(workflowConfig.activations) &&
|
|
326
451
|
workflowConfig.activations.length > 0 ||
|
|
327
452
|
Array.isArray(workflowConfig.providers) && workflowConfig.providers.length > 0)) {
|
|
@@ -732,6 +857,70 @@ export function validateAppConfig(value) {
|
|
|
732
857
|
}
|
|
733
858
|
validateCapabilityClosure(appCode, config, declaredCapabilities, diagnostics);
|
|
734
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();
|
|
735
924
|
if (config.events !== undefined && !Array.isArray(events.subscriptions)) {
|
|
736
925
|
diagnostics.push(diagnostic('APP_CONFIG_EVENT_SUBSCRIPTIONS_REQUIRED', 'events.subscriptions 必须是数组', 'events.subscriptions'));
|
|
737
926
|
}
|
|
@@ -742,34 +931,154 @@ export function validateAppConfig(value) {
|
|
|
742
931
|
if ('environmentKey' in subscription) {
|
|
743
932
|
diagnostics.push(diagnostic('APP_CONFIG_ENVIRONMENT_OVERRIDE_FORBIDDEN', '事件声明属于环境中立 AppVersion,不能声明 environmentKey', `${path}.environmentKey`));
|
|
744
933
|
}
|
|
745
|
-
|
|
746
|
-
|
|
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`));
|
|
747
938
|
}
|
|
748
939
|
if (!Array.isArray(subscription.eventTypes) ||
|
|
749
|
-
subscription.eventTypes.length === 0
|
|
750
|
-
|
|
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`));
|
|
949
|
+
}
|
|
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`));
|
|
751
977
|
}
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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`));
|
|
757
1056
|
}
|
|
758
1057
|
});
|
|
759
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();
|
|
760
1065
|
(Array.isArray(events.timers) ? events.timers : []).forEach((item, index) => {
|
|
761
1066
|
const timer = object(item);
|
|
762
1067
|
const path = `events.timers[${index}]`;
|
|
763
1068
|
if ('environmentKey' in timer) {
|
|
764
1069
|
diagnostics.push(diagnostic('APP_CONFIG_ENVIRONMENT_OVERRIDE_FORBIDDEN', '定时声明属于环境中立 AppVersion,不能声明 environmentKey', `${path}.environmentKey`));
|
|
765
1070
|
}
|
|
766
|
-
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))) {
|
|
767
1073
|
diagnostics.push(diagnostic('APP_CONFIG_TIMER_CODE_INVALID', '定时订阅 code 必须是 kebab-case', `${path}.code`));
|
|
768
1074
|
}
|
|
769
|
-
|
|
1075
|
+
timerCodes.add(string(timer.code));
|
|
1076
|
+
if (!declaredEventTypes.has(string(timer.eventType)) ||
|
|
770
1077
|
!string(timer.cronExpression) ||
|
|
771
|
-
!string(timer.timezone)
|
|
772
|
-
|
|
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));
|
|
773
1082
|
}
|
|
774
1083
|
const cronExpression = string(timer.cronExpression);
|
|
775
1084
|
const timezone = string(timer.timezone);
|
|
@@ -799,6 +1108,49 @@ export function validateAppConfig(value) {
|
|
|
799
1108
|
else if (Buffer.byteLength(JSON.stringify(timer.payload || {}), 'utf8') > 64 * 1024) {
|
|
800
1109
|
diagnostics.push(diagnostic('APP_CONFIG_TIMER_PAYLOAD_TOO_LARGE', '定时事件 payload 不能超过 64 KiB', `${path}.payload`));
|
|
801
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);
|
|
802
1154
|
});
|
|
803
1155
|
const workflows = object(config.workflows);
|
|
804
1156
|
if (config.workflows !== undefined &&
|