feat(plugin-workflow): add assignees config for prompt instruction (#690)
This commit is contained in:
parent
12f658391a
commit
a73c37c927
@ -8,6 +8,8 @@ import initActions from './actions';
|
|||||||
import initTriggers, { Trigger } from './triggers';
|
import initTriggers, { Trigger } from './triggers';
|
||||||
import initInstructions, { Instruction } from './instructions';
|
import initInstructions, { Instruction } from './instructions';
|
||||||
import Processor from './Processor';
|
import Processor from './Processor';
|
||||||
|
import calculators from './calculators';
|
||||||
|
import extensions from './extensions';
|
||||||
import WorkflowModel from './models/Workflow';
|
import WorkflowModel from './models/Workflow';
|
||||||
import ExecutionModel from './models/Execution';
|
import ExecutionModel from './models/Execution';
|
||||||
import { EXECUTION_STATUS } from './constants';
|
import { EXECUTION_STATUS } from './constants';
|
||||||
@ -17,6 +19,8 @@ import { EXECUTION_STATUS } from './constants';
|
|||||||
export default class WorkflowPlugin extends Plugin {
|
export default class WorkflowPlugin extends Plugin {
|
||||||
instructions: Registry<Instruction> = new Registry();
|
instructions: Registry<Instruction> = new Registry();
|
||||||
triggers: Registry<Trigger> = new Registry();
|
triggers: Registry<Trigger> = new Registry();
|
||||||
|
calculators = calculators;
|
||||||
|
extensions = extensions;
|
||||||
|
|
||||||
onBeforeSave = async (instance: WorkflowModel, options) => {
|
onBeforeSave = async (instance: WorkflowModel, options) => {
|
||||||
const Model = <typeof WorkflowModel>instance.constructor;
|
const Model = <typeof WorkflowModel>instance.constructor;
|
||||||
@ -80,6 +84,8 @@ export default class WorkflowPlugin extends Plugin {
|
|||||||
db.on('workflows.afterSave', (model: WorkflowModel) => this.toggle(model));
|
db.on('workflows.afterSave', (model: WorkflowModel) => this.toggle(model));
|
||||||
db.on('workflows.afterDestroy', (model: WorkflowModel) => this.toggle(model, false));
|
db.on('workflows.afterDestroy', (model: WorkflowModel) => this.toggle(model, false));
|
||||||
|
|
||||||
|
this.app.on('afterLoadAll', async () => this.extensions.reduce((promise, extend) => promise.then(() => extend(this)), Promise.resolve()));
|
||||||
|
|
||||||
// [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
|
||||||
@ -122,10 +128,10 @@ export default class WorkflowPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async trigger(workflow, context: Object, options: Transactionable = {}) {
|
async trigger(workflow: WorkflowModel, context: Object, options: Transactionable = {}): Promise<ExecutionModel | null> {
|
||||||
// `null` means not to trigger
|
// `null` means not to trigger
|
||||||
if (context === null) {
|
if (context === null) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let transaction = null;
|
let transaction = null;
|
||||||
@ -145,7 +151,7 @@ export default class WorkflowPlugin extends Plugin {
|
|||||||
|
|
||||||
if (existed) {
|
if (existed) {
|
||||||
console.warn(`workflow ${workflow.id} has already been triggered in same execution (${transaction.id}), and newly triggering will be skipped.`);
|
console.warn(`workflow ${workflow.id} has already been triggered in same execution (${transaction.id}), and newly triggering will be skipped.`);
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export default class Processor {
|
|||||||
[JOB_STATUS.PENDING]: EXECUTION_STATUS.STARTED,
|
[JOB_STATUS.PENDING]: EXECUTION_STATUS.STARTED,
|
||||||
[JOB_STATUS.RESOLVED]: EXECUTION_STATUS.RESOLVED,
|
[JOB_STATUS.RESOLVED]: EXECUTION_STATUS.RESOLVED,
|
||||||
[JOB_STATUS.REJECTED]: EXECUTION_STATUS.REJECTED,
|
[JOB_STATUS.REJECTED]: EXECUTION_STATUS.REJECTED,
|
||||||
[JOB_STATUS.CANCELLED]: EXECUTION_STATUS.CANCELLED,
|
[JOB_STATUS.CANCELED]: EXECUTION_STATUS.CANCELED,
|
||||||
};
|
};
|
||||||
|
|
||||||
transaction: Transaction;
|
transaction: Transaction;
|
||||||
@ -168,8 +168,6 @@ export default class Processor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let savedJob;
|
let savedJob;
|
||||||
// TODO(optimize): many checking of resuming or new could be improved
|
|
||||||
// could be implemented separately in exec() / resume()
|
|
||||||
if (job instanceof Model) {
|
if (job instanceof Model) {
|
||||||
savedJob = (await job.save({ transaction: this.transaction })) as unknown as JobModel;
|
savedJob = (await job.save({ transaction: this.transaction })) as unknown as JobModel;
|
||||||
} else {
|
} else {
|
||||||
|
@ -0,0 +1,573 @@
|
|||||||
|
import { MockServer } from '@nocobase/test';
|
||||||
|
import Database from '@nocobase/database';
|
||||||
|
import UserPlugin from '@nocobase/plugin-users';
|
||||||
|
import { getApp, sleep } from '..';
|
||||||
|
import { EXECUTION_STATUS, JOB_STATUS } from '../../constants';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe('workflow > instructions > prompt', () => {
|
||||||
|
describe('base', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let agent;
|
||||||
|
let db: Database;
|
||||||
|
let PostRepo;
|
||||||
|
let WorkflowModel;
|
||||||
|
let workflow;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await getApp();
|
||||||
|
agent = app.agent();
|
||||||
|
db = app.db;
|
||||||
|
WorkflowModel = db.getCollection('workflows').model;
|
||||||
|
PostRepo = db.getCollection('posts').repository;
|
||||||
|
|
||||||
|
workflow = await WorkflowModel.create({
|
||||||
|
enabled: true,
|
||||||
|
type: 'collection',
|
||||||
|
config: {
|
||||||
|
mode: 1,
|
||||||
|
collection: 'posts'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => db.close());
|
||||||
|
|
||||||
|
it('resume to resolve', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'prompt',
|
||||||
|
config: {
|
||||||
|
actions: {
|
||||||
|
[JOB_STATUS.RESOLVED]: 'submit'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
const [pending] = await workflow.getExecutions();
|
||||||
|
expect(pending.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await pending.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
|
||||||
|
const { status } = await agent.resource('jobs').submit({
|
||||||
|
filterByTk: j1.id,
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { a: 1 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(status).toBe(202);
|
||||||
|
|
||||||
|
// NOTE: wait for no await execution
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [resolved] = await workflow.getExecutions();
|
||||||
|
expect(resolved.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||||
|
const [j2] = await resolved.getJobs();
|
||||||
|
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
|
expect(j2.result).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('assignees', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let agent;
|
||||||
|
let userAgents;
|
||||||
|
let db: Database;
|
||||||
|
let PostRepo;
|
||||||
|
let WorkflowModel;
|
||||||
|
let workflow;
|
||||||
|
let UserModel;
|
||||||
|
let users;
|
||||||
|
let UserJobModel;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await getApp({
|
||||||
|
plugins: [
|
||||||
|
'@nocobase/plugin-users'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
agent = app.agent();
|
||||||
|
db = app.db;
|
||||||
|
WorkflowModel = db.getCollection('workflows').model;
|
||||||
|
PostRepo = db.getCollection('posts').repository;
|
||||||
|
UserModel = db.getCollection('users').model;
|
||||||
|
UserJobModel = db.getModel('users_jobs');
|
||||||
|
|
||||||
|
users = await UserModel.bulkCreate([
|
||||||
|
{ id: 1, nickname: 'a' },
|
||||||
|
{ id: 2, nickname: 'b' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UserPlugin;
|
||||||
|
userAgents = users.map((user) => app.agent().auth(userPlugin.jwtService.sign({
|
||||||
|
userId: user.id,
|
||||||
|
}), { type: 'bearer' }));
|
||||||
|
|
||||||
|
workflow = await WorkflowModel.create({
|
||||||
|
enabled: true,
|
||||||
|
type: 'collection',
|
||||||
|
config: {
|
||||||
|
mode: 1,
|
||||||
|
collection: 'posts'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => db.close());
|
||||||
|
|
||||||
|
describe('mode: 0 (single record)', () => {
|
||||||
|
it('the only user assigned could submit', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'prompt',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
const [pending] = await workflow.getExecutions();
|
||||||
|
expect(pending.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await pending.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
|
||||||
|
const usersJobs = await UserJobModel.findAll();
|
||||||
|
expect(usersJobs.length).toBe(1);
|
||||||
|
expect(usersJobs[0].status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
|
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||||
|
|
||||||
|
const res1 = await agent.resource('jobs').submit({
|
||||||
|
filterByTk: j1.id
|
||||||
|
});
|
||||||
|
expect(res1.status).toBe(401);
|
||||||
|
|
||||||
|
const res2 = await userAgents[1].resource('jobs').submit({
|
||||||
|
filterByTk: j1.id,
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res2.status).toBe(404);
|
||||||
|
|
||||||
|
const res3 = await userAgents[0].resource('jobs').submit({
|
||||||
|
filterByTk: j1.id,
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { a: 1 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res3.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [j2] = await pending.getJobs();
|
||||||
|
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
|
expect(j2.result).toEqual({ a: 1 });
|
||||||
|
|
||||||
|
const usersJobsAfter = await UserJobModel.findAll();
|
||||||
|
expect(usersJobsAfter.length).toBe(1);
|
||||||
|
expect(usersJobsAfter[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||||
|
expect(usersJobsAfter[0].result).toEqual({ a: 1 });
|
||||||
|
|
||||||
|
const res4 = await userAgents[0].resource('jobs').submit({
|
||||||
|
filterByTk: j1.id,
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { a: 2 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res4.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('any user assigned could submit', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'prompt',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id, users[1].id]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
const [pending] = await workflow.getExecutions();
|
||||||
|
expect(pending.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await pending.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
|
||||||
|
const res1 = await userAgents[1].resource('jobs').submit({
|
||||||
|
filterByTk: j1.id,
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { a: 1 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res1.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [j2] = await pending.getJobs();
|
||||||
|
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
|
expect(j2.result).toEqual({ a: 1 });
|
||||||
|
|
||||||
|
const res2 = await userAgents[0].resource('jobs').submit({
|
||||||
|
filterByTk: j1.id,
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { a: 2 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res2.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('also could submit to users_jobs api', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'prompt',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
const UserJobModel = db.getModel('users_jobs');
|
||||||
|
const usersJobs = await UserJobModel.findAll();
|
||||||
|
expect(usersJobs.length).toBe(1);
|
||||||
|
expect(usersJobs[0].get('status')).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(usersJobs[0].get('userId')).toBe(users[0].id);
|
||||||
|
|
||||||
|
const res = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: usersJobs[0].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { a: 1 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
|
expect(job.result).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mode: 1 (multiple record, all)', () => {
|
||||||
|
it('all resolved', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'prompt',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id, users[1].id],
|
||||||
|
mode: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
const UserJobModel = db.getModel('users_jobs');
|
||||||
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
|
order: [[ 'userId', 'ASC' ]]
|
||||||
|
});
|
||||||
|
expect(pendingJobs.length).toBe(2);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { a: 1 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res1.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [e1] = await workflow.getExecutions();
|
||||||
|
expect(e1.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await e1.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(j1.result).toBe(0.5);
|
||||||
|
const usersJobs1 = await UserJobModel.findAll({
|
||||||
|
order: [[ 'userId', 'ASC' ]]
|
||||||
|
});
|
||||||
|
expect(usersJobs1.length).toBe(2);
|
||||||
|
|
||||||
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { a: 2 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res2.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [e2] = await workflow.getExecutions();
|
||||||
|
expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||||
|
const [j2] = await e2.getJobs();
|
||||||
|
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
|
expect(j2.result).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('first rejected', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'prompt',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id, users[1].id],
|
||||||
|
mode: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
const UserJobModel = db.getModel('users_jobs');
|
||||||
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
|
order: [[ 'userId', 'ASC' ]]
|
||||||
|
});
|
||||||
|
expect(pendingJobs.length).toBe(2);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.REJECTED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res1.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [e1] = await workflow.getExecutions();
|
||||||
|
expect(e1.status).toBe(EXECUTION_STATUS.REJECTED);
|
||||||
|
const [j1] = await e1.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.REJECTED);
|
||||||
|
expect(j1.result).toBe(0.5);
|
||||||
|
const usersJobs1 = await UserJobModel.findAll({
|
||||||
|
order: [[ 'userId', 'ASC' ]]
|
||||||
|
});
|
||||||
|
expect(usersJobs1.length).toBe(2);
|
||||||
|
|
||||||
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.REJECTED,
|
||||||
|
result: { a: 2 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res2.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('last rejected', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'prompt',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id, users[1].id],
|
||||||
|
mode: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
const UserJobModel = db.getModel('users_jobs');
|
||||||
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
|
order: [[ 'userId', 'ASC' ]]
|
||||||
|
});
|
||||||
|
expect(pendingJobs.length).toBe(2);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res1.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [e1] = await workflow.getExecutions();
|
||||||
|
expect(e1.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await e1.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(j1.result).toBe(0.5);
|
||||||
|
const usersJobs1 = await UserJobModel.findAll({
|
||||||
|
order: [[ 'userId', 'ASC' ]]
|
||||||
|
});
|
||||||
|
expect(usersJobs1.length).toBe(2);
|
||||||
|
|
||||||
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.REJECTED,
|
||||||
|
result: { a: 2 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res2.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [e2] = await workflow.getExecutions();
|
||||||
|
expect(e2.status).toBe(EXECUTION_STATUS.REJECTED);
|
||||||
|
const [j2] = await e2.getJobs();
|
||||||
|
expect(j2.status).toBe(JOB_STATUS.REJECTED);
|
||||||
|
expect(j2.result).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mode: -1 (multiple record, any)', () => {
|
||||||
|
it('first resolved', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'prompt',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id, users[1].id],
|
||||||
|
mode: -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
const UserJobModel = db.getModel('users_jobs');
|
||||||
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
|
order: [[ 'userId', 'ASC' ]]
|
||||||
|
});
|
||||||
|
expect(pendingJobs.length).toBe(2);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res1.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [e1] = await workflow.getExecutions();
|
||||||
|
expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||||
|
const [j1] = await e1.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
|
expect(j1.result).toBe(0.5);
|
||||||
|
|
||||||
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.REJECTED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res2.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('any resolved', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'prompt',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id, users[1].id],
|
||||||
|
mode: -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
const UserJobModel = db.getModel('users_jobs');
|
||||||
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
|
order: [[ 'userId', 'ASC' ]]
|
||||||
|
});
|
||||||
|
expect(pendingJobs.length).toBe(2);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.REJECTED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res1.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [e1] = await workflow.getExecutions();
|
||||||
|
expect(e1.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await e1.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(j1.result).toBe(0.5);
|
||||||
|
|
||||||
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.RESOLVED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res2.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [e2] = await workflow.getExecutions();
|
||||||
|
expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||||
|
const [j2] = await e2.getJobs();
|
||||||
|
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
|
expect(j2.result).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all rejected', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'prompt',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id, users[1].id],
|
||||||
|
mode: -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
const UserJobModel = db.getModel('users_jobs');
|
||||||
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
|
order: [[ 'userId', 'ASC' ]]
|
||||||
|
});
|
||||||
|
expect(pendingJobs.length).toBe(2);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.REJECTED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res1.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [e1] = await workflow.getExecutions();
|
||||||
|
expect(e1.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await e1.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(j1.result).toBe(0.5);
|
||||||
|
|
||||||
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
|
values: {
|
||||||
|
status: JOB_STATUS.REJECTED
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res2.status).toBe(202);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [e2] = await workflow.getExecutions();
|
||||||
|
expect(e2.status).toBe(EXECUTION_STATUS.REJECTED);
|
||||||
|
const [j2] = await e2.getJobs();
|
||||||
|
expect(j2.status).toBe(JOB_STATUS.REJECTED);
|
||||||
|
expect(j2.result).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mode: (0,1) (multiple record, all to percent)', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mode: (-1,0) (multiple record, any to percent)', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,6 @@
|
|||||||
import * as workflows from './workflows';
|
import * as workflows from './workflows';
|
||||||
import * as nodes from './nodes';
|
import * as nodes from './nodes';
|
||||||
|
import * as jobs from './jobs';
|
||||||
|
|
||||||
function make(name, mod) {
|
function make(name, mod) {
|
||||||
return Object.keys(mod).reduce((result, key) => ({
|
return Object.keys(mod).reduce((result, key) => ({
|
||||||
@ -17,6 +18,7 @@ export default function({ app }) {
|
|||||||
}),
|
}),
|
||||||
...make('flow_nodes', {
|
...make('flow_nodes', {
|
||||||
update: nodes.update
|
update: nodes.update
|
||||||
})
|
}),
|
||||||
|
...make('jobs', jobs)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
28
packages/plugins/workflow/src/server/actions/jobs.ts
Normal file
28
packages/plugins/workflow/src/server/actions/jobs.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Context } from '@nocobase/actions';
|
||||||
|
import { JOB_STATUS } from '../constants';
|
||||||
|
|
||||||
|
export async function submit(context: Context, next) {
|
||||||
|
const { values } = context.action.params;
|
||||||
|
|
||||||
|
const { body: instance } = context;
|
||||||
|
|
||||||
|
// NOTE: validate status
|
||||||
|
if (instance.status !== JOB_STATUS.PENDING) {
|
||||||
|
return context.throw(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: validate assignee
|
||||||
|
instance.set({
|
||||||
|
status: values.status,
|
||||||
|
result: values.result
|
||||||
|
});
|
||||||
|
|
||||||
|
context.status = 202;
|
||||||
|
|
||||||
|
await next();
|
||||||
|
|
||||||
|
const plugin = context.app.pm.get('@nocobase/plugin-workflow');
|
||||||
|
const processor = plugin.createProcessor(instance.execution);
|
||||||
|
// NOTE: resume the process and no `await` for quick returning
|
||||||
|
processor.resume(instance);
|
||||||
|
}
|
@ -33,7 +33,7 @@ function migrateConfig(config, oldToNew) {
|
|||||||
case 'string':
|
case 'string':
|
||||||
return value
|
return value
|
||||||
.replace(
|
.replace(
|
||||||
/(\{\{\$jobsMapByNodeId\.)(\d+)/,
|
/(\{\{\$jobsMapByNodeId\.)([\w-]+)/,
|
||||||
(_, prefix, id) => `${prefix}${oldToNew.get(Number.parseInt(id, 10)).id}`
|
(_, prefix, id) => `${prefix}${oldToNew.get(Number.parseInt(id, 10)).id}`
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { get as getWithPath } from 'lodash';
|
import { toNumber, get as getWithPath } from 'lodash';
|
||||||
import { Registry } from "@nocobase/utils";
|
import { Registry } from "@nocobase/utils";
|
||||||
|
|
||||||
import JobModel from '../models/Job';
|
import JobModel from '../models/Job';
|
||||||
@ -141,23 +141,23 @@ calculators.register('<=', lte);
|
|||||||
|
|
||||||
|
|
||||||
function add(...args) {
|
function add(...args) {
|
||||||
return args.reduce((sum, a) => sum + a, 0);
|
return args.reduce((sum, a) => sum + toNumber(a), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function minus(a, b) {
|
function minus(a, b) {
|
||||||
return a - b;
|
return toNumber(a) - toNumber(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function multiple(...args) {
|
function multiple(...args) {
|
||||||
return args.reduce((result, a) => result * a, 1);
|
return args.reduce((result, a) => result * toNumber(a), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function divide(a, b) {
|
function divide(a, b) {
|
||||||
return a / b;
|
return toNumber(a) / toNumber(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mod(a, b) {
|
function mod(a, b) {
|
||||||
return a % b;
|
return toNumber(a) % toNumber(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
calculators.register('add', add);
|
calculators.register('add', add);
|
||||||
|
@ -2,14 +2,14 @@ export const EXECUTION_STATUS = {
|
|||||||
STARTED: 0,
|
STARTED: 0,
|
||||||
RESOLVED: 1,
|
RESOLVED: 1,
|
||||||
REJECTED: -1,
|
REJECTED: -1,
|
||||||
CANCELLED: -2
|
CANCELED: -2
|
||||||
};
|
};
|
||||||
|
|
||||||
export const JOB_STATUS = {
|
export const JOB_STATUS = {
|
||||||
PENDING: 0,
|
PENDING: 0,
|
||||||
RESOLVED: 1,
|
RESOLVED: 1,
|
||||||
REJECTED: -1,
|
REJECTED: -1,
|
||||||
CANCELLED: -2
|
CANCELED: -2
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BRANCH_INDEX = {
|
export const BRANCH_INDEX = {
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
import { Context, utils } from '@nocobase/actions';
|
||||||
|
import { EXECUTION_STATUS, JOB_STATUS } from '../../constants';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export async function submit(context: Context, next) {
|
||||||
|
const repository = utils.getRepositoryFromParams(context);
|
||||||
|
const { filterByTk, values } = context.action.params;
|
||||||
|
const { currentUser } = context.state;
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
return context.throw(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = await repository.findOne({
|
||||||
|
filterByTk,
|
||||||
|
// filter: {
|
||||||
|
// userId: currentUser?.id
|
||||||
|
// },
|
||||||
|
appends: ['job', 'node', 'execution'],
|
||||||
|
context
|
||||||
|
});
|
||||||
|
|
||||||
|
const { actions, assignees } = instance.node.config;
|
||||||
|
|
||||||
|
// NOTE: validate status
|
||||||
|
if (instance.status !== JOB_STATUS.PENDING
|
||||||
|
|| instance.job.status !== JOB_STATUS.PENDING
|
||||||
|
|| instance.execution.status !== EXECUTION_STATUS.STARTED
|
||||||
|
|| (actions && !actions[values.status])
|
||||||
|
) {
|
||||||
|
context.throw(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!assignees.includes(currentUser.id)
|
||||||
|
|| instance.userId !== currentUser.id
|
||||||
|
) {
|
||||||
|
return context.throw(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: validate assignee
|
||||||
|
await instance.update({
|
||||||
|
status: values.status,
|
||||||
|
result: values.result
|
||||||
|
});
|
||||||
|
|
||||||
|
context.body = instance;
|
||||||
|
context.status = 202;
|
||||||
|
|
||||||
|
await next();
|
||||||
|
|
||||||
|
instance.job.latestUserJob = instance;
|
||||||
|
|
||||||
|
const plugin = context.app.pm.get('@nocobase/plugin-workflow');
|
||||||
|
const processor = plugin.createProcessor(instance.execution);
|
||||||
|
// NOTE: resume the process and no `await` for quick returning
|
||||||
|
processor.resume(instance.job);
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { extend } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default extend({
|
||||||
|
name: 'jobs',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'users',
|
||||||
|
through: 'users_jobs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'usersJobs',
|
||||||
|
target: 'users_jobs',
|
||||||
|
foreignKey: 'jobId'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
@ -0,0 +1,17 @@
|
|||||||
|
import { extend } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default extend({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'jobs',
|
||||||
|
through: 'users_jobs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'usersJobs',
|
||||||
|
target: 'users_jobs'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
@ -0,0 +1,42 @@
|
|||||||
|
import { CollectionOptions } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'users_jobs',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'integer',
|
||||||
|
name: 'id',
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'job'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'execution'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'node',
|
||||||
|
target: 'flow_nodes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'workflow'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'integer',
|
||||||
|
name: 'status'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'jsonb',
|
||||||
|
name: 'result'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as CollectionOptions;
|
@ -0,0 +1,210 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { requireModule } from '@nocobase/utils';
|
||||||
|
import { Context } from '@nocobase/actions';
|
||||||
|
|
||||||
|
import Plugin from '../../Plugin';
|
||||||
|
import Prompt, { PromptConfig } from '../../instructions/prompt';
|
||||||
|
import { submit } from './actions';
|
||||||
|
import { JOB_STATUS } from '../../constants';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface AssignedPromptConfig extends PromptConfig {
|
||||||
|
assignees?: number[];
|
||||||
|
mode?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: for single record mode (mode: 0/null)
|
||||||
|
async function middleware(context: Context, next) {
|
||||||
|
const { body: job, state, action } = context;
|
||||||
|
const { assignees, mode } = job.node.config as AssignedPromptConfig;
|
||||||
|
|
||||||
|
// NOTE: skip to no user implementation
|
||||||
|
if (!assignees) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.currentUser) {
|
||||||
|
return context.throw(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!assignees.includes(state.currentUser.id)) {
|
||||||
|
return context.throw(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: multiple record mode could not use jobs:submit action
|
||||||
|
// should use users_jobs:submit/:id instead
|
||||||
|
if (mode) {
|
||||||
|
return context.throw(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
userId: context.state.currentUser.id,
|
||||||
|
jobId: job.id,
|
||||||
|
nodeId: job.nodeId,
|
||||||
|
executionId: job.executionId,
|
||||||
|
workflowId: job.execution.workflowId,
|
||||||
|
status: job.status,
|
||||||
|
result: job.result
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE: update users job after main job is done
|
||||||
|
const UserJobModel = context.db.getModel('users_jobs');
|
||||||
|
let userJob = await UserJobModel.findOne({
|
||||||
|
where: {
|
||||||
|
userId: context.state.currentUser.id,
|
||||||
|
jobId: job.id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (userJob) {
|
||||||
|
await userJob.update(data);
|
||||||
|
} else {
|
||||||
|
userJob = await UserJobModel.create(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(node, prevJob, processor) {
|
||||||
|
const { assignees, mode } = node.config as AssignedPromptConfig;
|
||||||
|
if (!assignees) {
|
||||||
|
const { plugin } = processor.options;
|
||||||
|
const origin = plugin.instructions.get('prompt') as Prompt;
|
||||||
|
return origin.constructor.prototype.run.call(this, node, prevJob, processor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = await processor.saveJob({
|
||||||
|
status: JOB_STATUS.PENDING,
|
||||||
|
result: mode ? [] : null,
|
||||||
|
nodeId: node.id,
|
||||||
|
upstreamId: prevJob?.id ?? null
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: batch create users jobs
|
||||||
|
const UserJobModel = processor.options.plugin.db.getModel('users_jobs');
|
||||||
|
await UserJobModel.bulkCreate(assignees.map(userId => ({
|
||||||
|
userId,
|
||||||
|
jobId: job.id,
|
||||||
|
nodeId: node.id,
|
||||||
|
executionId: job.executionId,
|
||||||
|
workflowId: node.workflowId,
|
||||||
|
status: JOB_STATUS.PENDING
|
||||||
|
})), {
|
||||||
|
transaction: processor.transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROMPT_ASSIGNED_MODE = {
|
||||||
|
SINGLE: Symbol('single'),
|
||||||
|
ALL: Symbol('all'),
|
||||||
|
ANY: Symbol('any'),
|
||||||
|
ALL_PERCENTAGE: Symbol('all percentage'),
|
||||||
|
ANY_PERCENTAGE: Symbol('any percentage')
|
||||||
|
};
|
||||||
|
|
||||||
|
const Modes = {
|
||||||
|
[PROMPT_ASSIGNED_MODE.SINGLE]: {
|
||||||
|
getStatus(distribution, assignees) {
|
||||||
|
const done = distribution.find(item => item.status !== JOB_STATUS.PENDING && item.count > 0);
|
||||||
|
return done ? done.status : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[PROMPT_ASSIGNED_MODE.ALL]: {
|
||||||
|
getStatus(distribution, assignees) {
|
||||||
|
const resolved = distribution.find(item => item.status === JOB_STATUS.RESOLVED);
|
||||||
|
if (resolved && resolved.count === assignees.length) {
|
||||||
|
return JOB_STATUS.RESOLVED;
|
||||||
|
}
|
||||||
|
// NOTE: `rejected` or `canceled`
|
||||||
|
const failed = distribution.find(item => item.status < JOB_STATUS.PENDING);
|
||||||
|
if (failed && failed.count) {
|
||||||
|
return failed.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[PROMPT_ASSIGNED_MODE.ANY]: {
|
||||||
|
getStatus(distribution, assignees) {
|
||||||
|
const resolved = distribution.find(item => item.status === JOB_STATUS.RESOLVED);
|
||||||
|
if (resolved && resolved.count) {
|
||||||
|
return JOB_STATUS.RESOLVED;
|
||||||
|
}
|
||||||
|
const failedCount = distribution.reduce((count, item) => item.status < JOB_STATUS.PENDING ? count + item.count : count, 0);
|
||||||
|
// NOTE: all failures are considered as rejected for now
|
||||||
|
if (failedCount === assignees.length) {
|
||||||
|
return JOB_STATUS.REJECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMode(mode) {
|
||||||
|
switch (true) {
|
||||||
|
case mode === 1:
|
||||||
|
return Modes[PROMPT_ASSIGNED_MODE.ALL];
|
||||||
|
case mode === -1:
|
||||||
|
return Modes[PROMPT_ASSIGNED_MODE.ANY];
|
||||||
|
case mode > 0:
|
||||||
|
return Modes[PROMPT_ASSIGNED_MODE.ALL_PERCENTAGE];
|
||||||
|
case mode < 0:
|
||||||
|
return Modes[PROMPT_ASSIGNED_MODE.ANY_PERCENTAGE];
|
||||||
|
default:
|
||||||
|
return Modes[PROMPT_ASSIGNED_MODE.SINGLE];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resume(node, job, processor) {
|
||||||
|
// NOTE: check all users jobs related if all done then continue as parallel
|
||||||
|
const { assignees, mode } = node.config as AssignedPromptConfig;
|
||||||
|
|
||||||
|
if (!assignees) {
|
||||||
|
const { plugin } = processor.options;
|
||||||
|
const origin = plugin.instructions.get('prompt') as Prompt;
|
||||||
|
return origin.constructor.prototype.resume.call(this, node, job, processor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserJobModel = processor.options.plugin.db.getModel('users_jobs');
|
||||||
|
const distribution = await UserJobModel.count({
|
||||||
|
where: {
|
||||||
|
jobId: job.id
|
||||||
|
},
|
||||||
|
group: ['status']
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitted = distribution.reduce((count, item) => item.status !== JOB_STATUS.PENDING ? count + item.count : count, 0);
|
||||||
|
const result = mode ? (submitted || 0) / assignees.length : job.latestUserJob?.result ?? job.result;
|
||||||
|
job.set({
|
||||||
|
status: getMode(mode).getStatus(distribution, assignees) ?? JOB_STATUS.PENDING,
|
||||||
|
result
|
||||||
|
});
|
||||||
|
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function(plugin: Plugin) {
|
||||||
|
const instruction = plugin.instructions.get('prompt') as Prompt;
|
||||||
|
instruction.extend({
|
||||||
|
run,
|
||||||
|
resume
|
||||||
|
});
|
||||||
|
|
||||||
|
instruction.use(middleware);
|
||||||
|
|
||||||
|
// TODO(bug): through table should be load first because primary
|
||||||
|
// await plugin.db.import({
|
||||||
|
// directory: path.join(__dirname, './collections')
|
||||||
|
// });
|
||||||
|
plugin.db.collection(requireModule(path.join(__dirname, './collections/users_jobs')));
|
||||||
|
plugin.db.collection(requireModule(path.join(__dirname, './collections/users')));
|
||||||
|
plugin.db.collection(requireModule(path.join(__dirname, './collections/jobs')));
|
||||||
|
|
||||||
|
plugin.app.actions({
|
||||||
|
'users_jobs:submit': submit
|
||||||
|
});
|
||||||
|
}
|
5
packages/plugins/workflow/src/server/extensions/index.ts
Normal file
5
packages/plugins/workflow/src/server/extensions/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import assignees from './assignees';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
assignees
|
||||||
|
];
|
@ -3,6 +3,7 @@ import { EXECUTION_STATUS, JOB_STATUS } from "../constants";
|
|||||||
import ExecutionModel from '../models/Execution';
|
import ExecutionModel from '../models/Execution';
|
||||||
import JobModel from '../models/Job';
|
import JobModel from '../models/Job';
|
||||||
import Processor from '../Processor';
|
import Processor from '../Processor';
|
||||||
|
import { Instruction } from '.';
|
||||||
|
|
||||||
type ValueOf<T> = T[keyof T];
|
type ValueOf<T> = T[keyof T];
|
||||||
|
|
||||||
@ -11,12 +12,12 @@ interface DelayConfig {
|
|||||||
duration: number;
|
duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class {
|
export default class implements Instruction {
|
||||||
timers: Map<number, NodeJS.Timeout> = new Map();
|
timers: Map<number, NodeJS.Timeout> = new Map();
|
||||||
|
|
||||||
constructor(protected plugin: Plugin) {
|
constructor(protected plugin: Plugin) {
|
||||||
plugin.app.on('beforeStart', () => this.load());
|
plugin.app.on('beforeStart', () => this.load());
|
||||||
plugin.app.on('beforeStop', () => this.unload())
|
plugin.app.on('beforeStop', () => this.unload());
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
@ -7,15 +7,6 @@ import FlowNodeModel from '../models/FlowNode';
|
|||||||
import Plugin from '..';
|
import Plugin from '..';
|
||||||
import Processor from '../Processor';
|
import Processor from '../Processor';
|
||||||
|
|
||||||
import prompt from './prompt';
|
|
||||||
import calculation from './calculation';
|
|
||||||
import condition from './condition';
|
|
||||||
import parallel from './parallel';
|
|
||||||
import query from './query';
|
|
||||||
import create from './create';
|
|
||||||
import update from './update';
|
|
||||||
import destroy from './destroy';
|
|
||||||
|
|
||||||
export type Job = {
|
export type Job = {
|
||||||
status: number;
|
status: number;
|
||||||
result?: unknown;
|
result?: unknown;
|
||||||
@ -51,18 +42,23 @@ export default function<T extends Instruction>(
|
|||||||
) {
|
) {
|
||||||
const { instructions } = plugin;
|
const { instructions } = plugin;
|
||||||
|
|
||||||
instructions.register('prompt', prompt);
|
const natives = [
|
||||||
instructions.register('calculation', calculation);
|
'calculation',
|
||||||
instructions.register('condition', condition);
|
'condition',
|
||||||
instructions.register('parallel', parallel);
|
'parallel',
|
||||||
instructions.register('query', query);
|
'delay',
|
||||||
instructions.register('create', create);
|
'prompt',
|
||||||
instructions.register('update', update);
|
'query',
|
||||||
instructions.register('destroy', destroy);
|
'create',
|
||||||
|
'update',
|
||||||
|
'destroy'
|
||||||
|
].reduce((result, key) => Object.assign(result, { [key]: key }), {});
|
||||||
|
|
||||||
instructions.register('delay', new (requireModule(path.join(__dirname, 'delay')))(plugin));
|
for (const [name, value] of Object.entries({ ...more, ...natives })) {
|
||||||
|
const instruction = typeof value === 'string'
|
||||||
|
? requireModule(path.isAbsolute(value) ? value : path.join(__dirname, value))
|
||||||
|
: value;
|
||||||
|
|
||||||
for (const [name, instruction] of Object.entries(more)) {
|
|
||||||
instructions.register(name, typeof instruction === 'function' ? new instruction(plugin) : instruction);
|
instructions.register(name, typeof instruction === 'function' ? new instruction(plugin) : instruction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,82 @@
|
|||||||
import { JOB_STATUS } from "../constants";
|
import compose from 'koa-compose';
|
||||||
|
|
||||||
|
import { Context, utils } from '@nocobase/actions';
|
||||||
|
|
||||||
|
import Plugin from '..';
|
||||||
|
import { JOB_STATUS } from "../constants";
|
||||||
|
import { Instruction } from '.';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface PromptConfig {
|
||||||
|
fields: [];
|
||||||
|
actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadJob(context: Context, next) {
|
||||||
|
const { filterByTk, values } = context.action.params;
|
||||||
|
if (!context.body) {
|
||||||
|
const jobRepo = utils.getRepositoryFromParams(context);
|
||||||
|
const job = await jobRepo.findOne({
|
||||||
|
filterByTk,
|
||||||
|
appends: ['node', 'execution'],
|
||||||
|
context
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filterByTk || !job) {
|
||||||
|
return context.throw(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache
|
||||||
|
context.body = job;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, config } = context.body.node;
|
||||||
|
if (type === 'prompt'
|
||||||
|
&& config.actions
|
||||||
|
&& !config.actions[values.status]) {
|
||||||
|
return context.throw(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class implements Instruction {
|
||||||
|
middlewares = [];
|
||||||
|
|
||||||
|
constructor(protected plugin: Plugin) {
|
||||||
|
plugin.app.resourcer.use(this.middleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware = async (context: Context, next) => {
|
||||||
|
const { actionName, resourceName } = context.action;
|
||||||
|
if (actionName === 'submit'
|
||||||
|
&& resourceName === 'jobs'
|
||||||
|
&& this.middlewares.length
|
||||||
|
) {
|
||||||
|
return compose([loadJob, ...this.middlewares])(context, next);
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
||||||
|
use(middleware) {
|
||||||
|
this.middlewares.push(middleware);
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
|
||||||
run(node, input, processor) {
|
run(node, input, processor) {
|
||||||
return {
|
return {
|
||||||
status: JOB_STATUS.PENDING
|
status: JOB_STATUS.PENDING
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
resume(node, job, processor) {
|
resume(node, job, processor) {
|
||||||
job.set('status', JOB_STATUS.RESOLVED);
|
if (!node.config.actions) {
|
||||||
|
job.set('status', JOB_STATUS.RESOLVED);
|
||||||
|
}
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extend(options: Instruction) {
|
||||||
|
Object.assign(this, options);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user