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.
@@ -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
- if (!/^[a-z][a-z0-9-]*$/.test(string(subscription.code))) {
746
- diagnostics.push(diagnostic('APP_CONFIG_EVENT_SUBSCRIPTION_CODE_INVALID', '事件订阅 code 必须是 kebab-case', `${path}.code`));
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
- diagnostics.push(diagnostic('APP_CONFIG_EVENT_TYPES_REQUIRED', '事件订阅必须声明 eventTypes', `${path}.eventTypes`));
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 endpointPath = string(subscription.endpointPath);
753
- if (!endpointPath.startsWith('/') ||
754
- endpointPath.startsWith('//') ||
755
- endpointPath.includes('..')) {
756
- diagnostics.push(diagnostic('APP_CONFIG_EVENT_ENDPOINT_INVALID', '事件订阅 endpointPath 必须是应用后端相对路径', `${path}.endpointPath`));
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
- if (!string(timer.eventType) ||
1075
+ timerCodes.add(string(timer.code));
1076
+ if (!declaredEventTypes.has(string(timer.eventType)) ||
770
1077
  !string(timer.cronExpression) ||
771
- !string(timer.timezone)) {
772
- diagnostics.push(diagnostic('APP_CONFIG_TIMER_SCHEDULE_REQUIRED', '定时订阅必须声明 eventType、六段 cronExpression 和 IANA timezone', path));
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 &&