Feat(plugin workflow): add more instructions (#201)
* test(plugin-workflow): add getter cases * feat(plugin-workflow): add query type for node instruction * feat(plugin-workflow): add crud instructions for node type * fix(plugin-workflow): fix transaction * fix(plugin-workflow): fix context data of model trigger
This commit is contained in:
parent
a0ebd36e5a
commit
46afc05898
@ -63,6 +63,7 @@
|
|||||||
"jest-localstorage-mock": "^2.3.0",
|
"jest-localstorage-mock": "^2.3.0",
|
||||||
"jest-styled-components": "6.3.3",
|
"jest-styled-components": "6.3.3",
|
||||||
"jest-watch-lerna-packages": "^1.1.0",
|
"jest-watch-lerna-packages": "^1.1.0",
|
||||||
|
"json-templates": "^4.2.0",
|
||||||
"koa": "^2.13.4",
|
"koa": "^2.13.4",
|
||||||
"koa-bodyparser": "^4.3.0",
|
"koa-bodyparser": "^4.3.0",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^4.0.0",
|
||||||
|
@ -24,7 +24,7 @@ export function getConfigByEnv() {
|
|||||||
port: process.env.DB_PORT,
|
port: process.env.DB_PORT,
|
||||||
dialect: process.env.DB_DIALECT,
|
dialect: process.env.DB_DIALECT,
|
||||||
logging: process.env.DB_LOG_SQL === 'on' ? console.log : false,
|
logging: process.env.DB_LOG_SQL === 'on' ? console.log : false,
|
||||||
storage: process.env.DB_STORAGE ? resolve(process.cwd(), process.env.DB_STORAGE) : ':memory:',
|
storage: process.env.DB_STORAGE && process.env.DB_STORAGE !== ':memory:' ? resolve(process.cwd(), process.env.DB_STORAGE) : ':memory:',
|
||||||
define: {
|
define: {
|
||||||
charset: 'utf8mb4',
|
charset: 'utf8mb4',
|
||||||
collate: 'utf8mb4_unicode_ci',
|
collate: 'utf8mb4_unicode_ci',
|
||||||
|
@ -13,8 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/acl": "0.6.0-alpha.0",
|
"@nocobase/acl": "0.6.0-alpha.0",
|
||||||
"@nocobase/database": "0.6.0-alpha.0",
|
"@nocobase/database": "0.6.0-alpha.0",
|
||||||
"@nocobase/server": "0.6.0-alpha.0",
|
"@nocobase/server": "0.6.0-alpha.0"
|
||||||
"json-templates": "^4.2.0"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { CollectionOptions } from '@nocobase/database';
|
import { CollectionOptions } from '@nocobase/database';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'targets',
|
name: 'approvals',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'belongsTo',
|
||||||
name: 'col1',
|
name: 'post',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'integer',
|
||||||
name: 'col2',
|
name: 'status',
|
||||||
|
defaultValue: 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
} as CollectionOptions;
|
} as CollectionOptions;
|
@ -10,6 +10,7 @@ export default {
|
|||||||
{
|
{
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
name: 'published',
|
name: 'published',
|
||||||
|
defaultValue: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} as CollectionOptions;
|
} as CollectionOptions;
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import { Application } from '@nocobase/server';
|
||||||
|
import Database from '@nocobase/database';
|
||||||
|
import { getApp } from '..';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe('workflow > instructions > create', () => {
|
||||||
|
let app: Application;
|
||||||
|
let db: Database;
|
||||||
|
let PostModel;
|
||||||
|
let WorkflowModel;
|
||||||
|
let workflow;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await getApp();
|
||||||
|
|
||||||
|
db = app.db;
|
||||||
|
WorkflowModel = db.getCollection('workflows').model;
|
||||||
|
PostModel = db.getCollection('posts').model;
|
||||||
|
|
||||||
|
workflow = await WorkflowModel.create({
|
||||||
|
title: 'test workflow',
|
||||||
|
enabled: true,
|
||||||
|
type: 'model',
|
||||||
|
config: {
|
||||||
|
mode: 1,
|
||||||
|
collection: 'posts'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => db.close());
|
||||||
|
|
||||||
|
describe('create one', () => {
|
||||||
|
it('params: from context', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'create',
|
||||||
|
config: {
|
||||||
|
collection: 'approvals',
|
||||||
|
params: {
|
||||||
|
values: {
|
||||||
|
postId: '{{$context.data.id}}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result.postId).toBe(post.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,58 @@
|
|||||||
|
import { Application } from '@nocobase/server';
|
||||||
|
import Database from '@nocobase/database';
|
||||||
|
import { getApp } from '..';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe('workflow > instructions > create', () => {
|
||||||
|
let app: Application;
|
||||||
|
let db: Database;
|
||||||
|
let PostModel;
|
||||||
|
let WorkflowModel;
|
||||||
|
let workflow;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await getApp();
|
||||||
|
|
||||||
|
db = app.db;
|
||||||
|
WorkflowModel = db.getCollection('workflows').model;
|
||||||
|
PostModel = db.getCollection('posts').model;
|
||||||
|
|
||||||
|
workflow = await WorkflowModel.create({
|
||||||
|
title: 'test workflow',
|
||||||
|
enabled: true,
|
||||||
|
type: 'model',
|
||||||
|
config: {
|
||||||
|
mode: 1,
|
||||||
|
collection: 'posts'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => db.close());
|
||||||
|
|
||||||
|
describe('destroy one', () => {
|
||||||
|
it('params: from context', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'destroy',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
id: '{{$context.data.id}}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result).toBe(1);
|
||||||
|
|
||||||
|
const count = await PostModel.count();
|
||||||
|
expect(count).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,218 @@
|
|||||||
|
import { Application } from '@nocobase/server';
|
||||||
|
import Database from '@nocobase/database';
|
||||||
|
import { getApp } from '..';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe('workflow > instructions > query', () => {
|
||||||
|
let app: Application;
|
||||||
|
let db: Database;
|
||||||
|
let PostModel;
|
||||||
|
let WorkflowModel;
|
||||||
|
let workflow;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await getApp();
|
||||||
|
|
||||||
|
db = app.db;
|
||||||
|
WorkflowModel = db.getCollection('workflows').model;
|
||||||
|
PostModel = db.getCollection('posts').model;
|
||||||
|
|
||||||
|
workflow = await WorkflowModel.create({
|
||||||
|
title: 'test workflow',
|
||||||
|
enabled: true,
|
||||||
|
type: 'model',
|
||||||
|
config: {
|
||||||
|
mode: 1,
|
||||||
|
collection: 'posts'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => db.close());
|
||||||
|
|
||||||
|
describe('query one', () => {
|
||||||
|
it('params: empty', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'query',
|
||||||
|
config: {
|
||||||
|
collection: 'posts'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result.title).toBe(post.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('params.filter: match', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'query',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
title: 't1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result.title).toBe(post.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('params.filter: unmatch', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'query',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
title: 't2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('params.filter: value from context', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'query',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
title: '{{$context.data.title}}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result.title).toBe(post.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('params.filter: value from job of node', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'echo'
|
||||||
|
});
|
||||||
|
const n2 = await workflow.createNode({
|
||||||
|
type: 'query',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
title: `{{$jobsMapByNodeId.${n1.id}.data.title}}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
upstreamId: n1.id
|
||||||
|
});
|
||||||
|
await n1.setDownstream(n2);
|
||||||
|
|
||||||
|
const post = await PostModel.create({ 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.sort', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'query',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
params: {
|
||||||
|
sort: 'id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const p1 = await PostModel.create({ title: 't1' });
|
||||||
|
const p2 = await PostModel.create({ title: 't2' });
|
||||||
|
|
||||||
|
// get the 2nd execution
|
||||||
|
const [execution] = await workflow.getExecutions({ order: [['id', 'DESC']] });
|
||||||
|
expect(execution.context.data.title).toBe(p2.title);
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result.title).toBe(p1.title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('query all', () => {
|
||||||
|
it('params: empty', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'query',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
multiple: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result.length).toBe(1);
|
||||||
|
expect(job.result[0].title).toBe(post.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('params.filter: match', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'query',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
multiple: true,
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
title: 't1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result.length).toBe(1);
|
||||||
|
expect(job.result[0].title).toBe(post.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('params.filter: unmatch', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'query',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
multiple: true,
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
title: 't2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,62 @@
|
|||||||
|
import { Application } from '@nocobase/server';
|
||||||
|
import Database from '@nocobase/database';
|
||||||
|
import { getApp } from '..';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe('workflow > instructions > update', () => {
|
||||||
|
let app: Application;
|
||||||
|
let db: Database;
|
||||||
|
let PostModel;
|
||||||
|
let WorkflowModel;
|
||||||
|
let workflow;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await getApp();
|
||||||
|
|
||||||
|
db = app.db;
|
||||||
|
WorkflowModel = db.getCollection('workflows').model;
|
||||||
|
PostModel = db.getCollection('posts').model;
|
||||||
|
|
||||||
|
workflow = await WorkflowModel.create({
|
||||||
|
title: 'test workflow',
|
||||||
|
enabled: true,
|
||||||
|
type: 'model',
|
||||||
|
config: {
|
||||||
|
mode: 1,
|
||||||
|
collection: 'posts'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => db.close());
|
||||||
|
|
||||||
|
describe('update one', () => {
|
||||||
|
it('params: from context', async () => {
|
||||||
|
const n2 = await workflow.createNode({
|
||||||
|
type: 'update',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
id: '{{$context.data.id}}'
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
published: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ 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);
|
||||||
|
expect(updatedPost.published).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
142
packages/plugin-workflow/src/__tests__/utils/getter.test.ts
Normal file
142
packages/plugin-workflow/src/__tests__/utils/getter.test.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { Application } from '@nocobase/server';
|
||||||
|
import Database from '@nocobase/database';
|
||||||
|
import { getApp } from '..';
|
||||||
|
|
||||||
|
import { get } from 'lodash';
|
||||||
|
|
||||||
|
import { getValue } from '../../utils/getter';
|
||||||
|
import { BRANCH_INDEX } from '../../constants';
|
||||||
|
|
||||||
|
describe('value getter', () => {
|
||||||
|
let app: Application;
|
||||||
|
let db: Database;
|
||||||
|
let JobModel;
|
||||||
|
let WorkflowModel;
|
||||||
|
let ExecutionModel;
|
||||||
|
let PostModel;
|
||||||
|
let workflow;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await getApp();
|
||||||
|
|
||||||
|
db = app.db;
|
||||||
|
WorkflowModel = db.getCollection('workflows').model;
|
||||||
|
JobModel = db.getCollection('jobs').model;
|
||||||
|
ExecutionModel = db.getCollection('executions').model;
|
||||||
|
PostModel = db.getCollection('posts').model;
|
||||||
|
|
||||||
|
workflow = await WorkflowModel.create({
|
||||||
|
title: 'test workflow',
|
||||||
|
enabled: true,
|
||||||
|
type: 'model',
|
||||||
|
config: {
|
||||||
|
mode: 1,
|
||||||
|
collection: 'posts'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => db.close());
|
||||||
|
|
||||||
|
describe('get from constants', () => {
|
||||||
|
it('null', () => {
|
||||||
|
const v1 = getValue({
|
||||||
|
value: null
|
||||||
|
}, null, null);
|
||||||
|
expect(v1).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('number', () => {
|
||||||
|
const v1 = getValue({
|
||||||
|
value: 1
|
||||||
|
}, null, null);
|
||||||
|
expect(v1).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get from context', () => {
|
||||||
|
it('paths', async () => {
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
|
||||||
|
const v1 = getValue({
|
||||||
|
type: 'context',
|
||||||
|
options: {}
|
||||||
|
}, null, execution);
|
||||||
|
expect(v1).toMatchObject({ data: { title: 't1' } });
|
||||||
|
|
||||||
|
const v2 = getValue({
|
||||||
|
type: 'context',
|
||||||
|
options: { path: 'data' }
|
||||||
|
}, null, execution);
|
||||||
|
expect(v2).toMatchObject({ title: 't1' });
|
||||||
|
|
||||||
|
const v3 = getValue({
|
||||||
|
type: 'context',
|
||||||
|
options: { path: 'data.title' }
|
||||||
|
}, null, execution);
|
||||||
|
expect(v3).toBe(post.title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get from job by id', () => {
|
||||||
|
it('base getting from executed job', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'echo'
|
||||||
|
});
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
await execution.prepare({}, true);
|
||||||
|
|
||||||
|
const v1 = getValue({
|
||||||
|
type: 'job',
|
||||||
|
options: {
|
||||||
|
nodeId: n1.id,
|
||||||
|
path: 'data.title'
|
||||||
|
}
|
||||||
|
}, null, execution);
|
||||||
|
|
||||||
|
expect(v1).toBe(post.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('result of unexecuted job could not be got', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'condition'
|
||||||
|
});
|
||||||
|
|
||||||
|
const n2 = await workflow.createNode({
|
||||||
|
type: 'echo',
|
||||||
|
branchIndex: BRANCH_INDEX.ON_TRUE,
|
||||||
|
upstreamId: n1.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const n3 = await workflow.createNode({
|
||||||
|
type: 'echo',
|
||||||
|
branchIndex: BRANCH_INDEX.ON_FALSE,
|
||||||
|
upstreamId: n1.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostModel.create({ title: 't1' });
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
await execution.prepare({}, true);
|
||||||
|
|
||||||
|
const v1 = getValue({
|
||||||
|
type: 'job',
|
||||||
|
options: {
|
||||||
|
nodeId: n3.id
|
||||||
|
}
|
||||||
|
}, null, execution);
|
||||||
|
|
||||||
|
expect(v1).toBeUndefined();
|
||||||
|
|
||||||
|
const v2 = getValue({
|
||||||
|
type: 'job',
|
||||||
|
options: {
|
||||||
|
nodeId: n2.id
|
||||||
|
}
|
||||||
|
}, null, execution);
|
||||||
|
|
||||||
|
expect(v2).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -36,7 +36,7 @@ export default {
|
|||||||
sourceKey: 'id',
|
sourceKey: 'id',
|
||||||
foreignKey: 'upstream_id',
|
foreignKey: 'upstream_id',
|
||||||
},
|
},
|
||||||
// only works when upstream node is branching type, like condition and parallel.
|
// only works when upstream node is branching type, such as condition and parallel.
|
||||||
// put here because the design of flow-links model is not really necessary for now.
|
// put here because the design of flow-links model is not really necessary for now.
|
||||||
// or it should be put into flow-links model.
|
// or it should be put into flow-links model.
|
||||||
{
|
{
|
||||||
@ -47,7 +47,7 @@ export default {
|
|||||||
},
|
},
|
||||||
// for reasons:
|
// for reasons:
|
||||||
// 1. redirect type node to solve cycle flow.
|
// 1. redirect type node to solve cycle flow.
|
||||||
// 2. recognize as true next node after branches.
|
// 2. recognize as real next node after branches.
|
||||||
{
|
{
|
||||||
interface: 'linkTo',
|
interface: 'linkTo',
|
||||||
name: 'downstream',
|
name: 'downstream',
|
||||||
|
22
packages/plugin-workflow/src/instructions/create.ts
Normal file
22
packages/plugin-workflow/src/instructions/create.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { JOB_STATUS } from "../constants";
|
||||||
|
import FlowNodeModel from "../models/FlowNode";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async run(this: FlowNodeModel, input, execution) {
|
||||||
|
const {
|
||||||
|
collection,
|
||||||
|
params = {}
|
||||||
|
} = this.config;
|
||||||
|
|
||||||
|
const repo = (<typeof FlowNodeModel>this.constructor).database.getRepository(collection);
|
||||||
|
const result = await repo.create({
|
||||||
|
...execution.getParsedValue(params),
|
||||||
|
transaction: execution.transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
status: JOB_STATUS.RESOLVED
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
22
packages/plugin-workflow/src/instructions/destroy.ts
Normal file
22
packages/plugin-workflow/src/instructions/destroy.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { JOB_STATUS } from "../constants";
|
||||||
|
import FlowNodeModel from "../models/FlowNode";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async run(this: FlowNodeModel, input, execution) {
|
||||||
|
const {
|
||||||
|
collection,
|
||||||
|
params = {}
|
||||||
|
} = this.config;
|
||||||
|
|
||||||
|
const repo = (<typeof FlowNodeModel>this.constructor).database.getRepository(collection);
|
||||||
|
const result = await repo.destroy({
|
||||||
|
...execution.getParsedValue(params),
|
||||||
|
transaction: execution.transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
status: JOB_STATUS.RESOLVED
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,10 @@ import FlowNodeModel from "../models/FlowNode";
|
|||||||
import prompt from './prompt';
|
import prompt from './prompt';
|
||||||
import condition from './condition';
|
import condition from './condition';
|
||||||
import parallel from './parallel';
|
import parallel from './parallel';
|
||||||
|
import query from "./query";
|
||||||
|
import create from "./create";
|
||||||
|
import update from "./update";
|
||||||
|
import destroy from "./destroy";
|
||||||
|
|
||||||
export interface Job {
|
export interface Job {
|
||||||
status: number;
|
status: number;
|
||||||
@ -42,3 +46,7 @@ export function registerInstruction(key: string, instruction: any) {
|
|||||||
registerInstruction('prompt', prompt);
|
registerInstruction('prompt', prompt);
|
||||||
registerInstruction('condition', condition);
|
registerInstruction('condition', condition);
|
||||||
registerInstruction('parallel', parallel);
|
registerInstruction('parallel', parallel);
|
||||||
|
registerInstruction('query', query);
|
||||||
|
registerInstruction('create', create);
|
||||||
|
registerInstruction('update', update);
|
||||||
|
registerInstruction('destroy', destroy);
|
||||||
|
24
packages/plugin-workflow/src/instructions/query.ts
Normal file
24
packages/plugin-workflow/src/instructions/query.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { JOB_STATUS } from "../constants";
|
||||||
|
import FlowNodeModel from "../models/FlowNode";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async run(this: FlowNodeModel, input, execution) {
|
||||||
|
const {
|
||||||
|
collection,
|
||||||
|
multiple,
|
||||||
|
params = {}
|
||||||
|
} = this.config;
|
||||||
|
|
||||||
|
const repo = (<typeof FlowNodeModel>this.constructor).database.getRepository(collection);
|
||||||
|
const options = execution.getParsedValue(params);
|
||||||
|
const result = await (multiple ? repo.find : repo.findOne).call(repo, {
|
||||||
|
...options,
|
||||||
|
transaction: execution.transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
status: JOB_STATUS.RESOLVED
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
23
packages/plugin-workflow/src/instructions/update.ts
Normal file
23
packages/plugin-workflow/src/instructions/update.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { JOB_STATUS } from "../constants";
|
||||||
|
import FlowNodeModel from "../models/FlowNode";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async run(this: FlowNodeModel, input, execution) {
|
||||||
|
const {
|
||||||
|
collection,
|
||||||
|
multiple = false,
|
||||||
|
params = {}
|
||||||
|
} = this.config;
|
||||||
|
|
||||||
|
const repo = (<typeof FlowNodeModel>this.constructor).database.getRepository(collection);
|
||||||
|
const result = await repo.update({
|
||||||
|
...execution.getParsedValue(params),
|
||||||
|
transaction: execution.transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: multiple ? result : (result[0] || null),
|
||||||
|
status: JOB_STATUS.RESOLVED
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import {
|
|||||||
HasManyGetAssociationsMixin,
|
HasManyGetAssociationsMixin,
|
||||||
Transaction
|
Transaction
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
|
import parse from 'json-templates';
|
||||||
|
|
||||||
import Database from '@nocobase/database';
|
import Database from '@nocobase/database';
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ export default class ExecutionModel extends Model {
|
|||||||
nodes: Array<FlowNodeModel> = [];
|
nodes: Array<FlowNodeModel> = [];
|
||||||
nodesMap = new Map<number, FlowNodeModel>();
|
nodesMap = new Map<number, FlowNodeModel>();
|
||||||
jobsMap = new Map<number, JobModel>();
|
jobsMap = new Map<number, JobModel>();
|
||||||
|
jobsMapByNodeId: { [key: number]: any } = {};
|
||||||
|
|
||||||
static StatusMap = {
|
static StatusMap = {
|
||||||
[JOB_STATUS.PENDING]: EXECUTION_STATUS.STARTED,
|
[JOB_STATUS.PENDING]: EXECUTION_STATUS.STARTED,
|
||||||
@ -70,14 +72,12 @@ export default class ExecutionModel extends Model {
|
|||||||
makeJobs(jobs: Array<JobModel>) {
|
makeJobs(jobs: Array<JobModel>) {
|
||||||
jobs.forEach(job => {
|
jobs.forEach(job => {
|
||||||
this.jobsMap.set(job.id, job);
|
this.jobsMap.set(job.id, job);
|
||||||
|
// TODO: should consider cycle, and from previous job
|
||||||
|
this.jobsMapByNodeId[job.nodeId] = job.result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepare(options) {
|
async prepare(options, commit = false) {
|
||||||
if (this.status !== EXECUTION_STATUS.STARTED) {
|
|
||||||
throw new Error(`execution was ended with status ${this.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options = options || {};
|
this.options = options || {};
|
||||||
const { transaction = await (<typeof ExecutionModel>this.constructor).database.sequelize.transaction() } = this.options;
|
const { transaction = await (<typeof ExecutionModel>this.constructor).database.sequelize.transaction() } = this.options;
|
||||||
this.transaction = transaction;
|
this.transaction = transaction;
|
||||||
@ -90,12 +90,22 @@ export default class ExecutionModel extends Model {
|
|||||||
|
|
||||||
this.makeNodes(nodes);
|
this.makeNodes(nodes);
|
||||||
|
|
||||||
const jobs = await this.getJobs({ transaction });
|
const jobs = await this.getJobs({
|
||||||
|
order: [['id', 'ASC']],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
this.makeJobs(jobs);
|
this.makeJobs(jobs);
|
||||||
|
|
||||||
|
if (commit) {
|
||||||
|
await this.commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(options: ExecutionOptions) {
|
async start(options: ExecutionOptions) {
|
||||||
|
if (this.status !== EXECUTION_STATUS.STARTED) {
|
||||||
|
throw new Error(`execution was ended with status ${this.status}`);
|
||||||
|
}
|
||||||
await this.prepare(options);
|
await this.prepare(options);
|
||||||
if (this.nodes.length) {
|
if (this.nodes.length) {
|
||||||
const head = this.nodes.find(item => !item.upstream);
|
const head = this.nodes.find(item => !item.upstream);
|
||||||
@ -107,6 +117,9 @@ export default class ExecutionModel extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async resume(job: JobModel, options: ExecutionOptions) {
|
async resume(job: JobModel, options: ExecutionOptions) {
|
||||||
|
if (this.status !== EXECUTION_STATUS.STARTED) {
|
||||||
|
throw new Error(`execution was ended with status ${this.status}`);
|
||||||
|
}
|
||||||
await this.prepare(options);
|
await this.prepare(options);
|
||||||
const node = this.nodesMap.get(job.nodeId);
|
const node = this.nodesMap.get(job.nodeId);
|
||||||
await this.recall(node, job);
|
await this.recall(node, job);
|
||||||
@ -201,13 +214,14 @@ export default class ExecutionModel extends Model {
|
|||||||
async saveJob(payload) {
|
async saveJob(payload) {
|
||||||
const { database } = <typeof WorkflowModel>this.constructor;
|
const { database } = <typeof WorkflowModel>this.constructor;
|
||||||
const { model } = database.getCollection('jobs');
|
const { model } = database.getCollection('jobs');
|
||||||
const [result] = await model.upsert({
|
const [job] = await model.upsert({
|
||||||
...payload,
|
...payload,
|
||||||
executionId: this.id
|
executionId: this.id
|
||||||
}, { transaction: this.transaction }) as [JobModel, boolean | null];
|
}, { transaction: this.transaction }) as [JobModel, boolean | null];
|
||||||
this.jobsMap.set(result.id, result);
|
this.jobsMap.set(job.id, job);
|
||||||
|
this.jobsMapByNodeId[job.nodeId] = job.result;
|
||||||
|
|
||||||
return result;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the first node in current branch
|
// find the first node in current branch
|
||||||
@ -238,4 +252,11 @@ export default class ExecutionModel extends Model {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParsedValue(value) {
|
||||||
|
return parse(value)({
|
||||||
|
$context: this.context,
|
||||||
|
$jobsMapByNodeId: this.jobsMapByNodeId
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { Model, BelongsToGetAssociationMixin } from 'sequelize';
|
import { Model, BelongsToGetAssociationMixin } from 'sequelize';
|
||||||
|
import Database from '@nocobase/database';
|
||||||
|
|
||||||
import WorkflowModel from './Workflow';
|
import WorkflowModel from './Workflow';
|
||||||
|
|
||||||
export default class FlowNodeModel extends Model {
|
export default class FlowNodeModel extends Model {
|
||||||
|
declare static readonly database: Database;
|
||||||
|
|
||||||
declare id: number;
|
declare id: number;
|
||||||
declare title: string;
|
declare title: string;
|
||||||
declare branchIndex: null | number;
|
declare branchIndex: null | number;
|
||||||
|
@ -2,6 +2,7 @@ import WorkflowModel from "../models/Workflow";
|
|||||||
|
|
||||||
export interface ModelChangeTriggerConfig {
|
export interface ModelChangeTriggerConfig {
|
||||||
collection: string;
|
collection: string;
|
||||||
|
mode: number;
|
||||||
// TODO: ICondition
|
// TODO: ICondition
|
||||||
filter: any;
|
filter: any;
|
||||||
}
|
}
|
||||||
@ -23,7 +24,7 @@ export default {
|
|||||||
const { database } = <typeof WorkflowModel>this.constructor;
|
const { database } = <typeof WorkflowModel>this.constructor;
|
||||||
const { collection, mode } = this.config;
|
const { collection, mode } = this.config;
|
||||||
const { model } = database.getCollection(collection);
|
const { model } = database.getCollection(collection);
|
||||||
const handler = (data: any, options) => callback({ data }, options);
|
const handler = (data: any, options) => callback({ data: data.get() }, options);
|
||||||
// TODO: duplication when mode change should be considered
|
// TODO: duplication when mode change should be considered
|
||||||
for (let [key, event] of MODE_BITMAP_EVENTS.entries()) {
|
for (let [key, event] of MODE_BITMAP_EVENTS.entries()) {
|
||||||
if (mode & key) {
|
if (mode & key) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { get } from 'lodash';
|
import { get as getWithPath } from 'lodash';
|
||||||
|
|
||||||
import ExecutionModel from '../models/Execution';
|
import ExecutionModel from '../models/Execution';
|
||||||
|
import JobModel from '../models/Job';
|
||||||
|
|
||||||
export type OperandType = 'context' | 'input' | 'job';
|
export type OperandType = 'context' | 'input' | 'job';
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ export type ObjectGetterOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type JobGetterOptions = ObjectGetterOptions & {
|
export type JobGetterOptions = ObjectGetterOptions & {
|
||||||
id: number
|
nodeId: number
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConstantOperand = {
|
export type ConstantOperand = {
|
||||||
@ -32,22 +33,34 @@ export type JobOperand = {
|
|||||||
options: JobGetterOptions;
|
options: JobGetterOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO(type): union type here is wrong
|
||||||
export type Operand = ContextOperand | InputOperand | JobOperand | ConstantOperand;
|
export type Operand = ContextOperand | InputOperand | JobOperand | ConstantOperand;
|
||||||
|
|
||||||
// TODO: other instructions may also use this method, could be moved to utils.
|
// HACK: if no path provided, return self
|
||||||
export function getValue(operand: Operand, input: any, execution: ExecutionModel) {
|
// @see https://github.com/lodash/lodash/pull/1270
|
||||||
|
// TODO(question): should add default value as lodash?
|
||||||
|
function get(object, path?: string | Array<string>) {
|
||||||
|
return path == null || !path.length ? object : getWithPath(object, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// NOTE:
|
||||||
|
// this method could only be used in executing nodes.
|
||||||
|
// because type of 'job' need loaded jobs in runtime execution.
|
||||||
|
// or the execution should be prepared first.
|
||||||
|
export function getValue(operand: Operand, lastJob: JobModel, execution: ExecutionModel) {
|
||||||
switch (operand.type) {
|
switch (operand.type) {
|
||||||
// from execution context
|
// from execution context
|
||||||
case 'context':
|
case 'context':
|
||||||
return get(execution, operand.options.path);
|
return get(execution.context, operand.options.path);
|
||||||
// from input from last job or manual
|
// from last job (or input job)
|
||||||
case 'input':
|
case 'input':
|
||||||
return get(input, operand.options.path);
|
return lastJob ?? get(lastJob.result, operand.options.path);
|
||||||
// from job in execution
|
// from job in execution
|
||||||
case 'job':
|
case 'job':
|
||||||
// assume jobs have been fetched from execution before
|
// assume jobs have been fetched from execution before
|
||||||
const job = execution.jobsMap.get(operand.options.id);
|
const job = execution.jobsMapByNodeId[operand.options.nodeId];
|
||||||
return get(job, operand.options.path);
|
return job && get(job, operand.options.path);
|
||||||
// constant
|
// constant
|
||||||
default:
|
default:
|
||||||
return operand.value;
|
return operand.value;
|
||||||
|
Loading…
Reference in New Issue
Block a user