@nocobase/plugin-workflow 1.9.0-beta.8 → 2.0.0-alpha.2

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 (37) hide show
  1. package/dist/client/{9e124936e3877c66.js → 27bd65abee87cafa.js} +1 -1
  2. package/dist/client/4985975bcaea35eb.js +10 -0
  3. package/dist/client/91bf4b18d5aad6a7.js +10 -0
  4. package/dist/client/components/TriggerWorkflowSelect.d.ts +10 -0
  5. package/dist/client/components/index.d.ts +1 -0
  6. package/dist/client/{2a8332e23037d42f.js → f68fbc145c3ddec3.js} +1 -1
  7. package/dist/client/flows/triggerWorkflows.d.ts +120 -0
  8. package/dist/client/index.d.ts +1 -0
  9. package/dist/client/index.js +1 -1
  10. package/dist/client/schemas/executions.d.ts +8 -8
  11. package/dist/client/triggers/index.d.ts +1 -1
  12. package/dist/common/collections/executions.d.ts +8 -8
  13. package/dist/common/collections/executions.js +2 -2
  14. package/dist/common/collections/flow_nodes.d.ts +21 -0
  15. package/dist/common/collections/flow_nodes.js +6 -0
  16. package/dist/common/collections/userWorkflowTasks.d.ts +13 -0
  17. package/dist/common/collections/userWorkflowTasks.js +6 -0
  18. package/dist/common/collections/workflowCategories.d.ts +21 -0
  19. package/dist/common/collections/workflowCategories.js +6 -0
  20. package/dist/common/collections/workflows.d.ts +42 -0
  21. package/dist/common/collections/workflows.js +6 -0
  22. package/dist/externalVersion.js +16 -16
  23. package/dist/node_modules/cron-parser/package.json +1 -1
  24. package/dist/node_modules/lru-cache/package.json +1 -1
  25. package/dist/node_modules/nodejs-snowflake/package.json +1 -1
  26. package/dist/server/Dispatcher.d.ts +47 -1
  27. package/dist/server/Dispatcher.js +368 -1
  28. package/dist/server/Plugin.d.ts +4 -24
  29. package/dist/server/Plugin.js +16 -316
  30. package/dist/server/index.d.ts +2 -1
  31. package/dist/server/index.js +0 -2
  32. package/dist/server/triggers/CollectionTrigger.d.ts +1 -1
  33. package/dist/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.d.ts +5 -5
  34. package/dist/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.js +33 -13
  35. package/package.json +16 -16
  36. package/dist/client/3b0762a72796b5f8.js +0 -10
  37. package/dist/client/48fc0fadf459229d.js +0 -10
@@ -40,16 +40,14 @@ __export(Plugin_exports, {
40
40
  });
41
41
  module.exports = __toCommonJS(Plugin_exports);
42
42
  var import_path = __toESM(require("path"));
43
- var import_crypto = require("crypto");
44
43
  var import_nodejs_snowflake = require("nodejs-snowflake");
45
- var import_sequelize = require("sequelize");
46
44
  var import_lru_cache = __toESM(require("lru-cache"));
47
45
  var import_database = require("@nocobase/database");
48
46
  var import_server = require("@nocobase/server");
49
47
  var import_utils = require("@nocobase/utils");
48
+ var import_Dispatcher = __toESM(require("./Dispatcher"));
50
49
  var import_Processor = __toESM(require("./Processor"));
51
50
  var import_actions = __toESM(require("./actions"));
52
- var import_constants = require("./constants");
53
51
  var import_functions = __toESM(require("./functions"));
54
52
  var import_CollectionTrigger = __toESM(require("./triggers/CollectionTrigger"));
55
53
  var import_ScheduleTrigger = __toESM(require("./triggers/ScheduleTrigger"));
@@ -61,37 +59,16 @@ var import_DestroyInstruction = __toESM(require("./instructions/DestroyInstructi
61
59
  var import_QueryInstruction = __toESM(require("./instructions/QueryInstruction"));
62
60
  var import_UpdateInstruction = __toESM(require("./instructions/UpdateInstruction"));
63
61
  var import_WorkflowRepository = __toESM(require("./repositories/WorkflowRepository"));
64
- const WORKER_JOB_WORKFLOW_PROCESS = "workflow:process";
65
62
  class PluginWorkflowServer extends import_server.Plugin {
66
63
  instructions = new import_utils.Registry();
67
64
  triggers = new import_utils.Registry();
68
65
  functions = new import_utils.Registry();
69
66
  enabledCache = /* @__PURE__ */ new Map();
70
67
  snowflake;
71
- ready = false;
72
- executing = null;
73
- pending = [];
74
- events = [];
75
- eventsCount = 0;
68
+ dispatcher = new import_Dispatcher.default(this);
76
69
  loggerCache;
77
70
  meter = null;
78
71
  checker = null;
79
- onQueueExecution = async (event) => {
80
- const ExecutionRepo = this.db.getRepository("executions");
81
- const execution = await ExecutionRepo.findOne({
82
- filterByTk: event.executionId
83
- });
84
- if (!execution || execution.dispatched) {
85
- this.getLogger("dispatcher").info(
86
- `execution (${event.executionId}) from queue not found or not in queueing status, skip`
87
- );
88
- return;
89
- }
90
- this.getLogger(execution.workflowId).info(
91
- `execution (${execution.id}) received from queue, adding to pending list`
92
- );
93
- this.run(execution);
94
- };
95
72
  onBeforeSave = async (instance, { transaction, cycling }) => {
96
73
  if (cycling) {
97
74
  return;
@@ -161,7 +138,7 @@ class PluginWorkflowServer extends import_server.Plugin {
161
138
  // * add all hooks for enabled workflows
162
139
  // * add hooks for create/update[enabled]/delete workflow to add/remove specific hooks
163
140
  onAfterStart = async () => {
164
- this.ready = true;
141
+ this.dispatcher.setReady(true);
165
142
  const collection = this.db.getCollection("workflows");
166
143
  const workflows = await collection.repository.find({
167
144
  appends: ["versionStats"]
@@ -182,28 +159,22 @@ class PluginWorkflowServer extends import_server.Plugin {
182
159
  }
183
160
  this.checker = setInterval(() => {
184
161
  this.getLogger("dispatcher").debug(`(cycling) check for queueing executions`);
185
- this.dispatch();
162
+ this.dispatcher.dispatch();
186
163
  }, 3e5);
187
164
  this.app.on("workflow:dispatch", () => {
188
165
  this.app.logger.info("workflow:dispatch");
189
- this.dispatch();
166
+ this.dispatcher.dispatch();
190
167
  });
191
168
  this.getLogger("dispatcher").info("(starting) check for queueing executions");
192
- this.dispatch();
193
- this.ready = true;
169
+ this.dispatcher.dispatch();
170
+ this.dispatcher.setReady(true);
194
171
  };
195
172
  onBeforeStop = async () => {
196
173
  this.app.logger.info(`stopping workflow plugin before app (${this.app.name}) shutdown...`);
197
174
  for (const workflow of this.enabledCache.values()) {
198
175
  this.toggle(workflow, false, { silent: true });
199
176
  }
200
- this.ready = false;
201
- if (this.events.length) {
202
- await this.prepare();
203
- }
204
- if (this.executing) {
205
- await this.executing;
206
- }
177
+ await this.dispatcher.beforeStop();
207
178
  if (this.checker) {
208
179
  clearInterval(this.checker);
209
180
  }
@@ -309,8 +280,8 @@ class PluginWorkflowServer extends import_server.Plugin {
309
280
  custom_epoch: pluginRecord == null ? void 0 : pluginRecord.createdAt.getTime()
310
281
  });
311
282
  this.app.backgroundJobManager.subscribe(`${this.name}.pendingExecution`, {
312
- idle: () => this.app.serving(WORKER_JOB_WORKFLOW_PROCESS) && !this.executing && !this.pending.length && !this.events.length,
313
- process: this.onQueueExecution
283
+ idle: () => this.app.serving(import_Dispatcher.WORKER_JOB_WORKFLOW_PROCESS) && this.dispatcher.idle,
284
+ process: this.dispatcher.onQueueExecution
314
285
  });
315
286
  }
316
287
  /**
@@ -332,7 +303,7 @@ class PluginWorkflowServer extends import_server.Plugin {
332
303
  this.meter = this.app.telemetry.metric.getMeter();
333
304
  const counter = this.meter.createObservableGauge("workflow.events.counter");
334
305
  counter.addCallback((result) => {
335
- result.observe(this.eventsCount);
306
+ result.observe(this.dispatcher.getEventsCount());
336
307
  });
337
308
  this.app.acl.registerSnippet({
338
309
  name: `pm.${this.name}.workflows`,
@@ -397,295 +368,24 @@ class PluginWorkflowServer extends import_server.Plugin {
397
368
  }
398
369
  }
399
370
  trigger(workflow, context, options = {}) {
400
- const logger = this.getLogger(workflow.id);
401
- if (!this.ready) {
402
- logger.warn(`app is not ready, event of workflow ${workflow.id} will be ignored`);
403
- logger.debug(`ignored event data:`, context);
404
- return;
405
- }
406
- if (!options.force && !options.manually && !workflow.enabled) {
407
- logger.warn(`workflow ${workflow.id} is not enabled, event will be ignored`);
408
- return;
409
- }
410
- const duplicated = this.events.find(([w, c, { eventKey }]) => {
411
- if (eventKey && options.eventKey) {
412
- return eventKey === options.eventKey;
413
- }
414
- });
415
- if (duplicated) {
416
- logger.warn(`event of workflow ${workflow.id} is duplicated (${options.eventKey}), event will be ignored`);
417
- return;
418
- }
419
- if (context == null) {
420
- logger.warn(`workflow ${workflow.id} event data context is null, event will be ignored`);
421
- return;
422
- }
423
- if (options.manually || this.isWorkflowSync(workflow)) {
424
- return this.triggerSync(workflow, context, options);
425
- }
426
- const { transaction, ...rest } = options;
427
- this.events.push([workflow, context, rest]);
428
- this.eventsCount = this.events.length;
429
- logger.info(`new event triggered, now events: ${this.events.length}`);
430
- logger.debug(`event data:`, { context });
431
- if (this.events.length > 1) {
432
- logger.info(`new event is pending to be prepared after previous preparation is finished`);
433
- return;
434
- }
435
- setImmediate(this.prepare);
371
+ return this.dispatcher.trigger(workflow, context, options);
436
372
  }
437
- async triggerSync(workflow, context, { deferred, ...options } = {}) {
438
- let execution;
439
- try {
440
- execution = await this.createExecution(workflow, context, options);
441
- } catch (err) {
442
- this.getLogger(workflow.id).error(`creating execution failed: ${err.message}`, err);
443
- return null;
444
- }
445
- try {
446
- return this.process(execution, null, options);
447
- } catch (err) {
448
- this.getLogger(execution.workflowId).error(`execution (${execution.id}) error: ${err.message}`, err);
449
- }
450
- return null;
451
- }
452
- async run(execution, job) {
453
- while (this.executing) {
454
- await this.executing;
455
- }
456
- this.executing = this.process(execution, job);
457
- await this.executing;
458
- this.executing = null;
459
- this.dispatch();
373
+ async run(pending) {
374
+ return this.dispatcher.run(pending);
460
375
  }
461
376
  async resume(job) {
462
- let { execution } = job;
463
- if (!execution) {
464
- execution = await job.getExecution();
465
- }
466
- this.getLogger(execution.workflowId).info(
467
- `execution (${execution.id}) resuming from job (${job.id}) added to pending list`
468
- );
469
- this.run(execution, job);
377
+ return this.dispatcher.resume(job);
470
378
  }
471
379
  /**
472
380
  * Start a deferred execution
473
381
  * @experimental
474
382
  */
475
383
  async start(execution) {
476
- if (execution.status) {
477
- return;
478
- }
479
- this.getLogger(execution.workflowId).info(`starting deferred execution (${execution.id})`);
480
- this.run(execution);
481
- }
482
- async validateEvent(workflow, context, options) {
483
- const trigger = this.triggers.get(workflow.type);
484
- const triggerValid = await trigger.validateEvent(workflow, context, options);
485
- if (!triggerValid) {
486
- return false;
487
- }
488
- const { stack } = options;
489
- let valid = true;
490
- if ((stack == null ? void 0 : stack.length) > 0) {
491
- const existed = await workflow.countExecutions({
492
- where: {
493
- id: stack
494
- },
495
- transaction: options.transaction
496
- });
497
- const limitCount = workflow.options.stackLimit || 1;
498
- if (existed >= limitCount) {
499
- this.getLogger(workflow.id).warn(
500
- `workflow ${workflow.id} has already been triggered in stacks executions (${stack}), and max call coont is ${limitCount}, newly triggering will be skipped.`
501
- );
502
- valid = false;
503
- }
504
- }
505
- return valid;
506
- }
507
- async createExecution(workflow, context, options) {
508
- var _a;
509
- const { deferred } = options;
510
- const transaction = await this.useDataSourceTransaction("main", options.transaction, true);
511
- const sameTransaction = options.transaction === transaction;
512
- const valid = await this.validateEvent(workflow, context, { ...options, transaction });
513
- if (!valid) {
514
- if (!sameTransaction) {
515
- await transaction.commit();
516
- }
517
- (_a = options.onTriggerFail) == null ? void 0 : _a.call(options, workflow, context, options);
518
- return Promise.reject(new Error("event is not valid"));
519
- }
520
- let execution;
521
- try {
522
- execution = await workflow.createExecution(
523
- {
524
- context,
525
- key: workflow.key,
526
- eventKey: options.eventKey ?? (0, import_crypto.randomUUID)(),
527
- stack: options.stack,
528
- dispatched: deferred ?? false
529
- },
530
- { transaction }
531
- );
532
- } catch (err) {
533
- if (!sameTransaction) {
534
- await transaction.rollback();
535
- }
536
- throw err;
537
- }
538
- this.getLogger(workflow.id).info(`execution of workflow ${workflow.id} created as ${execution.id}`);
539
- if (!workflow.stats) {
540
- workflow.stats = await workflow.getStats({ transaction });
541
- }
542
- await workflow.stats.increment("executed", { transaction });
543
- if (this.db.options.dialect !== "postgres") {
544
- await workflow.stats.reload({ transaction });
545
- }
546
- if (!workflow.versionStats) {
547
- workflow.versionStats = await workflow.getVersionStats({ transaction });
548
- }
549
- await workflow.versionStats.increment("executed", { transaction });
550
- if (this.db.options.dialect !== "postgres") {
551
- await workflow.versionStats.reload({ transaction });
552
- }
553
- if (!sameTransaction) {
554
- await transaction.commit();
555
- }
556
- execution.workflow = workflow;
557
- return execution;
558
- }
559
- prepare = async () => {
560
- if (this.executing && this.db.options.dialect === "sqlite") {
561
- await this.executing;
562
- }
563
- const event = this.events.shift();
564
- this.eventsCount = this.events.length;
565
- if (!event) {
566
- this.getLogger("dispatcher").info(`events queue is empty, no need to prepare`);
567
- return;
568
- }
569
- const logger = this.getLogger(event[0].id);
570
- logger.info(`preparing execution for event`);
571
- try {
572
- const execution = await this.createExecution(...event);
573
- if (!(execution == null ? void 0 : execution.dispatched)) {
574
- if (!this.executing && !this.pending.length) {
575
- logger.info(`local pending list is empty, adding execution (${execution.id}) to pending list`);
576
- this.pending.push([execution]);
577
- } else {
578
- logger.info(`local pending list is not empty, sending execution (${execution.id}) to queue`);
579
- if (this.ready) {
580
- this.app.backgroundJobManager.publish(`${this.name}.pendingExecution`, { executionId: execution.id });
581
- }
582
- }
583
- }
584
- } catch (error) {
585
- logger.error(`failed to create execution:`, { error });
586
- }
587
- if (this.events.length) {
588
- await this.prepare();
589
- } else {
590
- this.getLogger("dispatcher").info("no more events need to be prepared, dispatching...");
591
- if (this.executing) {
592
- await this.executing;
593
- }
594
- this.dispatch();
595
- }
596
- };
597
- dispatch() {
598
- if (!this.ready) {
599
- this.getLogger("dispatcher").warn(`app is not ready, new dispatching will be ignored`);
600
- return;
601
- }
602
- if (!this.app.serving(WORKER_JOB_WORKFLOW_PROCESS)) {
603
- this.getLogger("dispatcher").warn(
604
- `${WORKER_JOB_WORKFLOW_PROCESS} is not serving, new dispatching will be ignored`
605
- );
606
- return;
607
- }
608
- if (this.executing) {
609
- this.getLogger("dispatcher").warn(`workflow executing is not finished, new dispatching will be ignored`);
610
- return;
611
- }
612
- if (this.events.length) {
613
- return this.prepare();
614
- }
615
- this.executing = (async () => {
616
- let next = null;
617
- if (this.pending.length) {
618
- next = this.pending.shift();
619
- this.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`);
620
- } else {
621
- try {
622
- await this.db.sequelize.transaction(
623
- {
624
- isolationLevel: this.db.options.dialect === "sqlite" ? [][0] : import_sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ
625
- },
626
- async (transaction) => {
627
- const execution = await this.db.getRepository("executions").findOne({
628
- filter: {
629
- dispatched: false,
630
- "workflow.enabled": true
631
- },
632
- sort: "id",
633
- transaction
634
- });
635
- if (execution) {
636
- this.getLogger(execution.workflowId).info(`execution (${execution.id}) fetched from db`);
637
- await execution.update(
638
- {
639
- dispatched: true,
640
- status: import_constants.EXECUTION_STATUS.STARTED
641
- },
642
- { transaction }
643
- );
644
- execution.workflow = this.enabledCache.get(execution.workflowId);
645
- next = [execution];
646
- } else {
647
- this.getLogger("dispatcher").debug(`no execution in db queued to process`);
648
- }
649
- }
650
- );
651
- } catch (error) {
652
- this.getLogger("dispatcher").error(`fetching execution from db failed: ${error.message}`, { error });
653
- }
654
- }
655
- if (next) {
656
- await this.process(...next);
657
- }
658
- this.executing = null;
659
- if (next || this.pending.length) {
660
- this.getLogger("dispatcher").debug(`last process finished, will do another dispatch`);
661
- this.dispatch();
662
- }
663
- })();
384
+ return this.dispatcher.start(execution);
664
385
  }
665
386
  createProcessor(execution, options = {}) {
666
387
  return new import_Processor.default(execution, { ...options, plugin: this });
667
388
  }
668
- async process(execution, job, options = {}) {
669
- var _a, _b;
670
- const logger = this.getLogger(execution.workflowId);
671
- if (!execution.dispatched) {
672
- const transaction = await this.useDataSourceTransaction("main", options.transaction);
673
- await execution.update({ dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED }, { transaction });
674
- logger.info(`execution (${execution.id}) from pending list updated to started`);
675
- }
676
- const processor = this.createProcessor(execution, options);
677
- logger.info(`execution (${execution.id}) ${job ? "resuming" : "starting"}...`);
678
- try {
679
- await (job ? processor.resume(job) : processor.start());
680
- logger.info(`execution (${execution.id}) finished with status: ${execution.status}`, { execution });
681
- if (execution.status && ((_b = (_a = execution.workflow.options) == null ? void 0 : _a.deleteExecutionOnStatus) == null ? void 0 : _b.includes(execution.status))) {
682
- await execution.destroy({ transaction: processor.mainTransaction });
683
- }
684
- } catch (err) {
685
- logger.error(`execution (${execution.id}) error: ${err.message}`, err);
686
- }
687
- return processor;
688
- }
689
389
  async execute(workflow, values, options = {}) {
690
390
  const trigger = this.triggers.get(workflow.type);
691
391
  if (!trigger) {
@@ -12,6 +12,7 @@ export * from './instructions';
12
12
  export * from './functions';
13
13
  export * from './logicCalculate';
14
14
  export { Trigger } from './triggers';
15
+ export type { EventOptions } from './Dispatcher';
15
16
  export { default as Processor } from './Processor';
16
- export { default, EventOptions } from './Plugin';
17
+ export { default } from './Plugin';
17
18
  export * from './types';
@@ -37,7 +37,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
37
37
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
38
  var server_exports = {};
39
39
  __export(server_exports, {
40
- EventOptions: () => import_Plugin.EventOptions,
41
40
  Processor: () => import_Processor.default,
42
41
  Trigger: () => import_triggers.Trigger,
43
42
  default: () => import_Plugin.default
@@ -54,7 +53,6 @@ var import_Plugin = __toESM(require("./Plugin"));
54
53
  __reExport(server_exports, require("./types"), module.exports);
55
54
  // Annotate the CommonJS export names for ESM import in node:
56
55
  0 && (module.exports = {
57
- EventOptions,
58
56
  Processor,
59
57
  Trigger,
60
58
  ...require("./utils"),
@@ -9,7 +9,7 @@
9
9
  import { Model } from '@nocobase/database';
10
10
  import Trigger from '.';
11
11
  import type { WorkflowModel } from '../types';
12
- import type { EventOptions } from '../Plugin';
12
+ import type { EventOptions } from '../Dispatcher';
13
13
  export interface CollectionChangeTriggerConfig {
14
14
  collection: string;
15
15
  mode: number;
@@ -6,7 +6,7 @@
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 { Transactionable } from '@nocobase/database';
9
+ import { Model, Transactionable } from '@nocobase/database';
10
10
  import type Plugin from '../../Plugin';
11
11
  import type { WorkflowModel } from '../../types';
12
12
  export type ScheduleOnField = {
@@ -32,10 +32,10 @@ export default class DateFieldScheduleTrigger {
32
32
  constructor(workflow: Plugin);
33
33
  reload(): void;
34
34
  inspect(workflow: WorkflowModel): Promise<void>;
35
- loadRecordsToSchedule({ id, config: { collection, limit, startsOn, repeat, endsOn }, stats }: WorkflowModel, currentDate: Date): Promise<import("@nocobase/database").Model<any, any>[]>;
36
- getRecordNextTime(workflow: WorkflowModel, record: any, nextSecond?: boolean): any;
37
- schedule(workflow: WorkflowModel, record: any, nextTime: any, toggle?: boolean, options?: {}): Promise<void>;
38
- trigger(workflow: WorkflowModel, record: any, nextTime: any, { transaction }?: Transactionable): Promise<void>;
35
+ loadRecordsToSchedule({ id, config: { collection, limit, startsOn, repeat, endsOn }, stats }: WorkflowModel, currentDate: Date): Promise<Model<any, any>[]>;
36
+ getRecordNextTime(workflow: WorkflowModel, record: Model, nextSecond?: boolean): any;
37
+ schedule(workflow: WorkflowModel, record: Model, nextTime: number, toggle?: boolean, options?: {}): Promise<void>;
38
+ trigger(workflow: WorkflowModel, record: Model, nextTime: number, { transaction }?: Transactionable): Promise<void>;
39
39
  on(workflow: WorkflowModel): void;
40
40
  off(workflow: WorkflowModel): void;
41
41
  execute(workflow: any, values: any, options: any): Promise<void | import("../..").Processor>;
@@ -45,7 +45,7 @@ var import_utils = require("./utils");
45
45
  var import_data_source_manager = require("@nocobase/data-source-manager");
46
46
  var import_lodash = require("lodash");
47
47
  var import_utils2 = require("../../utils");
48
- function getOnTimestampWithOffset({ field, offset = 0, unit = 1e3 }, now) {
48
+ function getOnTimestampWithOffset({ field, offset = 0, unit = 864e5 }, now) {
49
49
  if (!field) {
50
50
  return null;
51
51
  }
@@ -62,7 +62,7 @@ function getDataOptionTime(record, on, dir = 1) {
62
62
  return time ? time : null;
63
63
  }
64
64
  case "object": {
65
- const { field, offset = 0, unit = 1e3 } = on;
65
+ const { field, offset = 0, unit = 864e5 } = on;
66
66
  if (!field || !record.get(field)) {
67
67
  return null;
68
68
  }
@@ -188,13 +188,14 @@ class DateFieldScheduleTrigger {
188
188
  if (typeof repeat === "number") {
189
189
  const tsFn = DialectTimestampFnMap[db.options.dialect];
190
190
  if (repeat > range && tsFn) {
191
+ const offsetSeconds = Math.round((startsOn.offset || 0) * (startsOn.unit || 1e3) / 1e3);
192
+ const repeatSeconds = Math.round(repeat / 1e3);
193
+ const nowSeconds = Math.round(timestamp / 1e3);
191
194
  const { field } = model.getAttributes()[startsOn.field];
192
- const modExp = (0, import_database.fn)(
193
- "MOD",
194
- (0, import_database.literal)(
195
- `${Math.round(timestamp / 1e3)} - ${tsFn(db.sequelize.getQueryInterface().quoteIdentifiers(field))}`
196
- ),
197
- Math.round(repeat / 1e3)
195
+ const modExp = (0, import_database.literal)(
196
+ `MOD(MOD(${tsFn(
197
+ db.sequelize.getQueryInterface().quoteIdentifiers(field)
198
+ )} + ${offsetSeconds} - ${nowSeconds}, ${repeatSeconds}) + ${repeatSeconds}, ${repeatSeconds})`
198
199
  );
199
200
  conditions.push((0, import_database.where)(modExp, { [import_database.Op.lt]: Math.round(range / 1e3) }));
200
201
  }
@@ -241,6 +242,7 @@ class DateFieldScheduleTrigger {
241
242
  if (limit && stats.executed >= limit) {
242
243
  return null;
243
244
  }
245
+ const logger = this.workflow.getLogger(workflow.id);
244
246
  const range = this.cacheCycle;
245
247
  const now = /* @__PURE__ */ new Date();
246
248
  now.setMilliseconds(nextSecond ? 1e3 : 0);
@@ -249,37 +251,50 @@ class DateFieldScheduleTrigger {
249
251
  const endTime = getDataOptionTime(record, endsOn);
250
252
  let nextTime = null;
251
253
  if (!startTime) {
254
+ logger.debug(`[Schedule on date field] getNextTime: startsOn not configured`);
252
255
  return null;
253
256
  }
254
257
  if (startTime > timestamp + range) {
258
+ logger.debug(`[Schedule on date field] getNextTime: startsOn is out of caching window`);
255
259
  return null;
256
260
  }
257
261
  if (startTime >= timestamp) {
258
- return !endTime || endTime >= startTime && endTime < timestamp + range ? startTime : null;
262
+ if (!endTime || startTime <= endTime) {
263
+ return startTime;
264
+ }
265
+ logger.debug(`[Schedule on date field] getNextTime: endsOn is before startsOn or out of caching window`);
266
+ return null;
259
267
  } else {
260
268
  if (!repeat) {
269
+ logger.debug(
270
+ `[Schedule on date field] getNextTime: startsOn is before current time and repeat is not configured`
271
+ );
261
272
  return null;
262
273
  }
263
274
  }
264
275
  if (typeof repeat === "number") {
265
- const nextRepeatTime = (startTime - timestamp) % repeat + repeat;
266
- if (nextRepeatTime > range) {
276
+ nextTime = timestamp + repeat - (timestamp - startTime) % repeat;
277
+ if (nextTime - timestamp > range) {
278
+ logger.debug(`[Schedule on date field] getNextTime: nextTime (${nextTime}) is out of caching window`);
267
279
  return null;
268
280
  }
269
- if (endTime && endTime < timestamp + nextRepeatTime) {
281
+ if (endTime && endTime < nextTime) {
282
+ logger.debug(`[Schedule on date field] getNextTime: nextTime is after endsOn`);
270
283
  return null;
271
284
  }
272
- nextTime = timestamp + nextRepeatTime;
273
285
  } else if (typeof repeat === "string") {
274
286
  nextTime = getCronNextTime(repeat, now);
275
287
  if (nextTime - timestamp > range) {
288
+ logger.debug(`[Schedule on date field] getNextTime: nextTime (${nextTime}) is out of caching window`);
276
289
  return null;
277
290
  }
278
291
  if (endTime && endTime < nextTime) {
292
+ logger.debug(`[Schedule on date field] getNextTime: nextTime is after endsOn`);
279
293
  return null;
280
294
  }
281
295
  }
282
296
  if (endTime && endTime <= timestamp) {
297
+ logger.debug(`[Schedule on date field] getNextTime: nextTime is after endsOn`);
283
298
  return null;
284
299
  }
285
300
  return nextTime;
@@ -316,6 +331,10 @@ class DateFieldScheduleTrigger {
316
331
  appends: workflow.config.appends,
317
332
  transaction
318
333
  });
334
+ if (!data) {
335
+ this.workflow.getLogger(workflow.id).warn(`[Schedule on date field] record (${recordPk}) not exists, will not trigger`);
336
+ return;
337
+ }
319
338
  const eventKey = `${workflow.id}:${recordPk}@${nextTime}`;
320
339
  this.cache.delete(eventKey);
321
340
  const json = (0, import_utils2.toJSON)(data);
@@ -355,6 +374,7 @@ class DateFieldScheduleTrigger {
355
374
  }
356
375
  const listener = async (data, { transaction }) => {
357
376
  const nextTime = this.getRecordNextTime(workflow, data);
377
+ this.workflow.getLogger().debug(`[Schedule on date field] record saved, nextTime: ${nextTime}`);
358
378
  return this.schedule(workflow, data, nextTime, Boolean(nextTime), { transaction });
359
379
  };
360
380
  this.events.set(name, listener);
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "displayName.zh-CN": "工作流",
5
5
  "description": "A powerful BPM tool that provides foundational support for business automation, with the capability to extend unlimited triggers and nodes.",
6
6
  "description.zh-CN": "一个强大的 BPM 工具,为业务自动化提供基础支持,并且可任意扩展更多的触发器和节点。",
7
- "version": "1.9.0-beta.8",
7
+ "version": "2.0.0-alpha.2",
8
8
  "license": "AGPL-3.0",
9
9
  "main": "./dist/server/index.js",
10
10
  "homepage": "https://docs.nocobase.com/handbook/workflow",
@@ -30,22 +30,22 @@
30
30
  "tinybench": "4.x"
31
31
  },
32
32
  "peerDependencies": {
33
- "@nocobase/actions": "1.x",
34
- "@nocobase/client": "1.x",
35
- "@nocobase/database": "1.x",
36
- "@nocobase/evaluators": "1.x",
37
- "@nocobase/logger": "1.x",
38
- "@nocobase/plugin-data-source-main": "1.x",
39
- "@nocobase/plugin-error-handler": "1.x",
40
- "@nocobase/plugin-mobile": "1.x",
41
- "@nocobase/plugin-users": "1.x",
42
- "@nocobase/plugin-workflow-test": "1.x",
43
- "@nocobase/resourcer": "1.x",
44
- "@nocobase/server": "1.x",
45
- "@nocobase/test": "1.x",
46
- "@nocobase/utils": "1.x"
33
+ "@nocobase/actions": "2.x",
34
+ "@nocobase/client": "2.x",
35
+ "@nocobase/database": "2.x",
36
+ "@nocobase/evaluators": "2.x",
37
+ "@nocobase/logger": "2.x",
38
+ "@nocobase/plugin-data-source-main": "2.x",
39
+ "@nocobase/plugin-error-handler": "2.x",
40
+ "@nocobase/plugin-mobile": "2.x",
41
+ "@nocobase/plugin-users": "2.x",
42
+ "@nocobase/plugin-workflow-test": "2.x",
43
+ "@nocobase/resourcer": "2.x",
44
+ "@nocobase/server": "2.x",
45
+ "@nocobase/test": "2.x",
46
+ "@nocobase/utils": "2.x"
47
47
  },
48
- "gitHead": "f3d4f3d1ba7adbf4d4c60e656c23da45565769c8",
48
+ "gitHead": "1322f486b248bef53ed8c8f42f0a39dfd02125fd",
49
49
  "keywords": [
50
50
  "Workflow"
51
51
  ]