refactor(plugin-workflow): multiple instances and event management (fix #384) (#408)

* refactor(plugin-workflow): multiple instances and event management (fix #384)

* fix(plugin-workflow): fix test case
This commit is contained in:
Junyi 2022-05-20 16:13:12 +08:00 committed by GitHub
parent 747851f2b1
commit 53482c5b22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 248 additions and 213 deletions

View File

@ -1,7 +1,7 @@
import { CollectionOptions } from '@nocobase/database';
export default {
name: 'approvals',
name: 'comments',
fields: [
{
type: 'belongsTo',

View File

@ -16,6 +16,14 @@ export default {
type: 'integer',
name: 'read',
defaultValue: 0
},
{
type: 'hasMany',
name: 'comments'
},
{
type: 'belongsToMany',
name: 'tags'
}
]
} as CollectionOptions;

View File

@ -0,0 +1,15 @@
import { CollectionOptions } from '@nocobase/database';
export default {
name: 'tags',
fields: [
{
type: 'belongsToMany',
name: 'posts',
},
{
type: 'string',
name: 'name'
}
],
} as CollectionOptions;

View File

@ -8,7 +8,7 @@ jest.setTimeout(300000);
describe('execution', () => {
let app: Application;
let db: Database;
let PostModel;
let PostRepo;
let WorkflowModel;
let workflow;
@ -17,7 +17,7 @@ describe('execution', () => {
db = app.db;
WorkflowModel = db.getCollection('workflows').model;
PostModel = db.getCollection('posts').model;
PostRepo = db.getCollection('posts').repository;
workflow = await WorkflowModel.create({
title: 'test workflow',
@ -34,7 +34,7 @@ describe('execution', () => {
describe('base', () => {
it('empty workflow without any nodes', async () => {
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.context.data.title).toEqual(post.title);
@ -47,7 +47,7 @@ describe('execution', () => {
type: 'echo'
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.RESOLVED);
@ -64,7 +64,7 @@ describe('execution', () => {
type: 'echo'
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.context.data.title).toEqual(post.title);
@ -91,7 +91,7 @@ describe('execution', () => {
await n1.setDownstream(n2);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.context.data.title).toEqual(post.title);
@ -110,7 +110,7 @@ describe('execution', () => {
type: 'error'
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.REJECTED);
@ -138,7 +138,7 @@ describe('execution', () => {
await n1.setDownstream(n2);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.STARTED);
@ -170,7 +170,7 @@ describe('execution', () => {
});
await n1.setDownstream(n2);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.STARTED);
@ -211,7 +211,7 @@ describe('execution', () => {
upstreamId: n1.id
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.RESOLVED);
@ -245,7 +245,7 @@ describe('execution', () => {
await n1.setDownstream(n3);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.STARTED);
@ -280,7 +280,7 @@ describe('execution', () => {
await n1.setDownstream(n3);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.STARTED);
@ -317,7 +317,7 @@ describe('execution', () => {
await n1.setDownstream(n3);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.RESOLVED);
@ -353,7 +353,7 @@ describe('execution', () => {
await n1.setDownstream(n4);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.RESOLVED);
@ -389,7 +389,7 @@ describe('execution', () => {
await n1.setDownstream(n4);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.STARTED);
@ -440,7 +440,7 @@ describe('execution', () => {
await n1.setDownstream(n5);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.STARTED);
@ -492,7 +492,7 @@ describe('execution', () => {
await n1.setDownstream(n5);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.STARTED);
@ -524,9 +524,9 @@ describe('execution', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const posts = await PostModel.findAll();
const posts = await PostRepo.find();
expect(posts.length).toBe(2);
const [execution] = await workflow.getExecutions();

View File

@ -7,7 +7,7 @@ import { getApp } from '..';
describe('workflow > instructions > calculation', () => {
let app: Application;
let db: Database;
let PostModel;
let PostRepo;
let WorkflowModel;
let workflow;
@ -16,7 +16,7 @@ describe('workflow > instructions > calculation', () => {
db = app.db;
WorkflowModel = db.getCollection('workflows').model;
PostModel = db.getCollection('posts').model;
PostRepo = db.getCollection('posts').repository;
workflow = await WorkflowModel.create({
title: 'test workflow',
@ -46,7 +46,7 @@ describe('workflow > instructions > calculation', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.result).toBe(2);
@ -66,7 +66,7 @@ describe('workflow > instructions > calculation', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.result).toBe(1);
@ -86,7 +86,7 @@ describe('workflow > instructions > calculation', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.result).toBe(1);
@ -113,7 +113,7 @@ describe('workflow > instructions > calculation', () => {
await n1.setDownstream(n2);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [n1Job, n2Job] = await execution.getJobs({ order: [['id', 'ASC']]});
expect(n2Job.result).toBe(1);
@ -140,7 +140,7 @@ describe('workflow > instructions > calculation', () => {
await n1.setDownstream(n2);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [n1Job, n2Job] = await execution.getJobs({ order: [['id', 'ASC']]});
expect(n2Job.result).toBe(1);
@ -160,7 +160,7 @@ describe('workflow > instructions > calculation', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.result).toBe(2);
@ -191,7 +191,7 @@ describe('workflow > instructions > calculation', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.result).toBe(-1);

View File

@ -8,7 +8,7 @@ import { EXECUTION_STATUS, BRANCH_INDEX } from '../../constants';
describe('workflow > instructions > condition', () => {
let app: Application;
let db: Database;
let PostModel;
let PostRepo;
let WorkflowModel;
let workflow;
@ -17,7 +17,7 @@ describe('workflow > instructions > condition', () => {
db = app.db;
WorkflowModel = db.getCollection('workflows').model;
PostModel = db.getCollection('posts').model;
PostRepo = db.getCollection('posts').repository;
workflow = await WorkflowModel.create({
title: 'test workflow',
@ -65,7 +65,7 @@ describe('workflow > instructions > condition', () => {
upstreamId: n1.id
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.RESOLVED);
@ -102,7 +102,7 @@ describe('workflow > instructions > condition', () => {
upstreamId: n1.id
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
expect(execution.status).toEqual(EXECUTION_STATUS.RESOLVED);
@ -140,7 +140,7 @@ describe('workflow > instructions > condition', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
@ -169,7 +169,7 @@ describe('workflow > instructions > condition', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
@ -198,7 +198,7 @@ describe('workflow > instructions > condition', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions({ include: ['jobs'] });
const [job] = execution.jobs;
@ -227,7 +227,7 @@ describe('workflow > instructions > condition', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
@ -261,7 +261,7 @@ describe('workflow > instructions > condition', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();

View File

@ -7,7 +7,7 @@ import { getApp } from '..';
describe('workflow > instructions > create', () => {
let app: Application;
let db: Database;
let PostModel;
let PostRepo;
let WorkflowModel;
let workflow;
@ -16,7 +16,7 @@ describe('workflow > instructions > create', () => {
db = app.db;
WorkflowModel = db.getCollection('workflows').model;
PostModel = db.getCollection('posts').model;
PostRepo = db.getCollection('posts').repository;
workflow = await WorkflowModel.create({
title: 'test workflow',
@ -36,7 +36,7 @@ describe('workflow > instructions > create', () => {
const n1 = await workflow.createNode({
type: 'create',
config: {
collection: 'approvals',
collection: 'comments',
params: {
values: {
postId: '{{$context.data.id}}'
@ -45,7 +45,7 @@ describe('workflow > instructions > create', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();

View File

@ -7,7 +7,7 @@ import { getApp } from '..';
describe('workflow > instructions > destroy', () => {
let app: Application;
let db: Database;
let PostModel;
let PostRepo;
let WorkflowModel;
let workflow;
@ -16,7 +16,7 @@ describe('workflow > instructions > destroy', () => {
db = app.db;
WorkflowModel = db.getCollection('workflows').model;
PostModel = db.getCollection('posts').model;
PostRepo = db.getCollection('posts').repository;
workflow = await WorkflowModel.create({
title: 'test workflow',
@ -45,13 +45,13 @@ describe('workflow > instructions > destroy', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.result).toBe(1);
const count = await PostModel.count();
const count = await PostRepo.count();
expect(count).toBe(0);
});
});

View File

@ -7,7 +7,9 @@ import { getApp } from '..';
describe('workflow > instructions > query', () => {
let app: Application;
let db: Database;
let PostModel;
let PostCollection;
let PostRepo;
let TagModel;
let WorkflowModel;
let workflow;
@ -16,7 +18,9 @@ describe('workflow > instructions > query', () => {
db = app.db;
WorkflowModel = db.getCollection('workflows').model;
PostModel = db.getCollection('posts').model;
PostCollection = db.getCollection('posts');
PostRepo = PostCollection.repository;
TagModel = db.getCollection('tags').model;
workflow = await WorkflowModel.create({
title: 'test workflow',
@ -40,7 +44,7 @@ describe('workflow > instructions > query', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
@ -60,7 +64,7 @@ describe('workflow > instructions > query', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
@ -80,7 +84,7 @@ describe('workflow > instructions > query', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
@ -100,7 +104,7 @@ describe('workflow > instructions > query', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
@ -125,13 +129,58 @@ describe('workflow > instructions > query', () => {
});
await n1.setDownstream(n2);
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
expect(jobs[1].result.title).toBe(post.title);
});
it('params.filter: by association field', async () => {
const n1 = await workflow.createNode({
type: 'query',
config: {
collection: 'tags',
params: {
filter: {
'posts.id': `{{$context.data.id}}`
}
}
}
});
const tag = await TagModel.create({ name: 'tag1' });
const post = await PostCollection.repository.create({
values: { title: 't1', tags: [tag.id] }
});
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.result.id).toBe(tag.id);
});
it('params.appends: with associations', async () => {
const n1 = await workflow.createNode({
type: 'query',
config: {
collection: 'tags',
params: {
appends: ['posts']
}
}
});
const tag = await TagModel.create({ name: 'tag1' });
const post = await PostCollection.repository.create({
values: { title: 't1', tags: [tag.id] }
});
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.result.posts.length).toBe(1);
expect(job.result.posts[0].id).toBe(post.id);
});
it('params.sort', async () => {
const n1 = await workflow.createNode({
type: 'query',
@ -143,8 +192,8 @@ describe('workflow > instructions > query', () => {
}
});
const p1 = await PostModel.create({ title: 't1' });
const p2 = await PostModel.create({ title: 't2' });
const p1 = await PostRepo.create({ values: { title: 't1' } });
const p2 = await PostRepo.create({ values: { title: 't2' } });
// get the 2nd execution
const [execution] = await workflow.getExecutions({ order: [['id', 'DESC']] });
@ -164,7 +213,7 @@ describe('workflow > instructions > query', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
@ -186,7 +235,7 @@ describe('workflow > instructions > query', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
@ -208,7 +257,7 @@ describe('workflow > instructions > query', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();

View File

@ -7,7 +7,7 @@ import { getApp } from '..';
describe('workflow > instructions > update', () => {
let app: Application;
let db: Database;
let PostModel;
let PostRepo;
let WorkflowModel;
let workflow;
@ -16,7 +16,7 @@ describe('workflow > instructions > update', () => {
db = app.db;
WorkflowModel = db.getCollection('workflows').model;
PostModel = db.getCollection('posts').model;
PostRepo = db.getCollection('posts').repository;
workflow = await WorkflowModel.create({
title: 'test workflow',
@ -48,14 +48,14 @@ describe('workflow > instructions > update', () => {
}
});
const post = await PostModel.create({ title: 't1' });
const post = await PostRepo.create({ values: { title: 't1' } });
expect(post.published).toBe(false);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.result.published).toBe(true);
const updatedPost = await PostModel.findByPk(post.id);
const updatedPost = await PostRepo.findById(post.id);
expect(updatedPost.published).toBe(true);
});
});
@ -92,9 +92,9 @@ describe('workflow > instructions > update', () => {
await n1.setDownstream(n2);
// NOTE: the result of post immediately created will not be changed by workflow
const { id } = await PostModel.create({ title: 'test' });
const { id } = await PostRepo.create({ values: { title: 'test' } });
// should get from db
const post = await PostModel.findByPk(id);
const post = await PostRepo.findById(id);
expect(post.title).toBe('changed');
});
});

View File

@ -1,6 +1,7 @@
import { Application } from '@nocobase/server';
import Database from '@nocobase/database';
import { getApp } from '.';
import { EXECUTION_STATUS } from '../constants';
@ -8,6 +9,7 @@ describe('workflow > workflow', () => {
let app: Application;
let db: Database;
let PostModel;
let PostRepo;
let WorkflowModel;
beforeEach(async () => {
@ -16,6 +18,7 @@ describe('workflow > workflow', () => {
db = app.db;
WorkflowModel = db.getCollection('workflows').model;
PostModel = db.getCollection('posts').model;
PostRepo = db.getCollection('posts').repository;
});
afterEach(() => db.close());
@ -31,41 +34,11 @@ describe('workflow > workflow', () => {
}
});
const { hooks } = PostModel.options;
expect(hooks.afterCreate.length).toBe(1);
await workflow.update({ enabled: false });
const post = await PostRepo.create({ values: { title: 't1' } });
await workflow.update({
config: {
mode: 2,
collection: 'posts'
}
});
expect(hooks.afterCreate.length).toBe(0);
expect(hooks.afterUpdate.length).toBe(1);
await workflow.update({
config: {
mode: 7,
collection: 'posts'
}
});
expect(hooks.afterCreate.length).toBe(1);
expect(hooks.afterUpdate.length).toBe(1);
expect(hooks.afterDestroy.length).toBe(1);
await workflow.update({
enabled: false
});
expect(hooks.afterCreate.length).toBe(0);
expect(hooks.afterUpdate.length).toBe(0);
expect(hooks.afterDestroy.length).toBe(0);
await workflow.update({
enabled: true
});
expect(hooks.afterCreate.length).toBe(1);
expect(hooks.afterUpdate.length).toBe(1);
expect(hooks.afterDestroy.length).toBe(1);
const executions = await workflow.getExecutions();
expect(executions.length).toBe(0);
});
});
});

View File

@ -8,7 +8,7 @@ function make(name, mod) {
}), {})
}
export default function(app) {
export default function({ app }) {
app.actions({
...make('workflows', workflows),
...make('workflows.nodes', {

View File

@ -1,5 +1,3 @@
import parse from 'json-templates';
import { Context, utils } from '@nocobase/actions';
import { Op } from '@nocobase/database';

View File

@ -13,15 +13,14 @@ export default {
const options = execution.getParsedValue(params);
const result = await (multiple ? repo.find : repo.findOne).call(repo, {
...options,
// NOTE: `raw` to avoid getting undefined value from Proxied model instance (#380)
// e.g. Object.prototype.hasOwnProperty.call(result, 'id') // false
// so the properties can not be get by json-templates(object-path)
raw: true,
transaction: execution.tx
});
// NOTE: `toJSON()` to avoid getting undefined value from Proxied model instance (#380)
// e.g. Object.prototype.hasOwnProperty.call(result, 'id') // false
// so the properties can not be get by json-templates(object-path)
return {
result,
result: multiple ? result.map(item => item.toJSON()) : result?.toJSON(),
status: JOB_STATUS.RESOLVED
};
}

View File

@ -1,7 +1,6 @@
import { Database, Model } from '@nocobase/database';
import { HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin } from 'sequelize';
import triggers from '../triggers';
import { EXECUTION_STATUS } from '../constants';
import ExecutionModel from './Execution';
import FlowNodeModel from './FlowNode';
@ -30,25 +29,6 @@ export default class WorkflowModel extends Model {
declare getExecutions: HasManyGetAssociationsMixin<ExecutionModel>;
declare createExecution: HasManyCreateAssociationMixin<ExecutionModel>;
static async mount() {
const collection = this.database.getCollection('workflows');
const workflows = await collection.repository.find({
filter: { enabled: true },
});
workflows.forEach((workflow: WorkflowModel) => {
workflow.toggle();
});
this.addHook('afterCreate', (model: WorkflowModel) => model.toggle());
this.addHook('afterUpdate', (model: WorkflowModel) => model.toggle());
this.addHook('afterDestroy', (model: WorkflowModel) => model.toggle(false));
}
getHookId() {
return `workflow-${this.get('id')}`;
}
getTransaction(options) {
if (!this.useTransaction) {
return undefined;
@ -59,17 +39,7 @@ export default class WorkflowModel extends Model {
: (<typeof WorkflowModel>this.constructor).database.sequelize.transaction();
}
async toggle(enable?: boolean) {
const type = this.get('type');
const { on, off } = triggers.get(type);
if (typeof enable !== 'undefined' ? enable : this.get('enabled')) {
on.call(this, this.trigger.bind(this));
} else {
off.call(this);
}
}
async trigger(context: Object, options) {
trigger = async (context: Object, options) => {
// `null` means not to trigger
if (context === null) {
return;

View File

@ -4,9 +4,19 @@ import { Plugin } from '@nocobase/server';
import WorkflowModel from './models/Workflow';
import ExecutionModel from './models/Execution';
import actions from './actions';
import initActions from './actions';
import initTriggers, { Trigger } from './triggers';
import { Registry } from '@nocobase/utils';
export default class extends Plugin {
triggers: Registry<Trigger> = new Registry<Trigger>();
getName(): string {
return this.getPackageName(__dirname);
}
export default class WorkflowPlugin extends Plugin {
async load(options = {}) {
const { db } = this.app;
@ -19,47 +29,40 @@ export default class WorkflowPlugin extends Plugin {
directory: path.resolve(__dirname, 'collections'),
});
actions(this.app);
initActions(this);
initTriggers(this);
// [Life Cycle]:
// * load all workflows in db
// * add all hooks for enabled workflows
// * add hooks for create/update[enabled]/delete workflow to add/remove specific hooks
this.app.on('beforeStart', async () => {
const { model } = db.getCollection('workflows');
await (model as typeof WorkflowModel).mount();
const collection = db.getCollection('workflows');
const workflows = await collection.repository.find({
filter: { enabled: true },
});
workflows.forEach((workflow: WorkflowModel) => {
this.toggle(workflow);
});
db.on('workflows.afterCreate', (model: WorkflowModel) => this.toggle(model));
db.on('workflows.afterUpdate', (model: WorkflowModel) => this.toggle(model));
db.on('workflows.afterDestroy', (model: WorkflowModel) => this.toggle(model, false));
});
// [Life Cycle]: initialize all necessary seed data
this.app.on('db.init', async () => {});
// const [Automation, AutomationJob] = database.getModels(['automations', 'automations_jobs']);
// Automation.addHook('afterCreate', async (model: AutomationModel) => {
// model.get('enabled') && await model.loadJobs();
// });
// Automation.addHook('afterUpdate', async (model: AutomationModel) => {
// if (!model.changed('enabled' as any)) {
// return;
// }
// model.get('enabled') ? await model.loadJobs() : await model.cancelJobs();
// });
// Automation.addHook('beforeDestroy', async (model: AutomationModel) => {
// await model.cancelJobs();
// });
// AutomationJob.addHook('afterCreate', async (model: AutomationJobModel) => {
// await model.bootstrap();
// });
// AutomationJob.addHook('beforeDestroy', async (model: AutomationJobModel) => {
// await model.cancel();
// });
// this.app.on('db.init', async () => {});
}
getName(): string {
return this.getPackageName(__dirname);
async toggle(workflow: WorkflowModel, enable?: boolean) {
const type = workflow.get('type');
const trigger = this.triggers.get(type);
if (typeof enable !== 'undefined' ? enable : workflow.get('enabled')) {
await trigger.on(workflow);
} else {
await trigger.off(workflow);
}
}
}

View File

@ -1,6 +1,8 @@
import { Model } from "@nocobase/database";
import { Trigger } from ".";
import WorkflowModel from "../models/Workflow";
export interface ModelChangeTriggerConfig {
export interface CollectionChangeTriggerConfig {
collection: string;
mode: number;
// TODO: ICondition
@ -14,58 +16,78 @@ const MODE_BITMAP = {
};
const MODE_BITMAP_EVENTS = new Map();
MODE_BITMAP_EVENTS.set(MODE_BITMAP.CREATE, 'afterCreate');
MODE_BITMAP_EVENTS.set(MODE_BITMAP.UPDATE, 'afterUpdate');
MODE_BITMAP_EVENTS.set(MODE_BITMAP.CREATE, 'afterCreateWithAssociations');
MODE_BITMAP_EVENTS.set(MODE_BITMAP.UPDATE, 'afterUpdateWithAssociations');
MODE_BITMAP_EVENTS.set(MODE_BITMAP.DESTROY, 'afterDestroy');
// async function, should return promise
function bindHandler(this: WorkflowModel, callback: Function) {
const { condition, changed } = this.config;
return (data: any, options) => {
// NOTE: if no configured fields changed, do not trigger
if (changed && changed.length && changed.every(name => !data.changed(name))) {
return;
}
// NOTE: if no configured condition match, do not trigger
if (condition && condition.$and.length) {
// TODO: check all conditions in condition against data
// const calculation = toCalculation(condition);
}
return callback({ data: data.get() }, options);
};
function getHookId(workflow, type) {
return `${type}#${workflow.get('id')}`;
}
export default {
name: 'collection',
on(this: WorkflowModel, callback: Function) {
const { database } = <typeof WorkflowModel>this.constructor;
const { collection, mode } = this.config;
// async function, should return promise
function handler(this: WorkflowModel, data: Model, options) {
const { condition, changed } = this.config;
// NOTE: if no configured fields changed, do not trigger
if (changed && changed.length && changed.every(name => !data.changed(name))) {
return;
}
// NOTE: if no configured condition match, do not trigger
if (condition && condition.$and.length) {
// TODO: check all conditions in condition against data
// const calculation = toCalculation(condition);
}
return this.trigger({ data: data.get() }, options);
}
export default class CollectionTrigger implements Trigger {
events = new Map();
on(workflow: WorkflowModel) {
const { database } = <typeof WorkflowModel>workflow.constructor;
const { collection, mode } = workflow.config;
const Collection = database.getCollection(collection);
if (!Collection) {
return;
}
for (let [key, event] of MODE_BITMAP_EVENTS.entries()) {
for (let [key, type] of MODE_BITMAP_EVENTS.entries()) {
const event = `${collection}.${type}`;
const name = getHookId(workflow, event);
if (mode & key) {
if (!Collection.model.options.hooks[event]?.find(item => item.name && item.name === this.getHookId())) {
Collection.model.addHook(event, this.getHookId(), bindHandler.call(this, callback));
if (!this.events.has(name)) {
const listener = handler.bind(workflow);
this.events.set(name, listener);
database.on(event, listener);
}
} else {
Collection.model.removeHook(event, this.getHookId());
const listener = this.events.get(name);
if (listener) {
database.off(event, listener);
this.events.delete(name);
}
}
}
},
off(this: WorkflowModel) {
const { database } = <typeof WorkflowModel>this.constructor;
const { collection, mode } = this.config;
}
off(workflow: WorkflowModel) {
const { database } = <typeof WorkflowModel>workflow.constructor;
const { collection, mode } = workflow.config;
const Collection = database.getCollection(collection);
if (!Collection) {
return;
}
for (let [key, event] of MODE_BITMAP_EVENTS.entries()) {
for (let [key, type] of MODE_BITMAP_EVENTS.entries()) {
const event = `${collection}.${type}`;
const name = getHookId(workflow, event);
if (mode & key) {
Collection.model.removeHook(event, this.getHookId());
const listener = this.events.get(name);
if (listener) {
database.off(event, listener);
this.events.delete(name);
}
}
}
}

View File

@ -1,15 +1,13 @@
import { Registry } from '@nocobase/utils';
import WorkflowModel from '../models/Workflow';
import collectionlTrigger from './collection';
import Collection from './collection';
// import Schedule from './schedule';
export interface Trigger {
name: string;
on(this: WorkflowModel, callback: Function): void;
off(this: WorkflowModel): void;
on(workflow: WorkflowModel): void;
off(workflow: WorkflowModel): void;
}
export const triggers = new Registry<Trigger>();
export default triggers;
triggers.register(collectionlTrigger.name, collectionlTrigger);
export default function({ triggers }) {
triggers.register('collection', new Collection());
// triggers.register('schedule', new Schedule());
}