codehooks-js 1.2.20 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +11 -1
- package/package.json +11 -3
- package/tsconfig.json +34 -21
- package/types/aggregation/index.d.mts +1 -38
- package/types/crudlify/index.d.mts +5 -9
- package/types/crudlify/lib/eventhooks.d.mts +0 -1
- package/types/crudlify/lib/query/q2m/index.d.mts +0 -7
- package/types/crudlify/lib/query/q2m/q2m.d.mts +0 -1
- package/types/crudlify/lib/schema/json-schema/index.d.mts +0 -1
- package/types/crudlify/lib/schema/yup/index.d.mts +1 -2
- package/types/crudlify/lib/schema/zod/index.d.mts +0 -1
- package/types/index.d.ts +117 -1023
- package/types/webserver.d.mts +1 -2
- package/types/workflow/engine.d.mts +123 -0
- package/types/workflow/engine.d.mts.map +1 -0
- package/types/workflow/index.d.mts +10 -0
- package/types/workflow/index.d.mts.map +1 -0
- package/webserver.mjs +42 -15
- package/workflow/engine.mjs +384 -0
- package/workflow/index.mjs +13 -0
package/types/webserver.d.mts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
export function serveStatic(options: any, app: any, filestore: any): void;
|
|
1
|
+
export function serveStatic(options: any, app: any, filestore: any, hook: any, fsoptions: any): void;
|
|
2
2
|
export function render(viewFile: any, data: any, settings: any, cb: any): Promise<any>;
|
|
3
|
-
//# sourceMappingURL=webserver.d.mts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export const setCollectionName: any;
|
|
2
|
+
export const setQueuePrefix: any;
|
|
3
|
+
export const Steps: StepsEngine;
|
|
4
|
+
declare const _default: StepsEngine;
|
|
5
|
+
export default _default;
|
|
6
|
+
/**
|
|
7
|
+
* StepsEngine class that manages step-based workflows
|
|
8
|
+
* @extends EventEmitter
|
|
9
|
+
*/
|
|
10
|
+
declare class StepsEngine extends EventEmitter<[never]> {
|
|
11
|
+
static instance: any;
|
|
12
|
+
static collectionName: string;
|
|
13
|
+
static queuePrefix: string;
|
|
14
|
+
/**
|
|
15
|
+
* Set the collection name for storing steps data
|
|
16
|
+
* @param {string} name - Collection name
|
|
17
|
+
* @throws {Error} If name is not a non-empty string
|
|
18
|
+
*/
|
|
19
|
+
static setCollectionName(name: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Set the queue prefix for steps jobs
|
|
22
|
+
* @param {string} prefix - Queue prefix
|
|
23
|
+
* @throws {Error} If prefix is not a non-empty string
|
|
24
|
+
*/
|
|
25
|
+
static setQueuePrefix(prefix: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Get the singleton instance of StepsEngine
|
|
28
|
+
* @returns {StepsEngine} The singleton instance
|
|
29
|
+
*/
|
|
30
|
+
static getInstance(): StepsEngine;
|
|
31
|
+
constructor();
|
|
32
|
+
definitions: Map<any, any>;
|
|
33
|
+
/**
|
|
34
|
+
* Get the step definition for a specific step
|
|
35
|
+
* @param {string} stepsName - Name of the steps workflow
|
|
36
|
+
* @param {string} stepName - Name of the step
|
|
37
|
+
* @returns {Function} The step function
|
|
38
|
+
* @throws {Error} If steps definition or step function not found
|
|
39
|
+
*/
|
|
40
|
+
getDefinition(stepsName: string, stepName: string): Function;
|
|
41
|
+
/**
|
|
42
|
+
* Validate a step definition
|
|
43
|
+
* @param {Function} step - The step function to validate
|
|
44
|
+
* @throws {Error} If step definition is invalid
|
|
45
|
+
*/
|
|
46
|
+
validateStepDefinition(step: Function): void;
|
|
47
|
+
/**
|
|
48
|
+
* Handle the next step in the workflow
|
|
49
|
+
* @param {string} stepsName - Name of the steps workflow
|
|
50
|
+
* @param {string|null} nextStep - Name of the next step or null if finished
|
|
51
|
+
* @param {Object} newState - The new state to set
|
|
52
|
+
* @param {string} instanceId - ID of the steps instance
|
|
53
|
+
* @param {Object} options - Step options (waitForInput, timeout, retry, abort)
|
|
54
|
+
* @returns {Promise<string|string[]>} Queue ID(s) for the next step(s)
|
|
55
|
+
* @throws {Error} If step execution fails
|
|
56
|
+
*/
|
|
57
|
+
handleNextStep(stepsName: string, nextStep: string | null, newState: any, instanceId: string, options: any): Promise<string | string[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Register a new steps workflow
|
|
60
|
+
* @param {Codehooks} app - Codehooks application instance
|
|
61
|
+
* @param {string} name - Unique identifier for the steps workflow
|
|
62
|
+
* @param {string} description - Human-readable description
|
|
63
|
+
* @param {Object} definition - Object containing step definitions
|
|
64
|
+
* @returns {Promise<string>} The registered steps name
|
|
65
|
+
* @throws {Error} If step definition is invalid
|
|
66
|
+
*/
|
|
67
|
+
register(app: Codehooks, name: string, description: string, definition: any): Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* Start a new steps instance
|
|
70
|
+
* @param {string} name - Name of the steps workflow to start
|
|
71
|
+
* @param {Object} initialState - Initial state for the steps instance
|
|
72
|
+
* @returns {Promise<{id: string}>} The steps instance ID
|
|
73
|
+
* @throws {Error} If starting steps fails
|
|
74
|
+
*/
|
|
75
|
+
start(name: string, initialState: any): Promise<{
|
|
76
|
+
id: string;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Update the state of a steps instance
|
|
80
|
+
* @param {string} stepsName - Name of the steps workflow
|
|
81
|
+
* @param {string} instanceId - ID of the steps instance
|
|
82
|
+
* @param {Object} state - New state to update with
|
|
83
|
+
* @param {Object} options - Options for the update, { continue: false } to avoid continuing the the step
|
|
84
|
+
* @returns {Promise<Object>} The updated state
|
|
85
|
+
*/
|
|
86
|
+
updateState(stepsName: string, instanceId: string, state: any, options?: any): Promise<any>;
|
|
87
|
+
/**
|
|
88
|
+
* Set the complete state of a steps instance
|
|
89
|
+
* @param {string} instanceId - ID of the steps instance
|
|
90
|
+
* @param {Object} state - Complete state to set
|
|
91
|
+
* @returns {Promise<void>}
|
|
92
|
+
*/
|
|
93
|
+
setState(instanceId: string, { _id, state }: any): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Continue a paused steps instance
|
|
96
|
+
* @param {string} stepsName - Name of the steps workflow
|
|
97
|
+
* @param {string} instanceId - ID of the steps instance
|
|
98
|
+
* @returns {Promise<{qId: string}>} Queue ID for the continued step
|
|
99
|
+
* @throws {Error} If steps instance not found
|
|
100
|
+
*/
|
|
101
|
+
continue(stepsName: string, instanceId: string): Promise<{
|
|
102
|
+
qId: string;
|
|
103
|
+
}>;
|
|
104
|
+
/**
|
|
105
|
+
* Get the status of a steps instance
|
|
106
|
+
* @param {string} id - ID of the steps instance
|
|
107
|
+
* @returns {Promise<Object>} The steps status
|
|
108
|
+
*/
|
|
109
|
+
getStepsStatus(id: string): Promise<any>;
|
|
110
|
+
/**
|
|
111
|
+
* List all steps instances matching a filter
|
|
112
|
+
* @param {Object} filter - Filter criteria for steps workflows
|
|
113
|
+
* @returns {Promise<Array>} List of steps instances
|
|
114
|
+
*/
|
|
115
|
+
getInstances(filter: any): Promise<any[]>;
|
|
116
|
+
/**
|
|
117
|
+
* Cancel a steps instance
|
|
118
|
+
* @param {string} id - ID of the steps instance to cancel
|
|
119
|
+
* @returns {Promise<Object>} The cancellation result
|
|
120
|
+
*/
|
|
121
|
+
cancelSteps(id: string): Promise<any>;
|
|
122
|
+
}
|
|
123
|
+
import { EventEmitter } from 'events';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.mts","sourceRoot":"","sources":["../../workflow/engine.mjs"],"names":[],"mappings":"AAsXA,oCAAiF;AACjF,iCAA2E;AAG3E,gCAA+C;;;AAnX/C;;;GAGG;AACH;IAQI,qBAAuB;IAGvB,8BAAoC;IACpC,2BAAkC;IAElC;;;;OAIG;IACH,+BAHW,MAAM,QAQhB;IAED;;;;OAIG;IACH,8BAHW,MAAM,QAQhB;IAED;;;OAGG;IACH,sBAFa,WAAW,CAOvB;IA3CG,iBAA4B;IA6ChC;;;;;;OAMG;IACH,yBALW,MAAM,YACN,MAAM,YAehB;IAED;;;;OAIG;IACH,6CAOC;IAED;;;;;;;;;OASG;IACH,0BARW,MAAM,YACN,MAAM,GAAC,IAAI,6BAEX,MAAM,iBAEJ,OAAO,CAAC,MAAM,GAAC,MAAM,EAAE,CAAC,CA4FpC;IAED;;;;;;;;OAQG;IACH,cAPW,SAAS,QACT,MAAM,eACN,MAAM,oBAEJ,OAAO,CAAC,MAAM,CAAC,CA6B3B;IAED;;;;;;OAMG;IACH,YALW,MAAM,sBAEJ,OAAO,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC,CAgCjC;IAED;;;;;;OAMG;IACH,uBALW,MAAM,cACN,MAAM,eAEJ,OAAO,KAAQ,CAW3B;IAED;;;;;OAKG;IACH,qBAJW,MAAM,wBAEJ,OAAO,CAAC,IAAI,CAAC,CAKzB;IAED;;;;;;OAMG;IACH,oBALW,MAAM,cACN,MAAM,GACJ,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,CAAC,CAwBlC;IAED;;;;OAIG;IACH,mBAHW,MAAM,GACJ,OAAO,KAAQ,CAQ3B;IAED;;;;OAIG;IACH,wBAFa,OAAO,OAAO,CAS1B;IAED;;;;OAIG;IACH,gBAHW,MAAM,GACJ,OAAO,KAAQ,CAW3B;CACJ"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export namespace StepsConfig {
|
|
2
|
+
export { setCollectionName };
|
|
3
|
+
export { setQueuePrefix };
|
|
4
|
+
}
|
|
5
|
+
export { Steps };
|
|
6
|
+
export default StepsEngine;
|
|
7
|
+
import { setCollectionName } from './engine.mjs';
|
|
8
|
+
import { setQueuePrefix } from './engine.mjs';
|
|
9
|
+
import { Steps } from './engine.mjs';
|
|
10
|
+
import StepsEngine from './engine.mjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../workflow/index.mjs"],"names":[],"mappings":";;;;;;kCAAsE,cAAc;+BAAd,cAAc;sBAAd,cAAc;wBAAd,cAAc"}
|
package/webserver.mjs
CHANGED
|
@@ -69,8 +69,9 @@ export function serveStatic(options, app, filestore, hook, fsoptions) {
|
|
|
69
69
|
|
|
70
70
|
// get mime type
|
|
71
71
|
const type = mime.getType(filePath)
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
const filestream = await filestore.getReadStream(filePath, fsoptions);
|
|
74
|
+
console.debug('Serve file', filePath, type)
|
|
74
75
|
res.set('Content-Type', type || 'application/octet-stream');
|
|
75
76
|
if (options.headers) {
|
|
76
77
|
res.headers(options.headers);
|
|
@@ -91,7 +92,7 @@ export function serveStatic(options, app, filestore, hook, fsoptions) {
|
|
|
91
92
|
})
|
|
92
93
|
|
|
93
94
|
} catch (error) {
|
|
94
|
-
console.debug("Error
|
|
95
|
+
console.debug("Error serving file", filePath, error.message)
|
|
95
96
|
if (options.notFound) {
|
|
96
97
|
// read default file
|
|
97
98
|
// add directory filesystem path
|
|
@@ -103,6 +104,7 @@ export function serveStatic(options, app, filestore, hook, fsoptions) {
|
|
|
103
104
|
const type = mime.getType(filePath)
|
|
104
105
|
const filestream = await filestore.getReadStream(filePath, fsoptions);
|
|
105
106
|
res.set('Content-Type', type || 'application/octet-stream');
|
|
107
|
+
res.status(404);
|
|
106
108
|
filestream
|
|
107
109
|
.on('data', (buf) => {
|
|
108
110
|
res.write(buf, 'buffer')
|
|
@@ -168,28 +170,53 @@ async function ejs(viewFile, data, settings, cb) {
|
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
let layoutFile = null;
|
|
171
174
|
async function handlebars(viewFile, data, settings, cb) {
|
|
172
175
|
try {
|
|
176
|
+
const db = await Datastore.open();
|
|
173
177
|
let engine = settings['view engine'].hbs || settings['view engine'].handlebars;
|
|
174
178
|
let layout = settings['layout'];
|
|
175
179
|
let views = settings['views'];
|
|
176
180
|
let tmpl = null;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
//console.log('Hbs view init', layout, views)
|
|
182
|
+
if (layout) {
|
|
183
|
+
const layoutPath = `${layout}`;
|
|
184
|
+
|
|
185
|
+
if (!layoutFile) {
|
|
186
|
+
|
|
187
|
+
const cachedLayout = await db.get(`hbs:layout:${layout}`);
|
|
188
|
+
if (cachedLayout) {
|
|
189
|
+
console.debug('Layout load from cache', layoutPath)
|
|
190
|
+
layoutFile = cachedLayout;
|
|
191
|
+
} else {
|
|
192
|
+
console.debug('Layout load from file', layoutPath)
|
|
193
|
+
layoutFile = await _coho_fs.readFile(layoutPath, {source: true});
|
|
194
|
+
db.set(`hbs:layout:${layout}`, layoutFile);
|
|
195
|
+
}
|
|
187
196
|
}
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
//console.log('Layout file', layoutFile)
|
|
198
|
+
engine.registerPartial('layout', layoutFile);
|
|
199
|
+
}
|
|
200
|
+
let txtfile = await db.get(`hbs:${viewFile}`);
|
|
201
|
+
|
|
202
|
+
if (tmplCache[viewFile] !== undefined) {
|
|
203
|
+
console.debug('Render view from cache', tmplCache[viewFile])
|
|
204
|
+
tmpl = tmplCache[viewFile];
|
|
205
|
+
return cb(null, tmpl(data));
|
|
206
|
+
}
|
|
207
|
+
if (txtfile) {
|
|
208
|
+
console.debug('Render view from db')
|
|
190
209
|
tmplCache[viewFile] = engine.compile(txtfile);
|
|
191
210
|
tmpl = tmplCache[viewFile];
|
|
192
|
-
|
|
211
|
+
return cb(null, tmpl(data));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
txtfile = await _coho_fs.readFile(`${settings.views}/${viewFile}.hbs`, {source: true});
|
|
216
|
+
console.debug('Creating hbs template', viewFile)
|
|
217
|
+
tmplCache[viewFile] = engine.compile(txtfile);
|
|
218
|
+
tmpl = tmplCache[viewFile];
|
|
219
|
+
db.set(`hbs:${viewFile}`, txtfile);
|
|
193
220
|
cb(null, tmpl(data));
|
|
194
221
|
} catch (err) {
|
|
195
222
|
console.error('Handle bars engine error:', err)
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Implements a steps engine that uses Codehooks.io as the state storage and queues for persistent workers.
|
|
3
|
+
The engine manages step transitions, state persistence, and event handling for step-based workflows.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* StepsEngine class that manages step-based workflows
|
|
10
|
+
* @extends EventEmitter
|
|
11
|
+
*/
|
|
12
|
+
class StepsEngine extends EventEmitter {
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.definitions = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Singleton instance
|
|
20
|
+
static instance = null;
|
|
21
|
+
|
|
22
|
+
// Configuration
|
|
23
|
+
static collectionName = 'stepsdata';
|
|
24
|
+
static queuePrefix = 'stepsqueue';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Set the collection name for storing steps data
|
|
28
|
+
* @param {string} name - Collection name
|
|
29
|
+
* @throws {Error} If name is not a non-empty string
|
|
30
|
+
*/
|
|
31
|
+
static setCollectionName(name) {
|
|
32
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
33
|
+
throw new Error('Collection name must be a non-empty string');
|
|
34
|
+
}
|
|
35
|
+
StepsEngine.collectionName = name.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set the queue prefix for steps jobs
|
|
40
|
+
* @param {string} prefix - Queue prefix
|
|
41
|
+
* @throws {Error} If prefix is not a non-empty string
|
|
42
|
+
*/
|
|
43
|
+
static setQueuePrefix(prefix) {
|
|
44
|
+
if (typeof prefix !== 'string' || !prefix.trim()) {
|
|
45
|
+
throw new Error('Queue prefix must be a non-empty string');
|
|
46
|
+
}
|
|
47
|
+
StepsEngine.queuePrefix = prefix.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the singleton instance of StepsEngine
|
|
52
|
+
* @returns {StepsEngine} The singleton instance
|
|
53
|
+
*/
|
|
54
|
+
static getInstance() {
|
|
55
|
+
if (!StepsEngine.instance) {
|
|
56
|
+
StepsEngine.instance = new StepsEngine();
|
|
57
|
+
}
|
|
58
|
+
return StepsEngine.instance;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the step definition for a specific step
|
|
63
|
+
* @param {string} stepsName - Name of the steps workflow
|
|
64
|
+
* @param {string} stepName - Name of the step
|
|
65
|
+
* @returns {Function} The step function
|
|
66
|
+
* @throws {Error} If steps definition or step function not found
|
|
67
|
+
*/
|
|
68
|
+
getDefinition(stepsName, stepName) {
|
|
69
|
+
const stepsDef = this.definitions.get(stepsName);
|
|
70
|
+
if (!stepsDef) {
|
|
71
|
+
throw new Error(`No Steps definition found for: ${stepsName}`);
|
|
72
|
+
}
|
|
73
|
+
// Get the specific step function from that workflow
|
|
74
|
+
const stepFunc = stepsDef[stepName];
|
|
75
|
+
if (!stepFunc) {
|
|
76
|
+
throw new Error(`No step function found for: ${stepName} in steps ${stepsName}`);
|
|
77
|
+
}
|
|
78
|
+
return stepFunc;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate a step definition
|
|
83
|
+
* @param {Function} step - The step function to validate
|
|
84
|
+
* @throws {Error} If step definition is invalid
|
|
85
|
+
*/
|
|
86
|
+
validateStepDefinition(step) {
|
|
87
|
+
if (typeof step !== 'function') {
|
|
88
|
+
throw new Error('Step definition must be a function');
|
|
89
|
+
}
|
|
90
|
+
if (step.length !== 2) {
|
|
91
|
+
throw new Error('Step definition must accept exactly 2 parameters');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle the next step in the workflow
|
|
97
|
+
* @param {string} stepsName - Name of the steps workflow
|
|
98
|
+
* @param {string|null} nextStep - Name of the next step or null if finished
|
|
99
|
+
* @param {Object} newState - The new state to set
|
|
100
|
+
* @param {string} instanceId - ID of the steps instance
|
|
101
|
+
* @param {Object} options - Step options (waitForInput, timeout, retry, abort)
|
|
102
|
+
* @returns {Promise<string|string[]>} Queue ID(s) for the next step(s)
|
|
103
|
+
* @throws {Error} If step execution fails
|
|
104
|
+
*/
|
|
105
|
+
async handleNextStep(stepsName, nextStep, newState, instanceId, options) {
|
|
106
|
+
try {
|
|
107
|
+
// Handle array of next steps
|
|
108
|
+
if (Array.isArray(nextStep)) {
|
|
109
|
+
const results = [];
|
|
110
|
+
for (const step of nextStep) {
|
|
111
|
+
const qid = await this.handleNextStep(stepsName, step, newState, instanceId, options);
|
|
112
|
+
results.push(qid);
|
|
113
|
+
}
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const isFinished = nextStep === null;
|
|
118
|
+
|
|
119
|
+
// Handle single next step
|
|
120
|
+
StepsEngine.getInstance().emit('stepStarted', { stepsName, step: nextStep, state: newState, instanceId });
|
|
121
|
+
const connection = await Datastore.open();
|
|
122
|
+
// remove the _id from the newState
|
|
123
|
+
delete newState._id;
|
|
124
|
+
// Update the existing steps state in the database
|
|
125
|
+
newState = await connection.updateOne(StepsEngine.collectionName,
|
|
126
|
+
{ _id: instanceId },
|
|
127
|
+
{ $set: { ...newState, nextStep: nextStep, updatedAt: new Date().toISOString(), isFinished: isFinished } });
|
|
128
|
+
|
|
129
|
+
StepsEngine.getInstance().emit('stateUpdated', { stepsName, state: newState, instanceId });
|
|
130
|
+
|
|
131
|
+
// Get the next step function
|
|
132
|
+
const func = StepsEngine.getInstance().getDefinition(stepsName, nextStep);
|
|
133
|
+
|
|
134
|
+
// Check if the step is waiting for input
|
|
135
|
+
if (options && options.waitForInput === true) {
|
|
136
|
+
console.log('Steps workflow is paused, waiting for input for at step', nextStep);
|
|
137
|
+
this.emit('stepWaiting', { stepsName, step: nextStep, instanceId });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Call the next step function with the step context
|
|
143
|
+
func.call(this, {...newState, instanceId: newState._id}, async function (nextStep, userState, options) {
|
|
144
|
+
try {
|
|
145
|
+
if (nextStep === null) {
|
|
146
|
+
console.log('No next step, steps workflow completed');
|
|
147
|
+
StepsEngine.getInstance().emit('completed', { message: 'Steps workflow completed', ...userState });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Protect system-level properties
|
|
151
|
+
const protectedState = {
|
|
152
|
+
_id: newState._id,
|
|
153
|
+
nextStep: newState.nextStep,
|
|
154
|
+
createdAt: newState.createdAt,
|
|
155
|
+
updatedAt: newState.updatedAt,
|
|
156
|
+
instanceId: newState.instanceId,
|
|
157
|
+
isFinished: newState.isFinished
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Merge states with userState taking precedence, but protecting system fields
|
|
161
|
+
const mergedState = {
|
|
162
|
+
...newState,
|
|
163
|
+
...userState,
|
|
164
|
+
...protectedState
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Enqueue the next step
|
|
168
|
+
const qres = connection.enqueue(`${StepsEngine.queuePrefix}`, {
|
|
169
|
+
stepsName: stepsName,
|
|
170
|
+
goto: nextStep,
|
|
171
|
+
state: mergedState,
|
|
172
|
+
options: options,
|
|
173
|
+
instanceId: instanceId
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
StepsEngine.getInstance().emit('stepEnqueued', { stepsName, step: nextStep, state: newState, instanceId });
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.log('error', error.message);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error('Error in step function:', error.message);
|
|
184
|
+
const connection = await Datastore.open();
|
|
185
|
+
await connection.updateOne(StepsEngine.collectionName,
|
|
186
|
+
{ _id: instanceId },
|
|
187
|
+
{ $set: { lastError: error.message, updatedAt: new Date().toISOString() } });
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Register a new steps workflow
|
|
198
|
+
* @param {Codehooks} app - Codehooks application instance
|
|
199
|
+
* @param {string} name - Unique identifier for the steps workflow
|
|
200
|
+
* @param {string} description - Human-readable description
|
|
201
|
+
* @param {Object} definition - Object containing step definitions
|
|
202
|
+
* @returns {Promise<string>} The registered steps name
|
|
203
|
+
* @throws {Error} If step definition is invalid
|
|
204
|
+
*/
|
|
205
|
+
async register(app, name, description, definition) {
|
|
206
|
+
StepsEngine.getInstance().emit('stepsRegistered', { name, description });
|
|
207
|
+
|
|
208
|
+
// Validate each step in the definition
|
|
209
|
+
for (const [stepName, step] of Object.entries(definition)) {
|
|
210
|
+
try {
|
|
211
|
+
if (stepName !== null) {
|
|
212
|
+
app.worker(`${StepsEngine.queuePrefix}`, async (req, res) => {
|
|
213
|
+
const { stepsName, goto, state, instanceId, options } = req.body.payload;
|
|
214
|
+
try {
|
|
215
|
+
const qid = await StepsEngine.getInstance().handleNextStep(stepsName, goto, state, instanceId, options);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('Error in step function:', error.message);
|
|
218
|
+
} finally {
|
|
219
|
+
res.end();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(`Invalid step definition '${stepName}': ${error.message}`);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.definitions.set(name, definition);
|
|
230
|
+
return name;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Start a new steps instance
|
|
235
|
+
* @param {string} name - Name of the steps workflow to start
|
|
236
|
+
* @param {Object} initialState - Initial state for the steps instance
|
|
237
|
+
* @returns {Promise<{id: string}>} The steps instance ID
|
|
238
|
+
* @throws {Error} If starting steps fails
|
|
239
|
+
*/
|
|
240
|
+
async start(name, initialState) {
|
|
241
|
+
StepsEngine.getInstance().emit('stepsStarted', { name, initialState });
|
|
242
|
+
|
|
243
|
+
return new Promise(async (resolve, reject) => {
|
|
244
|
+
try {
|
|
245
|
+
const funcs = this.definitions.get(name);
|
|
246
|
+
const firstStepName = Object.keys(funcs)[0];
|
|
247
|
+
const firstStep = funcs[firstStepName];
|
|
248
|
+
if (!firstStep) {
|
|
249
|
+
reject(new Error('No start step defined in steps'));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const connection = await Datastore.open();
|
|
253
|
+
// Create a new steps state in the database
|
|
254
|
+
const newState = await connection.insertOne(StepsEngine.collectionName,
|
|
255
|
+
{ ...initialState, nextStep: firstStepName, createdAt: new Date().toISOString() });
|
|
256
|
+
const { _id: ID } = await connection.enqueue(`${StepsEngine.queuePrefix}`, {
|
|
257
|
+
stepsName: name,
|
|
258
|
+
goto: firstStepName,
|
|
259
|
+
state: newState,
|
|
260
|
+
instanceId: newState._id,
|
|
261
|
+
options: {}
|
|
262
|
+
});
|
|
263
|
+
resolve(newState);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error('Error starting steps:', error.message);
|
|
266
|
+
reject(error);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Update the state of a steps instance
|
|
273
|
+
* @param {string} stepsName - Name of the steps workflow
|
|
274
|
+
* @param {string} instanceId - ID of the steps instance
|
|
275
|
+
* @param {Object} state - New state to update with
|
|
276
|
+
* @param {Object} options - Options for the update, { continue: false } to avoid continuing the the step
|
|
277
|
+
* @returns {Promise<Object>} The updated state
|
|
278
|
+
*/
|
|
279
|
+
async updateState(stepsName, instanceId, state, options={continue: true}) {
|
|
280
|
+
StepsEngine.getInstance().emit('stepsStateUpdating', { stepsName, instanceId, state });
|
|
281
|
+
const connection = await Datastore.open();
|
|
282
|
+
return new Promise(async (resolve, reject) => {
|
|
283
|
+
const doc = await connection.updateOne(StepsEngine.collectionName,
|
|
284
|
+
{ _id: instanceId },
|
|
285
|
+
{ $set: { ...state, updatedAt: new Date().toISOString() } });
|
|
286
|
+
if (options.continue) {
|
|
287
|
+
await this.continue(stepsName, instanceId);
|
|
288
|
+
}
|
|
289
|
+
resolve({ ...doc });
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Set the complete state of a steps instance
|
|
295
|
+
* @param {string} instanceId - ID of the steps instance
|
|
296
|
+
* @param {Object} state - Complete state to set
|
|
297
|
+
* @returns {Promise<void>}
|
|
298
|
+
*/
|
|
299
|
+
async setState(instanceId, { _id, state }) {
|
|
300
|
+
const connection = await Datastore.open();
|
|
301
|
+
await connection.replaceOne(StepsEngine.collectionName, { _id: _id }, { ...state });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Continue a paused steps instance
|
|
306
|
+
* @param {string} stepsName - Name of the steps workflow
|
|
307
|
+
* @param {string} instanceId - ID of the steps instance
|
|
308
|
+
* @returns {Promise<{qId: string}>} Queue ID for the continued step
|
|
309
|
+
* @throws {Error} If steps instance not found
|
|
310
|
+
*/
|
|
311
|
+
async continue(stepsName, instanceId) {
|
|
312
|
+
const connection = await Datastore.open();
|
|
313
|
+
const state = await connection.findOne(StepsEngine.collectionName, { _id: instanceId });
|
|
314
|
+
if (!state) {
|
|
315
|
+
throw new Error(`No steps found with instanceId: ${instanceId}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log('continue state', state);
|
|
319
|
+
StepsEngine.getInstance().emit('stepContinued', { stepsName, step: state.nextStep, instanceId });
|
|
320
|
+
|
|
321
|
+
return new Promise(async (resolve, reject) => {
|
|
322
|
+
const { _id: ID } = await connection.enqueue(`${StepsEngine.queuePrefix}`, {
|
|
323
|
+
stepsName,
|
|
324
|
+
goto: state.nextStep,
|
|
325
|
+
state: state,
|
|
326
|
+
options: {},
|
|
327
|
+
instanceId: instanceId
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
resolve({ qId: ID });
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get the status of a steps instance
|
|
336
|
+
* @param {string} id - ID of the steps instance
|
|
337
|
+
* @returns {Promise<Object>} The steps status
|
|
338
|
+
*/
|
|
339
|
+
async getStepsStatus(id) {
|
|
340
|
+
return new Promise(async (resolve, reject) => {
|
|
341
|
+
const connection = await Datastore.open();
|
|
342
|
+
const state = await connection.findOne(StepsEngine.collectionName, { _id: id });
|
|
343
|
+
resolve(state);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* List all steps instances matching a filter
|
|
349
|
+
* @param {Object} filter - Filter criteria for steps workflows
|
|
350
|
+
* @returns {Promise<Array>} List of steps instances
|
|
351
|
+
*/
|
|
352
|
+
async getInstances(filter) {
|
|
353
|
+
return new Promise(async (resolve, reject) => {
|
|
354
|
+
const connection = await Datastore.open();
|
|
355
|
+
const states = await connection.find(StepsEngine.collectionName, filter).toArray();
|
|
356
|
+
console.log('listSteps', StepsEngine.collectionName, filter, states.length);
|
|
357
|
+
resolve(states);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Cancel a steps instance
|
|
363
|
+
* @param {string} id - ID of the steps instance to cancel
|
|
364
|
+
* @returns {Promise<Object>} The cancellation result
|
|
365
|
+
*/
|
|
366
|
+
async cancelSteps(id) {
|
|
367
|
+
StepsEngine.getInstance().emit('stepsCancelled', { id });
|
|
368
|
+
return new Promise(async (resolve, reject) => {
|
|
369
|
+
const connection = await Datastore.open();
|
|
370
|
+
const state = await connection.updateOne(StepsEngine.collectionName,
|
|
371
|
+
{ _id: id },
|
|
372
|
+
{ $set: { status: 'cancelled' } });
|
|
373
|
+
resolve(state);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Export the static methods directly
|
|
379
|
+
export const setCollectionName = StepsEngine.setCollectionName.bind(StepsEngine);
|
|
380
|
+
export const setQueuePrefix = StepsEngine.setQueuePrefix.bind(StepsEngine);
|
|
381
|
+
|
|
382
|
+
// Export the singleton instance
|
|
383
|
+
export const Steps = StepsEngine.getInstance();
|
|
384
|
+
export default StepsEngine.getInstance();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import StepsEngine, { setCollectionName, setQueuePrefix, Steps } from './engine.mjs';
|
|
2
|
+
|
|
3
|
+
// Re-export the static methods
|
|
4
|
+
export const StepsConfig = {
|
|
5
|
+
setCollectionName,
|
|
6
|
+
setQueuePrefix
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Re-export the workflow instance
|
|
10
|
+
export { Steps };
|
|
11
|
+
|
|
12
|
+
// Export the engine as default
|
|
13
|
+
export default StepsEngine;
|