@nocobase/plugin-workflow 1.5.0-beta.8 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/client/112c72b90000c81b.js +10 -0
  2. package/dist/client/87dff3ee7e6061ee.js +10 -0
  3. package/dist/client/9d4370267ad5749d.js +10 -0
  4. package/dist/client/FlowContext.d.ts +2 -0
  5. package/dist/client/index.js +1 -1
  6. package/dist/client/nodes/calculation.d.ts +3 -2
  7. package/dist/client/nodes/condition.d.ts +1 -0
  8. package/dist/client/nodes/create.d.ts +3 -2
  9. package/dist/client/nodes/destroy.d.ts +2 -0
  10. package/dist/client/nodes/end.d.ts +2 -0
  11. package/dist/client/nodes/index.d.ts +1 -0
  12. package/dist/client/nodes/query.d.ts +5 -4
  13. package/dist/client/nodes/update.d.ts +3 -2
  14. package/dist/client/triggers/schedule/ScheduleModes.d.ts +5 -2
  15. package/dist/client/triggers/schedule/index.d.ts +2 -0
  16. package/dist/client/variable.d.ts +16 -1
  17. package/dist/externalVersion.js +11 -11
  18. package/dist/locale/en-US.json +3 -1
  19. package/dist/locale/zh-CN.json +8 -6
  20. package/dist/node_modules/cron-parser/package.json +1 -1
  21. package/dist/node_modules/lru-cache/package.json +1 -1
  22. package/dist/server/Plugin.d.ts +9 -6
  23. package/dist/server/Plugin.js +113 -43
  24. package/dist/server/actions/workflows.js +5 -2
  25. package/dist/server/collections/executions.js +8 -0
  26. package/dist/server/instructions/CreateInstruction.js +1 -1
  27. package/dist/server/instructions/DestroyInstruction.js +1 -1
  28. package/dist/server/instructions/UpdateInstruction.js +1 -1
  29. package/dist/server/triggers/CollectionTrigger.d.ts +6 -6
  30. package/dist/server/triggers/CollectionTrigger.js +51 -31
  31. package/dist/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.d.ts +5 -1
  32. package/dist/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.js +53 -14
  33. package/dist/server/triggers/ScheduleTrigger/StaticScheduleTrigger.d.ts +1 -0
  34. package/dist/server/triggers/ScheduleTrigger/StaticScheduleTrigger.js +3 -0
  35. package/dist/server/triggers/ScheduleTrigger/index.d.ts +2 -2
  36. package/dist/server/triggers/ScheduleTrigger/index.js +21 -3
  37. package/dist/server/triggers/index.d.ts +4 -1
  38. package/package.json +3 -3
  39. package/dist/client/5ed8ff0f70ed5911.js +0 -10
  40. package/dist/client/92877729dbcede8f.js +0 -10
  41. package/dist/client/e7b9d67c6a964bec.js +0 -10
@@ -41,6 +41,7 @@ __export(Plugin_exports, {
41
41
  module.exports = __toCommonJS(Plugin_exports);
42
42
  var import_path = __toESM(require("path"));
43
43
  var import_crypto = require("crypto");
44
+ var import_sequelize = require("sequelize");
44
45
  var import_lru_cache = __toESM(require("lru-cache"));
45
46
  var import_database = require("@nocobase/database");
46
47
  var import_server = require("@nocobase/server");
@@ -126,7 +127,7 @@ class PluginWorkflowServer extends import_server.Plugin {
126
127
  /**
127
128
  * @experimental
128
129
  */
129
- getLogger(workflowId) {
130
+ getLogger(workflowId = "dispatcher") {
130
131
  const now = /* @__PURE__ */ new Date();
131
132
  const date = `${now.getFullYear()}-${`0${now.getMonth() + 1}`.slice(-2)}-${`0${now.getDate()}`.slice(-2)}`;
132
133
  const key = `${date}-${workflowId}}`;
@@ -256,7 +257,6 @@ class PluginWorkflowServer extends import_server.Plugin {
256
257
  (model, { transaction }) => this.toggle(model, false, { transaction })
257
258
  );
258
259
  this.app.on("afterStart", async () => {
259
- this.app.setMaintainingMessage("check for not started executions");
260
260
  this.ready = true;
261
261
  const collection = db.getCollection("workflows");
262
262
  const workflows = await collection.repository.find({
@@ -266,8 +266,14 @@ class PluginWorkflowServer extends import_server.Plugin {
266
266
  this.toggle(workflow, true, { silent: true });
267
267
  });
268
268
  this.checker = setInterval(() => {
269
+ this.getLogger("dispatcher").info(`(cycling) check for queueing executions`);
269
270
  this.dispatch();
270
271
  }, 3e5);
272
+ this.app.on("workflow:dispatch", () => {
273
+ this.app.logger.info("workflow:dispatch");
274
+ this.dispatch();
275
+ });
276
+ this.getLogger("dispatcher").info("(starting) check for queueing executions");
271
277
  this.dispatch();
272
278
  });
273
279
  this.app.on("beforeStop", async () => {
@@ -323,10 +329,19 @@ class PluginWorkflowServer extends import_server.Plugin {
323
329
  logger.debug(`ignored event data:`, context);
324
330
  return;
325
331
  }
326
- if (!options.manually && !workflow.enabled) {
332
+ if (!options.force && !options.manually && !workflow.enabled) {
327
333
  logger.warn(`workflow ${workflow.id} is not enabled, event will be ignored`);
328
334
  return;
329
335
  }
336
+ const duplicated = this.events.find(([w, c, { eventKey }]) => {
337
+ if (eventKey && options.eventKey) {
338
+ return eventKey === options.eventKey;
339
+ }
340
+ });
341
+ if (duplicated) {
342
+ logger.warn(`event of workflow ${workflow.id} is duplicated (${options.eventKey}), event will be ignored`);
343
+ return;
344
+ }
330
345
  if (context == null) {
331
346
  logger.warn(`workflow ${workflow.id} event data context is null, event will be ignored`);
332
347
  return;
@@ -340,9 +355,10 @@ class PluginWorkflowServer extends import_server.Plugin {
340
355
  logger.info(`new event triggered, now events: ${this.events.length}`);
341
356
  logger.debug(`event data:`, { context });
342
357
  if (this.events.length > 1) {
358
+ logger.info(`new event is pending to be prepared after previous preparation is finished`);
343
359
  return;
344
360
  }
345
- setTimeout(this.prepare);
361
+ setImmediate(this.prepare);
346
362
  }
347
363
  async triggerSync(workflow, context, { deferred, ...options } = {}) {
348
364
  let execution;
@@ -367,33 +383,63 @@ class PluginWorkflowServer extends import_server.Plugin {
367
383
  `execution (${job.execution.id}) resuming from job (${job.id}) added to pending list`
368
384
  );
369
385
  this.pending.push([job.execution, job]);
386
+ if (this.executing) {
387
+ await this.executing;
388
+ }
370
389
  this.dispatch();
371
390
  }
372
391
  /**
373
392
  * Start a deferred execution
374
393
  * @experimental
375
394
  */
376
- start(execution) {
395
+ async start(execution) {
377
396
  if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
378
397
  return;
379
398
  }
399
+ this.getLogger(execution.workflowId).info(`starting deferred execution (${execution.id})`);
380
400
  this.pending.push([execution]);
401
+ if (this.executing) {
402
+ await this.executing;
403
+ }
381
404
  this.dispatch();
382
405
  }
383
- createProcessor(execution, options = {}) {
384
- return new import_Processor.default(execution, { ...options, plugin: this });
406
+ async validateEvent(workflow, context, options) {
407
+ const trigger = this.triggers.get(workflow.type);
408
+ const triggerValid = await trigger.validateEvent(workflow, context, options);
409
+ if (!triggerValid) {
410
+ return false;
411
+ }
412
+ const { stack } = options;
413
+ let valid = true;
414
+ if ((stack == null ? void 0 : stack.length) > 0) {
415
+ const existed = await workflow.countExecutions({
416
+ where: {
417
+ id: stack
418
+ },
419
+ transaction: options.transaction
420
+ });
421
+ const limitCount = workflow.options.stackLimit || 1;
422
+ if (existed >= limitCount) {
423
+ this.getLogger(workflow.id).warn(
424
+ `workflow ${workflow.id} has already been triggered in stacks executions (${stack}), and max call coont is ${limitCount}, newly triggering will be skipped.`
425
+ );
426
+ valid = false;
427
+ }
428
+ }
429
+ return valid;
385
430
  }
386
431
  async createExecution(workflow, context, options) {
432
+ var _a;
387
433
  const { deferred } = options;
388
434
  const transaction = await this.useDataSourceTransaction("main", options.transaction, true);
389
435
  const sameTransaction = options.transaction === transaction;
390
- const trigger = this.triggers.get(workflow.type);
391
- const valid = await trigger.validateEvent(workflow, context, { ...options, transaction });
436
+ const valid = await this.validateEvent(workflow, context, { ...options, transaction });
392
437
  if (!valid) {
393
438
  if (!sameTransaction) {
394
439
  await transaction.commit();
395
440
  }
396
- return null;
441
+ (_a = options.onTriggerFail) == null ? void 0 : _a.call(options, workflow, context, options);
442
+ return Promise.reject(new Error("event is not valid"));
397
443
  }
398
444
  let execution;
399
445
  try {
@@ -402,6 +448,7 @@ class PluginWorkflowServer extends import_server.Plugin {
402
448
  context,
403
449
  key: workflow.key,
404
450
  eventKey: options.eventKey ?? (0, import_crypto.randomUUID)(),
451
+ stack: options.stack,
405
452
  status: deferred ? import_constants.EXECUTION_STATUS.STARTED : import_constants.EXECUTION_STATUS.QUEUEING
406
453
  },
407
454
  { transaction }
@@ -441,7 +488,7 @@ class PluginWorkflowServer extends import_server.Plugin {
441
488
  const event = this.events.shift();
442
489
  this.eventsCount = this.events.length;
443
490
  if (!event) {
444
- this.getLogger("dispatcher").warn(`events queue is empty, no need to prepare`);
491
+ this.getLogger("dispatcher").info(`events queue is empty, no need to prepare`);
445
492
  return;
446
493
  }
447
494
  const logger = this.getLogger(event[0].id);
@@ -451,12 +498,16 @@ class PluginWorkflowServer extends import_server.Plugin {
451
498
  if ((execution == null ? void 0 : execution.status) === import_constants.EXECUTION_STATUS.QUEUEING && !this.executing && !this.pending.length) {
452
499
  this.pending.push([execution]);
453
500
  }
454
- } catch (err) {
455
- logger.error(`failed to create execution: ${err.message}`, err);
501
+ } catch (error) {
502
+ logger.error(`failed to create execution:`, { error });
456
503
  }
457
504
  if (this.events.length) {
458
505
  await this.prepare();
459
506
  } else {
507
+ this.getLogger("dispatcher").info("no more events need to be prepared, dispatching...");
508
+ if (this.executing) {
509
+ await this.executing;
510
+ }
460
511
  this.dispatch();
461
512
  }
462
513
  };
@@ -474,45 +525,64 @@ class PluginWorkflowServer extends import_server.Plugin {
474
525
  }
475
526
  this.executing = (async () => {
476
527
  let next = null;
477
- try {
478
- if (this.pending.length) {
479
- next = this.pending.shift();
480
- this.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`);
481
- } else {
482
- const execution = await this.db.getRepository("executions").findOne({
483
- filter: {
484
- status: import_constants.EXECUTION_STATUS.QUEUEING,
485
- "workflow.enabled": true,
486
- "workflow.id": {
487
- [import_database.Op.not]: null
488
- }
528
+ if (this.pending.length) {
529
+ next = this.pending.shift();
530
+ this.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`);
531
+ } else {
532
+ try {
533
+ await this.db.sequelize.transaction(
534
+ {
535
+ isolationLevel: this.db.options.dialect === "sqlite" ? [][0] : import_sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ
489
536
  },
490
- appends: ["workflow"],
491
- sort: "id"
492
- });
493
- if (execution) {
494
- this.getLogger(execution.workflowId).info(`execution (${execution.id}) fetched from db`);
495
- next = [execution];
496
- }
497
- }
498
- if (next) {
499
- await this.process(...next);
500
- }
501
- } finally {
502
- this.executing = null;
503
- if (next) {
504
- this.dispatch();
537
+ async (transaction) => {
538
+ const execution = await this.db.getRepository("executions").findOne({
539
+ filter: {
540
+ status: import_constants.EXECUTION_STATUS.QUEUEING,
541
+ "workflow.enabled": true
542
+ },
543
+ sort: "id",
544
+ transaction
545
+ });
546
+ if (execution) {
547
+ this.getLogger(execution.workflowId).info(`execution (${execution.id}) fetched from db`);
548
+ await execution.update(
549
+ {
550
+ status: import_constants.EXECUTION_STATUS.STARTED
551
+ },
552
+ { transaction }
553
+ );
554
+ execution.workflow = this.enabledCache.get(execution.workflowId);
555
+ next = [execution];
556
+ } else {
557
+ this.getLogger("dispatcher").info(`no execution in db queued to process`);
558
+ }
559
+ }
560
+ );
561
+ } catch (error) {
562
+ this.getLogger("dispatcher").error(`fetching execution from db failed: ${error.message}`, { error });
505
563
  }
506
564
  }
565
+ if (next) {
566
+ await this.process(...next);
567
+ }
568
+ this.executing = null;
569
+ if (next || this.pending.length) {
570
+ this.getLogger("dispatcher").info(`last process finished, will do another dispatch`);
571
+ this.dispatch();
572
+ }
507
573
  })();
508
574
  }
575
+ createProcessor(execution, options = {}) {
576
+ return new import_Processor.default(execution, { ...options, plugin: this });
577
+ }
509
578
  async process(execution, job, options = {}) {
510
579
  var _a, _b;
580
+ const logger = this.getLogger(execution.workflowId);
511
581
  if (execution.status === import_constants.EXECUTION_STATUS.QUEUEING) {
512
582
  const transaction = await this.useDataSourceTransaction("main", options.transaction);
513
583
  await execution.update({ status: import_constants.EXECUTION_STATUS.STARTED }, { transaction });
584
+ logger.info(`queueing execution (${execution.id}) from pending list updated to started`);
514
585
  }
515
- const logger = this.getLogger(execution.workflowId);
516
586
  const processor = this.createProcessor(execution, options);
517
587
  logger.info(`execution (${execution.id}) ${job ? "resuming" : "starting"}...`);
518
588
  try {
@@ -526,7 +596,7 @@ class PluginWorkflowServer extends import_server.Plugin {
526
596
  }
527
597
  return processor;
528
598
  }
529
- async execute(workflow, context, options = {}) {
599
+ async execute(workflow, values, options = {}) {
530
600
  const trigger = this.triggers.get(workflow.type);
531
601
  if (!trigger) {
532
602
  throw new Error(`trigger type "${workflow.type}" of workflow ${workflow.id} is not registered`);
@@ -534,7 +604,7 @@ class PluginWorkflowServer extends import_server.Plugin {
534
604
  if (!trigger.execute) {
535
605
  throw new Error(`"execute" method of trigger ${workflow.type} is not implemented`);
536
606
  }
537
- return trigger.execute(workflow, context, options);
607
+ return trigger.execute(workflow, values, options);
538
608
  }
539
609
  /**
540
610
  * @experimental
@@ -121,7 +121,10 @@ async function trigger(context, next) {
121
121
  }
122
122
  async function execute(context, next) {
123
123
  const plugin = context.app.pm.get(import_Plugin.default);
124
- const { filterByTk, autoRevision } = context.action.params;
124
+ const { filterByTk, values, autoRevision } = context.action.params;
125
+ if (!values) {
126
+ return context.throw(400, "values is required");
127
+ }
125
128
  if (!filterByTk) {
126
129
  return context.throw(400, "filterByTk is required");
127
130
  }
@@ -137,7 +140,7 @@ async function execute(context, next) {
137
140
  const { executed } = workflow;
138
141
  let processor;
139
142
  try {
140
- processor = await plugin.execute(workflow, context, { manually: true });
143
+ processor = await plugin.execute(workflow, values, { manually: true });
141
144
  if (!processor) {
142
145
  return context.throw(400, "workflow not triggered");
143
146
  }
@@ -61,6 +61,14 @@ var executions_default = {
61
61
  {
62
62
  type: "integer",
63
63
  name: "status"
64
+ },
65
+ {
66
+ type: "json",
67
+ name: "stack"
68
+ },
69
+ {
70
+ type: "json",
71
+ name: "output"
64
72
  }
65
73
  ]
66
74
  };
@@ -44,7 +44,7 @@ class CreateInstruction extends import__.Instruction {
44
44
  const created = await repository.create({
45
45
  ...options,
46
46
  context: {
47
- stack: Array.from(new Set((processor.execution.context.stack ?? []).concat(processor.execution.id)))
47
+ stack: Array.from(new Set((processor.execution.stack ?? []).concat(processor.execution.id)))
48
48
  },
49
49
  transaction
50
50
  });
@@ -42,7 +42,7 @@ class DestroyInstruction extends import__.Instruction {
42
42
  const result = await repository.destroy({
43
43
  ...options,
44
44
  context: {
45
- stack: Array.from(new Set((processor.execution.context.stack ?? []).concat(processor.execution.id)))
45
+ stack: Array.from(new Set((processor.execution.stack ?? []).concat(processor.execution.id)))
46
46
  },
47
47
  transaction: this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction)
48
48
  });
@@ -42,7 +42,7 @@ class UpdateInstruction extends import__.Instruction {
42
42
  const result = await repository.update({
43
43
  ...options,
44
44
  context: {
45
- stack: Array.from(new Set((processor.execution.context.stack ?? []).concat(processor.execution.id)))
45
+ stack: Array.from(new Set((processor.execution.stack ?? []).concat(processor.execution.id)))
46
46
  },
47
47
  transaction: this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction)
48
48
  });
@@ -6,11 +6,10 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
- import { Model, Transactionable } from '@nocobase/database';
9
+ import { Model } from '@nocobase/database';
10
10
  import Trigger from '.';
11
11
  import type { WorkflowModel } from '../types';
12
12
  import type { EventOptions } from '../Plugin';
13
- import { Context } from '@nocobase/actions';
14
13
  export interface CollectionChangeTriggerConfig {
15
14
  collection: string;
16
15
  mode: number;
@@ -19,12 +18,13 @@ export interface CollectionChangeTriggerConfig {
19
18
  export default class CollectionTrigger extends Trigger {
20
19
  events: Map<any, any>;
21
20
  private static handler;
22
- prepare(workflow: WorkflowModel, data: Model | Record<string, any>, options: any): Promise<{
21
+ prepare(workflow: WorkflowModel, data: Model | Record<string, any> | string | number, options: any): Promise<{
23
22
  data: any;
24
- stack: any;
25
23
  }>;
26
24
  on(workflow: WorkflowModel): void;
27
25
  off(workflow: WorkflowModel): void;
28
- validateEvent(workflow: WorkflowModel, context: any, options: Transactionable): Promise<boolean>;
29
- execute(workflow: WorkflowModel, context: Context, options: EventOptions): Promise<void | import("..").Processor>;
26
+ execute(workflow: WorkflowModel, values: any, options: EventOptions): Promise<void | import("..").Processor>;
27
+ validateContext(values: any): {
28
+ data: string;
29
+ };
30
30
  }
@@ -74,17 +74,19 @@ class CollectionTrigger extends import__.default {
74
74
  if (!ctx) {
75
75
  return;
76
76
  }
77
+ const { stack } = options.context ?? {};
77
78
  if (workflow.sync) {
78
79
  await this.workflow.trigger(workflow, ctx, {
79
- transaction
80
+ transaction,
81
+ stack
80
82
  });
81
83
  } else {
82
84
  if (transaction) {
83
85
  transaction.afterCommit(() => {
84
- this.workflow.trigger(workflow, ctx);
86
+ this.workflow.trigger(workflow, ctx, { stack });
85
87
  });
86
88
  } else {
87
- this.workflow.trigger(workflow, ctx);
89
+ this.workflow.trigger(workflow, ctx, { stack });
88
90
  }
89
91
  }
90
92
  }
@@ -95,13 +97,24 @@ class CollectionTrigger extends import__.default {
95
97
  const collection = collectionManager.getCollection(collectionName);
96
98
  const { transaction, context } = options;
97
99
  const { repository, filterTargetKey } = collection;
98
- if (data instanceof import_database.Model && changed && changed.length && changed.filter((name) => {
100
+ let target = data;
101
+ let filterByTk;
102
+ let loadNeeded = false;
103
+ if (target && typeof target === "object") {
104
+ filterByTk = Array.isArray(filterTargetKey) ? (0, import_lodash.pick)(
105
+ target,
106
+ filterTargetKey.sort((a, b) => a.localeCompare(b))
107
+ ) : target[filterTargetKey];
108
+ } else {
109
+ filterByTk = target;
110
+ loadNeeded = true;
111
+ }
112
+ if (target instanceof import_database.Model && changed && changed.length && changed.filter((name) => {
99
113
  const field = collection.getField(name);
100
114
  return field && !["linkTo", "hasOne", "hasMany", "belongsToMany"].includes(field.options.type);
101
- }).every((name) => !data.changedWithAssociations(getFieldRawName(collection, name)))) {
115
+ }).every((name) => !target.changedWithAssociations(getFieldRawName(collection, name)))) {
102
116
  return null;
103
117
  }
104
- const filterByTk = Array.isArray(filterTargetKey) ? (0, import_lodash.pick)(data, filterTargetKey) : { [filterTargetKey]: data[filterTargetKey] };
105
118
  if ((0, import_utils.isValidFilter)(condition) && !(mode & MODE_BITMAP.DESTROY)) {
106
119
  const count = await repository.count({
107
120
  filterByTk,
@@ -113,22 +126,20 @@ class CollectionTrigger extends import__.default {
113
126
  return null;
114
127
  }
115
128
  }
116
- let result = data;
117
- if ((appends == null ? void 0 : appends.length) && !(mode & MODE_BITMAP.DESTROY)) {
129
+ if (loadNeeded || (appends == null ? void 0 : appends.length) && !(mode & MODE_BITMAP.DESTROY)) {
118
130
  const includeFields = appends.reduce((set, field) => {
119
131
  set.add(field.split(".")[0]);
120
132
  set.add(field);
121
133
  return set;
122
134
  }, /* @__PURE__ */ new Set());
123
- result = await repository.findOne({
135
+ target = await repository.findOne({
124
136
  filterByTk,
125
137
  appends: Array.from(includeFields),
126
138
  transaction
127
139
  });
128
140
  }
129
141
  return {
130
- data: (0, import_utils2.toJSON)(result),
131
- stack: context == null ? void 0 : context.stack
142
+ data: (0, import_utils2.toJSON)(target)
132
143
  };
133
144
  }
134
145
  on(workflow) {
@@ -182,26 +193,27 @@ class CollectionTrigger extends import__.default {
182
193
  }
183
194
  }
184
195
  }
185
- async validateEvent(workflow, context, options) {
186
- if (context.stack) {
187
- const existed = await workflow.countExecutions({
188
- where: {
189
- id: context.stack
190
- },
191
- transaction: options.transaction
192
- });
193
- if (existed) {
194
- this.workflow.getLogger(workflow.id).warn(
195
- `workflow ${workflow.id} has already been triggered in stack executions (${context.stack}), and newly triggering will be skipped.`
196
- );
197
- return false;
198
- }
199
- }
200
- return true;
201
- }
202
- async execute(workflow, context, options) {
203
- var _a;
204
- const ctx = await this.prepare(workflow, (_a = context.action.params.values) == null ? void 0 : _a.data, options);
196
+ // async validateEvent(workflow: WorkflowModel, context: any, options: Transactionable): Promise<boolean> {
197
+ // if (context.stack) {
198
+ // const existed = await workflow.countExecutions({
199
+ // where: {
200
+ // id: context.stack,
201
+ // },
202
+ // transaction: options.transaction,
203
+ // });
204
+ // if (existed) {
205
+ // this.workflow
206
+ // .getLogger(workflow.id)
207
+ // .warn(
208
+ // `workflow ${workflow.id} has already been triggered in stack executions (${context.stack}), and newly triggering will be skipped.`,
209
+ // );
210
+ // return false;
211
+ // }
212
+ // }
213
+ // return true;
214
+ // }
215
+ async execute(workflow, values, options) {
216
+ const ctx = await this.prepare(workflow, values == null ? void 0 : values.data, options);
205
217
  const [dataSourceName] = (0, import_data_source_manager.parseCollectionName)(workflow.config.collection);
206
218
  const { transaction } = options;
207
219
  return this.workflow.trigger(workflow, ctx, {
@@ -209,4 +221,12 @@ class CollectionTrigger extends import__.default {
209
221
  transaction: this.workflow.useDataSourceTransaction(dataSourceName, transaction)
210
222
  });
211
223
  }
224
+ validateContext(values) {
225
+ if (!values.data) {
226
+ return {
227
+ data: "Data is required"
228
+ };
229
+ }
230
+ return null;
231
+ }
212
232
  }
@@ -30,10 +30,14 @@ export default class DateFieldScheduleTrigger {
30
30
  constructor(workflow: Plugin);
31
31
  reload(): Promise<void>;
32
32
  inspect(workflows: WorkflowModel[]): void;
33
- loadRecordsToSchedule({ config: { collection, limit, startsOn, repeat, endsOn }, allExecuted }: WorkflowModel, currentDate: Date): Promise<import("@nocobase/database").Model<any, any>[]>;
33
+ loadRecordsToSchedule({ id, config: { collection, limit, startsOn, repeat, endsOn }, allExecuted }: WorkflowModel, currentDate: Date): Promise<import("@nocobase/database").Model<any, any>[]>;
34
34
  getRecordNextTime(workflow: WorkflowModel, record: any, nextSecond?: boolean): any;
35
35
  schedule(workflow: WorkflowModel, record: any, nextTime: any, toggle?: boolean, options?: {}): Promise<void>;
36
36
  trigger(workflow: WorkflowModel, record: any, nextTime: any, { transaction }?: Transactionable): Promise<void>;
37
37
  on(workflow: WorkflowModel): void;
38
38
  off(workflow: WorkflowModel): void;
39
+ execute(workflow: any, values: any, options: any): Promise<void | import("../..").Processor>;
40
+ validateContext(values: any): {
41
+ data: string;
42
+ };
39
43
  }
@@ -43,6 +43,7 @@ var import_database = require("@nocobase/database");
43
43
  var import_cron_parser = __toESM(require("cron-parser"));
44
44
  var import_utils = require("./utils");
45
45
  var import_data_source_manager = require("@nocobase/data-source-manager");
46
+ var import_lodash = require("lodash");
46
47
  function getOnTimestampWithOffset({ field, offset = 0, unit = 1e3 }, now) {
47
48
  if (!field) {
48
49
  return null;
@@ -64,7 +65,7 @@ function getDataOptionTime(record, on, dir = 1) {
64
65
  if (!field || !record.get(field)) {
65
66
  return null;
66
67
  }
67
- const second = new Date(record.get(field).getTime());
68
+ const second = new Date(record.get(field));
68
69
  second.setMilliseconds(0);
69
70
  return second.getTime() + offset * unit * dir;
70
71
  }
@@ -131,6 +132,7 @@ class DateFieldScheduleTrigger {
131
132
  const now = /* @__PURE__ */ new Date();
132
133
  workflows.forEach(async (workflow) => {
133
134
  const records = await this.loadRecordsToSchedule(workflow, now);
135
+ this.workflow.getLogger(workflow.id).info(`[Schedule on date field] ${records.length} records to schedule`);
134
136
  records.forEach((record) => {
135
137
  const nextTime = this.getRecordNextTime(workflow, record);
136
138
  this.schedule(workflow, record, nextTime, Boolean(nextTime));
@@ -144,17 +146,20 @@ class DateFieldScheduleTrigger {
144
146
  // b. repeat in range (number or cron):
145
147
  // i. endsOn after now -> yes
146
148
  // ii. endsOn before now -> no
147
- async loadRecordsToSchedule({ config: { collection, limit, startsOn, repeat, endsOn }, allExecuted }, currentDate) {
149
+ async loadRecordsToSchedule({ id, config: { collection, limit, startsOn, repeat, endsOn }, allExecuted }, currentDate) {
148
150
  const { dataSourceManager } = this.workflow.app;
149
151
  if (limit && allExecuted >= limit) {
152
+ this.workflow.getLogger(id).warn(`[Schedule on date field] limit reached (all executed ${allExecuted})`);
150
153
  return [];
151
154
  }
152
155
  if (!startsOn) {
156
+ this.workflow.getLogger(id).warn(`[Schedule on date field] "startsOn" is not configured`);
153
157
  return [];
154
158
  }
155
159
  const timestamp = currentDate.getTime();
156
160
  const startTimestamp = getOnTimestampWithOffset(startsOn, currentDate);
157
161
  if (!startTimestamp) {
162
+ this.workflow.getLogger(id).warn(`[Schedule on date field] "startsOn.field" is not configured`);
158
163
  return [];
159
164
  }
160
165
  const [dataSourceName, collectionName] = (0, import_data_source_manager.parseCollectionName)(collection);
@@ -181,7 +186,7 @@ class DateFieldScheduleTrigger {
181
186
  const modExp = (0, import_database.fn)(
182
187
  "MOD",
183
188
  (0, import_database.literal)(
184
- `${Math.round(timestamp / 1e3)} - ${db.sequelize.getQueryInterface().quoteIdentifiers(tsFn(field))}`
189
+ `${Math.round(timestamp / 1e3)} - ${tsFn(db.sequelize.getQueryInterface().quoteIdentifiers(field))}`
185
190
  ),
186
191
  Math.round(repeat / 1e3)
187
192
  );
@@ -193,21 +198,21 @@ class DateFieldScheduleTrigger {
193
198
  }
194
199
  }
195
200
  if (endsOn) {
196
- const now = /* @__PURE__ */ new Date();
197
- const endTimestamp = getOnTimestampWithOffset(endsOn, now);
198
- if (!endTimestamp) {
199
- return [];
200
- }
201
201
  if (typeof endsOn === "string") {
202
- if (endTimestamp <= timestamp) {
202
+ if ((0, import_utils.parseDateWithoutMs)(endsOn) <= timestamp) {
203
203
  return [];
204
204
  }
205
205
  } else {
206
- conditions.push({
207
- [endsOn.field]: {
208
- [import_database.Op.gte]: new Date(endTimestamp)
209
- }
210
- });
206
+ const endTimestamp = getOnTimestampWithOffset(endsOn, currentDate);
207
+ if (endTimestamp) {
208
+ conditions.push({
209
+ [endsOn.field]: {
210
+ [import_database.Op.gte]: new Date(endTimestamp)
211
+ }
212
+ });
213
+ } else {
214
+ this.workflow.getLogger(id).warn(`[Schedule on date field] "endsOn.field" is not configured`);
215
+ }
211
216
  }
212
217
  }
213
218
  } else {
@@ -217,6 +222,7 @@ class DateFieldScheduleTrigger {
217
222
  }
218
223
  });
219
224
  }
225
+ this.workflow.getLogger(id).debug(`[Schedule on date field] conditions: `, { conditions });
220
226
  return model.findAll({
221
227
  where: {
222
228
  [import_database.Op.and]: conditions
@@ -363,4 +369,37 @@ class DateFieldScheduleTrigger {
363
369
  this.events.delete(name);
364
370
  }
365
371
  }
372
+ async execute(workflow, values, options) {
373
+ var _a;
374
+ const [dataSourceName, collectionName] = (0, import_data_source_manager.parseCollectionName)(workflow.config.collection);
375
+ const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName);
376
+ const { filterTargetKey, repository } = collectionManager.getCollection(collectionName);
377
+ let { data } = values;
378
+ let filterByTk;
379
+ let loadNeeded = false;
380
+ if (data && typeof data === "object") {
381
+ filterByTk = Array.isArray(filterTargetKey) ? (0, import_lodash.pick)(
382
+ data,
383
+ filterTargetKey.sort((a, b) => a.localeCompare(b))
384
+ ) : data[filterTargetKey];
385
+ } else {
386
+ filterByTk = data;
387
+ loadNeeded = true;
388
+ }
389
+ if (loadNeeded || ((_a = workflow.config.appends) == null ? void 0 : _a.length)) {
390
+ data = await repository.findOne({
391
+ filterByTk,
392
+ appends: workflow.config.appends
393
+ });
394
+ }
395
+ return this.workflow.trigger(workflow, { ...values, data, date: (values == null ? void 0 : values.date) ?? /* @__PURE__ */ new Date() }, options);
396
+ }
397
+ validateContext(values) {
398
+ if (!(values == null ? void 0 : values.data)) {
399
+ return {
400
+ data: "Data is required"
401
+ };
402
+ }
403
+ return null;
404
+ }
366
405
  }