refactor(plugin-workflow): add revision column to execution (#491)

This commit is contained in:
Junyi 2022-06-09 16:40:10 +08:00 committed by GitHub
parent e57e60e6cb
commit 7839e78164
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 147 additions and 87 deletions

View File

@ -11,7 +11,7 @@ export const ExecutionResourceProvider = ({ request, ...others }) => {
...request?.params, ...request?.params,
filter: { filter: {
...(request?.params?.filter), ...(request?.params?.filter),
workflowId: workflow.id key: workflow.key
} }
} }
}, },

View File

@ -8,7 +8,6 @@ import {
useAPIClient, useAPIClient,
useCompile, useCompile,
useDocumentTitle, useDocumentTitle,
useRecord,
useResourceActionContext, useResourceActionContext,
useResourceContext useResourceContext
} from '..'; } from '..';
@ -79,13 +78,13 @@ export function WorkflowCanvas() {
refresh(); refresh();
} }
async function onDuplicate() { async function onRevision() {
const { data: { data: duplicated } } = await resource.duplicate({ const { data: { data: revision } } = await resource.revision({
filterByTk: workflow[targetKey] filterByTk: workflow[targetKey]
}); });
message.success(t('Operation succeeded')); message.success(t('Operation succeeded'));
history.push(`${duplicated.id}`); history.push(`${revision.id}`);
} }
return ( return (
@ -134,7 +133,7 @@ export function WorkflowCanvas() {
/> />
{workflow.executed && !revisions.find(item => !item.executed && new Date(item.createdAt) > new Date(workflow.createdAt)) {workflow.executed && !revisions.find(item => !item.executed && new Date(item.createdAt) > new Date(workflow.createdAt))
? ( ? (
<Button onClick={onDuplicate}>{t('Copy to new version')}</Button> <Button onClick={onRevision}>{t('Copy to new version')}</Button>
) )
: null : null
} }

View File

@ -16,6 +16,17 @@ const collection = {
'x-read-pretty': true, 'x-read-pretty': true,
} as ISchema, } as ISchema,
}, },
{
interface: 'number',
type: 'number',
name: 'workflowId',
uiSchema: {
type: 'number',
title: '{{t("Version")}}',
'x-component': 'InputNumber',
'x-read-pretty': true,
} as ISchema,
},
{ {
type: 'number', type: 'number',
name: 'status', name: 'status',
@ -95,6 +106,18 @@ export const executionSchema = {
}, },
} }
}, },
workflowId: {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
workflowId: {
type: 'number',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
}
},
status: { status: {
type: 'void', type: 'void',
'x-decorator': 'Table.Column.Decorator', 'x-decorator': 'Table.Column.Decorator',

View File

@ -1,12 +1,12 @@
import { Application } from '@nocobase/server'; import { MockServer } from '@nocobase/test';
import Database from '@nocobase/database'; import Database from '@nocobase/database';
import { getApp } from '.'; import { getApp } from '.';
import { EXECUTION_STATUS } from '../constants';
describe('workflow > workflow', () => { describe('workflow > workflow', () => {
let app: Application; let app: MockServer;
let agent;
let db: Database; let db: Database;
let PostModel; let PostModel;
let PostRepo; let PostRepo;
@ -14,7 +14,7 @@ describe('workflow > workflow', () => {
beforeEach(async () => { beforeEach(async () => {
app = await getApp(); app = await getApp();
agent = app.agent();
db = app.db; db = app.db;
WorkflowModel = db.getCollection('workflows').model; WorkflowModel = db.getCollection('workflows').model;
PostModel = db.getCollection('posts').model; PostModel = db.getCollection('posts').model;
@ -41,4 +41,55 @@ describe('workflow > workflow', () => {
expect(executions.length).toBe(0); expect(executions.length).toBe(0);
}); });
}); });
describe('revisions', () => {
it('executed count', async () => {
const w1 = await WorkflowModel.create({
enabled: true,
type: 'collection',
config: {
mode: 1,
collection: 'posts'
}
});
expect(w1.current).toBe(true);
const { body, status } = await agent.resource(`workflows`).revision({
filterByTk: w1.id
});
expect(status).toBe(200);
const { data: w2 } = body;
expect(w2.config).toMatchObject(w1.config);
expect(w2.key).toBe(w1.key);
const p1 = await PostRepo.create({ values: { title: 't1' } });
await WorkflowModel.update({
enabled: true
}, {
where: {
id: w2.id
},
individualHooks: true
});
const [w1next, w2next] = await WorkflowModel.findAll({
order: [['id', 'ASC']]
});
expect(w1next.enabled).toBe(false);
expect(w1next.current).toBe(false);
expect(w2next.enabled).toBe(true);
expect(w2next.current).toBe(true);
const p2 = await PostRepo.create({ values: { title: 't2' } });
const [e1] = await w1next.getExecutions();
const [e2] = await w2next.getExecutions();
expect(e1.key).toBe(e2.key);
expect(e1.workflowId).toBe(w1.id);
expect(e2.workflowId).toBe(w2.id);
});
});
}); });

View File

@ -1,53 +1,7 @@
import { Context, utils } from '@nocobase/actions'; import { Context, utils } from '@nocobase/actions';
import { Op } from '@nocobase/database';
export async function update(context: Context, next) {
const { db } = context;
const repository = utils.getRepositoryFromParams(context);
const { filterByTk, values, whitelist, blacklist, filter, updateAssociationValues } = context.action.params;
context.body = await db.sequelize.transaction(async transaction => {
const others: { enabled?: boolean, current?: boolean } = {};
if (values.enabled) {
values.current = true;
others.enabled = false;
}
if (values.current) {
others.current = false;
await repository.update({
filter: {
key: values.key,
id: {
[Op.ne]: filterByTk
}
},
values: others,
context,
transaction
});
}
const instance = await repository.update({
filterByTk,
values,
whitelist,
blacklist,
filter,
updateAssociationValues,
context,
transaction
});
return instance;
});
await next();
}
function typeOf(value) { function typeOf(value) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return 'array'; return 'array';
@ -81,7 +35,7 @@ function migrateConfig(config, oldToNew) {
return migrate(config); return migrate(config);
} }
export async function duplicate(context: Context, next) { export async function revision(context: Context, next) {
const { db } = context; const { db } = context;
const repository = utils.getRepositoryFromParams(context); const repository = utils.getRepositoryFromParams(context);
const { filterByTk } = context.action.params; const { filterByTk } = context.action.params;

View File

@ -3,18 +3,18 @@ import { CollectionOptions } from '@nocobase/database';
export default { export default {
name: 'executions', name: 'executions',
model: 'ExecutionModel', model: 'ExecutionModel',
title: '执行流程',
fields: [ fields: [
{ {
interface: 'linkTo',
type: 'belongsTo', type: 'belongsTo',
name: 'workflow', name: 'workflow'
title: '所属工作流' },
{
type: 'uid',
name: 'key'
}, },
{ {
type: 'boolean', type: 'boolean',
name: 'useTransaction', name: 'useTransaction',
title: '使用事务',
defaultValue: false defaultValue: false
}, },
{ {
@ -23,22 +23,16 @@ export default {
defaultValue: null defaultValue: null
}, },
{ {
interface: 'linkTo',
type: 'hasMany', type: 'hasMany',
name: 'jobs', name: 'jobs',
title: '流程记录'
}, },
{ {
interface: 'json',
type: 'jsonb', type: 'jsonb',
name: 'context', name: 'context',
title: '上下文数据'
}, },
{ {
interface: 'select',
type: 'integer', type: 'integer',
name: 'status', name: 'status',
title: '状态'
} }
] ]
} as CollectionOptions; } as CollectionOptions;

View File

@ -3,7 +3,6 @@ import { CollectionOptions } from '@nocobase/database';
export default { export default {
name: 'workflows', name: 'workflows',
model: 'WorkflowModel', model: 'WorkflowModel',
title: '自动化',
fields: [ fields: [
{ {
name: 'key', name: 'key',
@ -12,56 +11,52 @@ export default {
{ {
type: 'string', type: 'string',
name: 'title', name: 'title',
title: '工作流名称',
required: true required: true
}, },
{ {
type: 'boolean', type: 'boolean',
name: 'enabled', name: 'enabled',
title: '启用',
defaultValue: false defaultValue: false
}, },
{ {
type: 'text', type: 'text',
name: 'description', name: 'description'
title: '描述'
}, },
{ {
type: 'string', type: 'string',
title: '触发方式',
name: 'type', name: 'type',
required: true required: true
}, },
{ {
type: 'jsonb', type: 'jsonb',
title: '触发配置',
name: 'config', name: 'config',
required: true, required: true,
defaultValue: {} defaultValue: {}
}, },
{ {
type: 'boolean', type: 'boolean',
title: '使用事务',
name: 'useTransaction', name: 'useTransaction',
defaultValue: true defaultValue: true
}, },
{ {
type: 'hasMany', type: 'hasMany',
name: 'nodes', name: 'nodes',
target: 'flow_nodes', target: 'flow_nodes'
title: '流程节点'
}, },
{ {
type: 'hasMany', type: 'hasMany',
name: 'executions', name: 'executions'
target: 'executions',
title: '触发执行'
}, },
{ {
type: 'integer', type: 'integer',
name: 'executed', name: 'executed',
defaultValue: 0 defaultValue: 0
}, },
{
type: 'integer',
name: 'allExecuted',
defaultValue: 0
},
{ {
type: 'boolean', type: 'boolean',
name: 'current', name: 'current',

View File

@ -26,6 +26,8 @@ export default class ExecutionModel extends Model {
declare createdAt: Date; declare createdAt: Date;
declare updatedAt: Date; declare updatedAt: Date;
declare key: string;
declare workflow?: WorkflowModel; declare workflow?: WorkflowModel;
declare getWorkflow: BelongsToGetAssociationMixin<WorkflowModel>; declare getWorkflow: BelongsToGetAssociationMixin<WorkflowModel>;

View File

@ -9,8 +9,10 @@ export default class WorkflowModel extends Model {
declare static database: Database; declare static database: Database;
declare id: number; declare id: number;
declare key: string;
declare title: string; declare title: string;
declare enabled: boolean; declare enabled: boolean;
declare current: boolean;
declare description?: string; declare description?: string;
declare type: string; declare type: string;
declare config: any; declare config: any;
@ -63,6 +65,7 @@ export default class WorkflowModel extends Model {
const execution = await this.createExecution({ const execution = await this.createExecution({
context, context,
key: this.key,
status: EXECUTION_STATUS.STARTED, status: EXECUTION_STATUS.STARTED,
useTransaction: this.useTransaction, useTransaction: this.useTransaction,
transaction: transaction.id transaction: transaction.id
@ -73,6 +76,21 @@ export default class WorkflowModel extends Model {
// NOTE: not to trigger afterUpdate hook here // NOTE: not to trigger afterUpdate hook here
await this.update({ executed }, { transaction, hooks: false }); await this.update({ executed }, { transaction, hooks: false });
const allExecuted = await (<typeof ExecutionModel>execution.constructor).count({
where: {
key: this.key
},
transaction
});
await (<typeof WorkflowModel>this.constructor).update({
allExecuted
}, {
where: {
key: this.key
},
transaction
});
execution.workflow = this; execution.workflow = this;
await execution.start({ transaction }); await execution.start({ transaction });

View File

@ -1,6 +1,7 @@
import path from 'path'; import path from 'path';
import { Plugin } from '@nocobase/server'; import { Plugin } from '@nocobase/server';
import { Op } from '@nocobase/database';
import WorkflowModel from './models/Workflow'; import WorkflowModel from './models/Workflow';
import ExecutionModel from './models/Execution'; import ExecutionModel from './models/Execution';
@ -33,6 +34,10 @@ export default class extends Plugin {
initTriggers(this); initTriggers(this);
db.on('workflows.beforeSave', this.setCurrent);
db.on('workflows.afterSave', (model: WorkflowModel) => this.toggle(model));
db.on('workflows.afterDestroy', (model: WorkflowModel) => this.toggle(model, false));
// [Life Cycle]: // [Life Cycle]:
// * load all workflows in db // * load all workflows in db
// * add all hooks for enabled workflows // * add all hooks for enabled workflows
@ -46,15 +51,34 @@ export default class extends Plugin {
workflows.forEach((workflow: WorkflowModel) => { workflows.forEach((workflow: WorkflowModel) => {
this.toggle(workflow); this.toggle(workflow);
}); });
db.on('workflows.afterSave', (model: WorkflowModel) => this.toggle(model));
db.on('workflows.afterDestroy', (model: WorkflowModel) => this.toggle(model, false));
}); });
// [Life Cycle]: initialize all necessary seed data // [Life Cycle]: initialize all necessary seed data
// this.app.on('db.init', async () => {}); // this.app.on('db.init', async () => {});
} }
setCurrent = async (workflow: WorkflowModel, options) => {
const others: { enabled?: boolean, current?: boolean } = {};
if (workflow.enabled) {
workflow.set('current', true);
others.enabled = false;
}
if (workflow.current) {
others.current = false;
await (<typeof WorkflowModel>workflow.constructor).update(others, {
where: {
key: workflow.key,
id: {
[Op.ne]: workflow.id
}
},
individualHooks: true,
transaction: options.transaction
});
}
}
toggle(workflow: WorkflowModel, enable?: boolean) { toggle(workflow: WorkflowModel, enable?: boolean) {
const type = workflow.get('type'); const type = workflow.get('type');
const trigger = this.triggers.get(type); const trigger = this.triggers.get(type);

View File

@ -320,7 +320,7 @@ function nextInCycle(this: ScheduleTrigger, workflow, now: Date): boolean {
export default class ScheduleTrigger implements Trigger { export default class ScheduleTrigger implements Trigger {
static CacheRules = [ static CacheRules = [
// ({ enabled }) => enabled, // ({ enabled }) => enabled,
({ config, executed }) => config.limit ? executed < config.limit : true, ({ config, allExecuted }) => config.limit ? allExecuted < config.limit : true,
({ config }) => ['repeat', 'startsOn'].some(key => config[key]), ({ config }) => ['repeat', 'startsOn'].some(key => config[key]),
nextInCycle, nextInCycle,
function(workflow, now) { function(workflow, now) {
@ -331,7 +331,7 @@ export default class ScheduleTrigger implements Trigger {
]; ];
static TriggerRules = [ static TriggerRules = [
({ config, executed }) => config.limit ? executed < config.limit : true, ({ config, allExecuted }) => config.limit ? allExecuted < config.limit : true,
({ config }) => ['repeat', 'startsOn'].some(key => config[key]), ({ config }) => ['repeat', 'startsOn'].some(key => config[key]),
function (workflow, now) { function (workflow, now) {
const { repeat } = workflow.config; const { repeat } = workflow.config;