@anteros/core 0.0.1-alpha.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/README.md +143 -0
- package/database/collection.ts +160 -0
- package/database/decorator.ts +172 -0
- package/database/file.ts +93 -0
- package/database/mongodbadapter.ts +1128 -0
- package/database/rest.ts +14 -0
- package/database/schema.ts +160 -0
- package/database/tenant.ts +37 -0
- package/database/workflow.ts +384 -0
- package/index.ts +28 -0
- package/lib/asyncContextStorage.ts +68 -0
- package/lib/define.ts +114 -0
- package/lib/error.ts +21 -0
- package/lib/files.ts +459 -0
- package/lib/middleware.ts +66 -0
- package/lib/routes.ts +44 -0
- package/lib/scripts.ts +47 -0
- package/lib/services.ts +45 -0
- package/lib/sockets.ts +44 -0
- package/lib/workflow.ts +60 -0
- package/package.json +31 -0
- package/server/api.ts +789 -0
- package/server/boot.ts +101 -0
- package/server/config.ts +107 -0
- package/server/env.ts +16 -0
- package/server/hono.ts +176 -0
- package/server/io.ts +15 -0
- package/server/routes.ts +48 -0
- package/server/security.ts +138 -0
- package/tests/api.test.ts +281 -0
- package/tsconfig.json +36 -0
- package/types/activity.d.ts +45 -0
- package/types/api.d.ts +85 -0
- package/types/collection.d.ts +82 -0
- package/types/config.d.ts +55 -0
- package/types/field.d.ts +72 -0
- package/types/file.d.ts +120 -0
- package/types/hook.d.ts +30 -0
- package/types/middleware.d.ts +18 -0
- package/types/mongo.d.ts +61 -0
- package/types/options.d.ts +7 -0
- package/types/rest.d.ts +18 -0
- package/types/route.d.ts +19 -0
- package/types/schema.d.ts +0 -0
- package/types/scripts.d.ts +10 -0
- package/types/service.d.ts +37 -0
- package/types/task.d.ts +12 -0
- package/types/tenant.d.ts +16 -0
- package/types/token.d.ts +14 -0
- package/types/websocket.d.ts +15 -0
- package/types/workflow.d.ts +91 -0
- package/utils/cache.ts +96 -0
- package/utils/crypto.ts +226 -0
- package/utils/func.ts +1037 -0
- package/utils/index.ts +17 -0
package/database/rest.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RestOptions } from "../types/rest";
|
|
2
|
+
import { MongoRest } from "./mongodbadapter";
|
|
3
|
+
|
|
4
|
+
class Rest extends MongoRest {
|
|
5
|
+
constructor(options: RestOptions) {
|
|
6
|
+
super(options);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const useRest = Rest
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
useRest,
|
|
14
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Collection as CollectionType } from "../types/collection";
|
|
3
|
+
import Joi, { type AnySchema } from "joi";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
let s = Joi.object({
|
|
7
|
+
name: Joi.string().required(),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
function buildSchema(col: CollectionType, opts = {
|
|
14
|
+
partial: false
|
|
15
|
+
}) {
|
|
16
|
+
|
|
17
|
+
let propertiesSchema = {
|
|
18
|
+
createdAt: Joi.date(),
|
|
19
|
+
updatedAt: Joi.date(),
|
|
20
|
+
} as {
|
|
21
|
+
[key: string]: AnySchema
|
|
22
|
+
}
|
|
23
|
+
for (const f of col.fields) {
|
|
24
|
+
|
|
25
|
+
if (f.type && f.name) {
|
|
26
|
+
|
|
27
|
+
let fieldName = f.name
|
|
28
|
+
|
|
29
|
+
if (f.type == 'string') {
|
|
30
|
+
propertiesSchema[fieldName] = Joi.string()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (f.type == 'password') {
|
|
34
|
+
propertiesSchema[fieldName] = Joi.string()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (f.type == 'number') {
|
|
38
|
+
propertiesSchema[fieldName] = Joi.number()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (f.type == 'integer') {
|
|
42
|
+
propertiesSchema[fieldName] = Joi.number().integer()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (f.type == 'boolean') {
|
|
46
|
+
propertiesSchema[fieldName] = Joi.boolean()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (f.type.match(/(date|datetime-local)/)) {
|
|
50
|
+
propertiesSchema[fieldName] = Joi.date()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (f.type == 'array') {
|
|
54
|
+
propertiesSchema[fieldName] = Joi.array()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (f.type == 'json') {
|
|
58
|
+
propertiesSchema[fieldName] = Joi.object()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (f.type == 'uuid') {
|
|
62
|
+
propertiesSchema[fieldName] = Joi.string().uuid()
|
|
63
|
+
}
|
|
64
|
+
if (f.type == 'email') {
|
|
65
|
+
propertiesSchema[fieldName] = Joi.string().email()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (f.type == 'url') {
|
|
69
|
+
propertiesSchema[fieldName] = Joi.string().uri()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (f.type == 'ipv4') {
|
|
73
|
+
propertiesSchema[fieldName] = Joi.string().ip({ version: 'ipv4' })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (f.type == 'ipv6') {
|
|
77
|
+
propertiesSchema[fieldName] = Joi.string().ip({ version: 'ipv6' })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (f.type == 'enum' && !f.enumOptions?.multiple) {
|
|
81
|
+
propertiesSchema[fieldName] = Joi.string().valid(...f.enumOptions?.items || [])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (f.type == 'enum' && f.enumOptions?.multiple) {
|
|
85
|
+
propertiesSchema[fieldName] = Joi.array().items(Joi.string().valid(...f.enumOptions?.items || []))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (f.type == 'random') {
|
|
89
|
+
propertiesSchema[fieldName] = Joi.string()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (f.type == 'random' && f?.randomOptions?.toNumber) {
|
|
93
|
+
propertiesSchema[fieldName] = Joi.number()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (f?.type.match(/(geojson\.Point|geojson\.LineString|geojson\.Polygon)/)) {
|
|
97
|
+
propertiesSchema[fieldName] = Joi.object({
|
|
98
|
+
type: Joi.string().valid("Point", "LineString", "Polygon", "MultiPoint").required(),
|
|
99
|
+
coordinates: Joi.alternatives().conditional("type", [
|
|
100
|
+
{
|
|
101
|
+
is: "Point",
|
|
102
|
+
then: Joi.array().items(Joi.number()).length(2).required(), // [lng, lat]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
is: Joi.string().valid("LineString", "MultiPoint"),
|
|
106
|
+
then: Joi.array().items(Joi.array().items(Joi.number()).length(2)).required(), // [[lng, lat], ...]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
is: "Polygon",
|
|
110
|
+
then: Joi.array().items(Joi.array().items(Joi.array().items(Joi.number()).length(2))).required(), // [[[lng, lat], ...]]
|
|
111
|
+
},
|
|
112
|
+
]),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (f?.type == 'relationship') {
|
|
117
|
+
propertiesSchema[fieldName] = Joi.string().optional().messages({
|
|
118
|
+
'string.base': `${f.name} must be a string (ObjectId)`,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (f?.type == 'relationship' && f?.relation?.hasMany) {
|
|
123
|
+
propertiesSchema[fieldName] = Joi.array().items(Joi.string().optional()).messages({
|
|
124
|
+
'string.base': `${f.name} must be a string (ObjectId)`,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (f.validate?.schema) {
|
|
129
|
+
propertiesSchema[fieldName] = f.validate.schema
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (f?.required) {
|
|
133
|
+
propertiesSchema[fieldName] = propertiesSchema[fieldName]?.required()!
|
|
134
|
+
} else {
|
|
135
|
+
propertiesSchema[fieldName] = propertiesSchema[fieldName]?.optional()!
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (f?.nullable) {
|
|
139
|
+
propertiesSchema[fieldName] = propertiesSchema[fieldName]?.allow(null)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (f?.empty) {
|
|
143
|
+
propertiesSchema[fieldName] = propertiesSchema[fieldName]?.allow('')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let schema = Joi.object(propertiesSchema).min(1)
|
|
150
|
+
return opts.partial ? buildSchemaForkOptional(schema) : schema
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildSchemaForkOptional(schemaPassed: AnySchema): AnySchema {
|
|
154
|
+
return schemaPassed.fork(
|
|
155
|
+
Object.keys(schemaPassed.describe().keys),
|
|
156
|
+
(field) => field.optional()
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { buildSchema, buildSchemaForkOptional }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Tenant } from "../types/tenant";
|
|
2
|
+
import { cfg } from "../server/config";
|
|
3
|
+
import { useRest } from "./rest";
|
|
4
|
+
async function syncTenants() {
|
|
5
|
+
try {
|
|
6
|
+
for await (let tenant of cfg.tenants ?? []) {
|
|
7
|
+
let rest = new useRest({
|
|
8
|
+
database: {
|
|
9
|
+
uri: tenant.database.uri,
|
|
10
|
+
options: {
|
|
11
|
+
timeoutMS: 2000,
|
|
12
|
+
...tenant.database.options,
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
const { client, db } = await rest.connect()
|
|
17
|
+
tenant.database.client = client
|
|
18
|
+
tenant.database.db = db
|
|
19
|
+
}
|
|
20
|
+
} catch (err: any) {
|
|
21
|
+
console.error('Error bootstrapping tenants', err?.message)
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getTenant(tenantId: string): Tenant | null {
|
|
27
|
+
let findTenant = cfg.tenants?.find(tenant => tenant.id == tenantId)
|
|
28
|
+
if (findTenant) {
|
|
29
|
+
return findTenant
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
syncTenants,
|
|
36
|
+
getTenant
|
|
37
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { cfg } from "../server/config";
|
|
2
|
+
import { getTenant } from "./tenant";
|
|
3
|
+
import { AppError, fn } from "../lib/error";
|
|
4
|
+
import * as func from "../utils/func";
|
|
5
|
+
import { useRest } from "./rest";
|
|
6
|
+
import { getWorkflow } from "../lib/workflow";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import type { WorkflowDefinition, WorkflowRun, WorkflowRunStep, WorkflowRunStatus } from "../types/workflow";
|
|
9
|
+
|
|
10
|
+
class Workflow {
|
|
11
|
+
#tenant_id: string;
|
|
12
|
+
|
|
13
|
+
constructor(tenant_id: string) {
|
|
14
|
+
this.#tenant_id = tenant_id;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private async getCollection() {
|
|
18
|
+
const tenant = getTenant(this.#tenant_id);
|
|
19
|
+
const db = tenant?.database?.db;
|
|
20
|
+
if (!db) throw new AppError('Database not found', { code: 'DB_NOT_FOUND', status: 500 });
|
|
21
|
+
return db.collection('_workflows_');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run a workflow by its ID with the given data.
|
|
26
|
+
* Executes each step sequentially and tracks progress.
|
|
27
|
+
*/
|
|
28
|
+
async run(workflowId: string, data: any, context?: any): Promise<WorkflowRun> {
|
|
29
|
+
const wf = getWorkflow(workflowId, this.#tenant_id);
|
|
30
|
+
if (!wf) throw new AppError(`Workflow '${workflowId}' not found`, { code: 'WORKFLOW_NOT_FOUND', status: 404 });
|
|
31
|
+
|
|
32
|
+
const runId = crypto.randomUUID();
|
|
33
|
+
const now = new Date();
|
|
34
|
+
|
|
35
|
+
const run: WorkflowRun = {
|
|
36
|
+
_id: runId,
|
|
37
|
+
workflowId,
|
|
38
|
+
workflowVersion: wf.version,
|
|
39
|
+
tenant_id: this.#tenant_id,
|
|
40
|
+
status: 'running',
|
|
41
|
+
progress: 0,
|
|
42
|
+
data,
|
|
43
|
+
context,
|
|
44
|
+
currentStep: 0,
|
|
45
|
+
totalExecuted: 0,
|
|
46
|
+
totalSkipped: 0,
|
|
47
|
+
steps: wf.steps.map(s => ({
|
|
48
|
+
stepId: s.id,
|
|
49
|
+
name: s.name,
|
|
50
|
+
status: 'pending' as WorkflowRunStatus,
|
|
51
|
+
startedAt: now,
|
|
52
|
+
})),
|
|
53
|
+
compensations: [],
|
|
54
|
+
createdAt: now,
|
|
55
|
+
updatedAt: now,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
await this.saveRun(run);
|
|
59
|
+
|
|
60
|
+
// Execute global exec if present
|
|
61
|
+
try {
|
|
62
|
+
if (wf.exec) {
|
|
63
|
+
await wf.exec({ data, prevOutput: null, rest: this, error: fn.error, jwt: func.jwt, input: undefined });
|
|
64
|
+
}
|
|
65
|
+
} catch (err: any) {
|
|
66
|
+
run.status = 'failed';
|
|
67
|
+
run.error = { message: err.message || 'Workflow execution failed', stepId: '__global__' };
|
|
68
|
+
run.updatedAt = new Date();
|
|
69
|
+
await this.saveRun(run);
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Execute steps sequentially
|
|
74
|
+
for (let i = 0; i < wf.steps.length; i++) {
|
|
75
|
+
const stepDef = wf.steps[i]!;
|
|
76
|
+
run.currentStep = i;
|
|
77
|
+
const stepRun: WorkflowRunStep = {
|
|
78
|
+
stepId: stepDef.id,
|
|
79
|
+
name: stepDef.name,
|
|
80
|
+
status: 'running',
|
|
81
|
+
input: data,
|
|
82
|
+
startedAt: new Date(),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
run.steps[i] = stepRun;
|
|
86
|
+
run.updatedAt = new Date();
|
|
87
|
+
await this.saveRun(run);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const prevOutput = i > 0 ? run.steps[i - 1]?.output ?? null : null;
|
|
91
|
+
|
|
92
|
+
// Vérifier la condition du step
|
|
93
|
+
if (stepDef.condition) {
|
|
94
|
+
const shouldRun = await stepDef.condition({ data, prevOutput });
|
|
95
|
+
if (!shouldRun) {
|
|
96
|
+
stepRun.status = 'skipped';
|
|
97
|
+
stepRun.completedAt = new Date();
|
|
98
|
+
run.steps[i] = stepRun;
|
|
99
|
+
run.totalSkipped = run.steps.filter(s => s.status === 'skipped').length;
|
|
100
|
+
run.progress = Math.round(((i + 1) / wf.steps.length) * 100);
|
|
101
|
+
run.updatedAt = new Date();
|
|
102
|
+
await this.saveRun(run);
|
|
103
|
+
continue; // saute ce step
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = await stepDef.exec({ data, prevOutput, input: stepDef.input, rest: this, error: fn.error, jwt: func.jwt });
|
|
108
|
+
stepRun.status = 'completed';
|
|
109
|
+
stepRun.output = result;
|
|
110
|
+
stepRun.completedAt = new Date();
|
|
111
|
+
run.steps[i] = stepRun;
|
|
112
|
+
run.totalExecuted = run.steps.filter(s => s.status === 'completed').length;
|
|
113
|
+
run.progress = Math.round(((i + 1) / wf.steps.length) * 100);
|
|
114
|
+
} catch (err: any) {
|
|
115
|
+
stepRun.status = 'failed';
|
|
116
|
+
stepRun.error = { message: err.message || 'Step failed', code: err.code };
|
|
117
|
+
stepRun.completedAt = new Date();
|
|
118
|
+
run.steps[i] = stepRun;
|
|
119
|
+
run.totalExecuted = run.steps.filter(s => s.status === 'completed').length;
|
|
120
|
+
run.status = 'failed';
|
|
121
|
+
run.error = { message: err.message || 'Workflow failed', code: err.code, stepId: stepDef.id };
|
|
122
|
+
run.updatedAt = new Date();
|
|
123
|
+
await this.saveRun(run);
|
|
124
|
+
|
|
125
|
+
// Exécuter les compensations (steps réussis en ordre inverse)
|
|
126
|
+
if (wf.compensations?.length) {
|
|
127
|
+
const failedIndex = run.steps.findIndex(s => s.status === 'failed');
|
|
128
|
+
const stepsToCompensate = run.steps.slice(0, failedIndex).filter(s => s.status === 'completed').reverse();
|
|
129
|
+
run.compensations = [];
|
|
130
|
+
for (const completedStep of stepsToCompensate) {
|
|
131
|
+
const compDef = wf.compensations?.find(c => c.depend?.includes(completedStep.stepId) || c.id === completedStep.stepId);
|
|
132
|
+
if (!compDef?.exec) continue;
|
|
133
|
+
try {
|
|
134
|
+
await compDef.exec({
|
|
135
|
+
data: run.data,
|
|
136
|
+
prevOutput: completedStep.output,
|
|
137
|
+
input: undefined,
|
|
138
|
+
rest: this,
|
|
139
|
+
error: fn.error,
|
|
140
|
+
jwt: func.jwt,
|
|
141
|
+
});
|
|
142
|
+
run.compensations.push({
|
|
143
|
+
stepId: compDef.id,
|
|
144
|
+
name: compDef.name,
|
|
145
|
+
status: 'completed',
|
|
146
|
+
output: null,
|
|
147
|
+
startedAt: new Date(),
|
|
148
|
+
completedAt: new Date(),
|
|
149
|
+
});
|
|
150
|
+
} catch (compErr: any) {
|
|
151
|
+
run.compensations.push({
|
|
152
|
+
stepId: compDef.id,
|
|
153
|
+
name: compDef.name,
|
|
154
|
+
status: 'failed',
|
|
155
|
+
error: { message: compErr.message || 'Compensation failed' },
|
|
156
|
+
startedAt: new Date(),
|
|
157
|
+
completedAt: new Date(),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
run.updatedAt = new Date();
|
|
162
|
+
await this.saveRun(run);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
run.status = 'completed';
|
|
170
|
+
run.progress = 100;
|
|
171
|
+
run.completedAt = new Date();
|
|
172
|
+
run.updatedAt = new Date();
|
|
173
|
+
await this.saveRun(run);
|
|
174
|
+
return run;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resume a paused or failed workflow run from the last failed/pending step.
|
|
179
|
+
*/
|
|
180
|
+
async resume(runId: string, data?: any): Promise<WorkflowRun> {
|
|
181
|
+
const run = await this.getRun(runId);
|
|
182
|
+
if (!run) throw new AppError(`Workflow run '${runId}' not found`, { code: 'RUN_NOT_FOUND', status: 404 });
|
|
183
|
+
if (run.status === 'completed') throw new AppError('Workflow already completed', { code: 'RUN_COMPLETED', status: 400 });
|
|
184
|
+
|
|
185
|
+
const wf = getWorkflow(run.workflowId, this.#tenant_id);
|
|
186
|
+
if (!wf) throw new AppError(`Workflow '${run.workflowId}' not found`, { code: 'WORKFLOW_NOT_FOUND', status: 404 });
|
|
187
|
+
|
|
188
|
+
run.status = 'running';
|
|
189
|
+
run.data = data || run.data;
|
|
190
|
+
run.updatedAt = new Date();
|
|
191
|
+
|
|
192
|
+
// Find the first non-completed step
|
|
193
|
+
const startIndex = run.steps.findIndex(s => s.status !== 'completed');
|
|
194
|
+
if (startIndex === -1) {
|
|
195
|
+
run.status = 'completed';
|
|
196
|
+
run.progress = 100;
|
|
197
|
+
run.completedAt = new Date();
|
|
198
|
+
await this.saveRun(run);
|
|
199
|
+
return run;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (let i = startIndex; i < wf.steps.length; i++) {
|
|
203
|
+
const stepDef = wf.steps[i]!;
|
|
204
|
+
run.currentStep = i;
|
|
205
|
+
const stepRun: WorkflowRunStep = {
|
|
206
|
+
stepId: stepDef.id,
|
|
207
|
+
name: stepDef.name,
|
|
208
|
+
status: 'running',
|
|
209
|
+
input: run.data,
|
|
210
|
+
startedAt: new Date(),
|
|
211
|
+
};
|
|
212
|
+
run.steps[i] = stepRun;
|
|
213
|
+
run.updatedAt = new Date();
|
|
214
|
+
await this.saveRun(run);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const prevOutput = i > 0 ? run.steps[i - 1]?.output ?? null : null;
|
|
218
|
+
|
|
219
|
+
// Vérifier la condition du step
|
|
220
|
+
if (stepDef.condition) {
|
|
221
|
+
const shouldRun = await stepDef.condition({ data: run.data, prevOutput });
|
|
222
|
+
if (!shouldRun) {
|
|
223
|
+
stepRun.status = 'skipped';
|
|
224
|
+
stepRun.completedAt = new Date();
|
|
225
|
+
run.steps[i] = stepRun;
|
|
226
|
+
run.totalSkipped = run.steps.filter(s => s.status === 'skipped').length;
|
|
227
|
+
run.progress = Math.round(((i + 1) / wf.steps.length) * 100);
|
|
228
|
+
run.updatedAt = new Date();
|
|
229
|
+
await this.saveRun(run);
|
|
230
|
+
continue; // saute ce step
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const result = await stepDef.exec({ data: run.data, prevOutput, input: stepDef.input, rest: this, error: fn.error, jwt: func.jwt });
|
|
235
|
+
stepRun.status = 'completed';
|
|
236
|
+
stepRun.output = result;
|
|
237
|
+
stepRun.completedAt = new Date();
|
|
238
|
+
run.steps[i] = stepRun;
|
|
239
|
+
run.totalExecuted = run.steps.filter(s => s.status === 'completed').length;
|
|
240
|
+
run.progress = Math.round(((i + 1) / wf.steps.length) * 100);
|
|
241
|
+
} catch (err: any) {
|
|
242
|
+
stepRun.status = 'failed';
|
|
243
|
+
stepRun.error = { message: err.message || 'Step failed', code: err.code };
|
|
244
|
+
stepRun.completedAt = new Date();
|
|
245
|
+
run.steps[i] = stepRun;
|
|
246
|
+
run.totalExecuted = run.steps.filter(s => s.status === 'completed').length;
|
|
247
|
+
run.status = 'failed';
|
|
248
|
+
run.error = { message: err.message || 'Workflow failed', code: err.code, stepId: stepDef.id };
|
|
249
|
+
run.updatedAt = new Date();
|
|
250
|
+
await this.saveRun(run);
|
|
251
|
+
|
|
252
|
+
// Exécuter les compensations (steps réussis en ordre inverse)
|
|
253
|
+
if (wf.compensations?.length) {
|
|
254
|
+
const failedIndex = run.steps.findIndex(s => s.status === 'failed');
|
|
255
|
+
const stepsToCompensate = run.steps.slice(0, failedIndex).filter(s => s.status === 'completed').reverse();
|
|
256
|
+
run.compensations = [];
|
|
257
|
+
for (const completedStep of stepsToCompensate) {
|
|
258
|
+
const compDef = wf.compensations?.find(c => c.depend?.includes(completedStep.stepId) || c.id === completedStep.stepId);
|
|
259
|
+
if (!compDef?.exec) continue;
|
|
260
|
+
try {
|
|
261
|
+
await compDef.exec({
|
|
262
|
+
data: run.data,
|
|
263
|
+
prevOutput: completedStep.output,
|
|
264
|
+
input: undefined,
|
|
265
|
+
rest: this,
|
|
266
|
+
error: fn.error,
|
|
267
|
+
jwt: func.jwt,
|
|
268
|
+
});
|
|
269
|
+
run.compensations.push({
|
|
270
|
+
stepId: compDef.id,
|
|
271
|
+
name: compDef.name,
|
|
272
|
+
status: 'completed',
|
|
273
|
+
output: null,
|
|
274
|
+
startedAt: new Date(),
|
|
275
|
+
completedAt: new Date(),
|
|
276
|
+
});
|
|
277
|
+
} catch (compErr: any) {
|
|
278
|
+
run.compensations.push({
|
|
279
|
+
stepId: compDef.id,
|
|
280
|
+
name: compDef.name,
|
|
281
|
+
status: 'failed',
|
|
282
|
+
error: { message: compErr.message || 'Compensation failed' },
|
|
283
|
+
startedAt: new Date(),
|
|
284
|
+
completedAt: new Date(),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
run.updatedAt = new Date();
|
|
289
|
+
await this.saveRun(run);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
run.status = 'completed';
|
|
297
|
+
run.progress = 100;
|
|
298
|
+
run.completedAt = new Date();
|
|
299
|
+
run.updatedAt = new Date();
|
|
300
|
+
await this.saveRun(run);
|
|
301
|
+
return run;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Get a workflow run by ID */
|
|
305
|
+
async getRun(runId: string): Promise<WorkflowRun | null> {
|
|
306
|
+
const col = await this.getCollection();
|
|
307
|
+
const doc = await col.findOne({ _id: runId as any });
|
|
308
|
+
return doc as unknown as WorkflowRun | null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** List all runs for a workflow */
|
|
312
|
+
async listRuns(workflowId: string, limit = 20): Promise<WorkflowRun[]> {
|
|
313
|
+
const col = await this.getCollection();
|
|
314
|
+
const docs = await col.find({ workflowId }).sort({ createdAt: -1 }).limit(limit).toArray();
|
|
315
|
+
return docs as unknown as WorkflowRun[];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Get progress of a run (0-100) */
|
|
319
|
+
async getProgress(runId: string): Promise<{ progress: number; status: WorkflowRunStatus; currentStep: number; totalSteps: number; totalExecuted: number; totalSkipped: number } | null> {
|
|
320
|
+
const run = await this.getRun(runId);
|
|
321
|
+
if (!run) return null;
|
|
322
|
+
return {
|
|
323
|
+
progress: run.progress,
|
|
324
|
+
status: run.status,
|
|
325
|
+
currentStep: run.currentStep,
|
|
326
|
+
totalSteps: run.steps.length,
|
|
327
|
+
totalExecuted: run.totalExecuted,
|
|
328
|
+
totalSkipped: run.totalSkipped,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Pause a running workflow */
|
|
333
|
+
async pause(runId: string): Promise<void> {
|
|
334
|
+
const col = await this.getCollection();
|
|
335
|
+
await col.updateOne(
|
|
336
|
+
{ _id: runId as any, status: 'running' },
|
|
337
|
+
{ $set: { status: 'paused', updatedAt: new Date() } }
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Cancel a workflow run permanently */
|
|
342
|
+
async cancel(runId: string): Promise<void> {
|
|
343
|
+
const col = await this.getCollection();
|
|
344
|
+
await col.updateOne(
|
|
345
|
+
{ _id: runId as any },
|
|
346
|
+
{ $set: { status: 'cancelled', updatedAt: new Date() } }
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Resume all paused or failed workflows */
|
|
351
|
+
async resumeAll(data?: any): Promise<{ resumed: number; failed: number }> {
|
|
352
|
+
const col = await this.getCollection();
|
|
353
|
+
const runs = await col.find({
|
|
354
|
+
status: { $in: ['paused', 'failed'] }
|
|
355
|
+
}).toArray() as unknown as WorkflowRun[];
|
|
356
|
+
|
|
357
|
+
let resumed = 0;
|
|
358
|
+
let failed = 0;
|
|
359
|
+
|
|
360
|
+
for (const run of runs) {
|
|
361
|
+
try {
|
|
362
|
+
await this.resume(run._id, data);
|
|
363
|
+
resumed++;
|
|
364
|
+
} catch {
|
|
365
|
+
failed++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { resumed, failed };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private async saveRun(run: WorkflowRun): Promise<void> {
|
|
373
|
+
const col = await this.getCollection();
|
|
374
|
+
await col.replaceOne(
|
|
375
|
+
{ _id: run._id as any },
|
|
376
|
+
run as any,
|
|
377
|
+
{ upsert: true }
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function createWorkflow(tenant_id: string): Workflow {
|
|
383
|
+
return new Workflow(tenant_id);
|
|
384
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { define } from "./lib/define";
|
|
2
|
+
import { bootApp } from "./server/boot";
|
|
3
|
+
import { useRest } from "./database/rest";
|
|
4
|
+
import * as v from "joi";
|
|
5
|
+
import utils from "./utils";
|
|
6
|
+
import * as crypto from "./utils/crypto";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// Imort bentocache use as cache
|
|
10
|
+
import { useMemoryCache, useFilesystemCache, useRedisCache } from "./utils/cache";
|
|
11
|
+
const cache = {
|
|
12
|
+
useMemoryCache,
|
|
13
|
+
useFilesystemCache,
|
|
14
|
+
useRedisCache
|
|
15
|
+
}
|
|
16
|
+
const app = {
|
|
17
|
+
boot: bootApp
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
define,
|
|
22
|
+
app,
|
|
23
|
+
useRest,
|
|
24
|
+
v,
|
|
25
|
+
utils,
|
|
26
|
+
cache,
|
|
27
|
+
crypto,
|
|
28
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
+
|
|
3
|
+
const asyncContextStorage = new AsyncLocalStorage<Map<string, unknown>>();
|
|
4
|
+
|
|
5
|
+
const REQUEST_STORAGE_PREFIX = ":::requestStorage:::";
|
|
6
|
+
const SESSION_STORAGE_PREFIX = "::sessionStorage::";
|
|
7
|
+
|
|
8
|
+
const requestCtxStorage = {
|
|
9
|
+
set(key: string, value: unknown): void {
|
|
10
|
+
asyncContextStorage.getStore()?.set(`${REQUEST_STORAGE_PREFIX}${key}`, value);
|
|
11
|
+
},
|
|
12
|
+
get<T = unknown>(key: string): T | undefined {
|
|
13
|
+
return asyncContextStorage.getStore()?.get(`${REQUEST_STORAGE_PREFIX}${key}`) as T | undefined;
|
|
14
|
+
},
|
|
15
|
+
delete(key: string): void {
|
|
16
|
+
asyncContextStorage.getStore()?.delete(`${REQUEST_STORAGE_PREFIX}${key}`);
|
|
17
|
+
},
|
|
18
|
+
clear(): void {
|
|
19
|
+
asyncContextStorage.getStore()?.clear();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface SessionSetParams {
|
|
24
|
+
state: {
|
|
25
|
+
user: Record<string, unknown>;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
role: string;
|
|
29
|
+
token?: string;
|
|
30
|
+
expireIn?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SessionData {
|
|
34
|
+
state: SessionSetParams["state"];
|
|
35
|
+
uuid: string;
|
|
36
|
+
role: string;
|
|
37
|
+
token?: string;
|
|
38
|
+
expireIn?: number;
|
|
39
|
+
isAuth: boolean;
|
|
40
|
+
_v: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sessionCtxStorage = {
|
|
44
|
+
set(params: SessionSetParams): void {
|
|
45
|
+
const store = asyncContextStorage.getStore();
|
|
46
|
+
if (!store) return;
|
|
47
|
+
const data: SessionData = {
|
|
48
|
+
state: params.state,
|
|
49
|
+
uuid: crypto.randomUUID(),
|
|
50
|
+
role: params.role,
|
|
51
|
+
token: params.token,
|
|
52
|
+
expireIn: params.expireIn,
|
|
53
|
+
isAuth: !!params.state?.user && !!params.token,
|
|
54
|
+
_v: undefined,
|
|
55
|
+
};
|
|
56
|
+
store.set(SESSION_STORAGE_PREFIX, data);
|
|
57
|
+
},
|
|
58
|
+
get(): SessionData | undefined {
|
|
59
|
+
return asyncContextStorage.getStore()?.get(SESSION_STORAGE_PREFIX) as SessionData | undefined;
|
|
60
|
+
},
|
|
61
|
+
clear(): void {
|
|
62
|
+
asyncContextStorage.getStore()?.delete(SESSION_STORAGE_PREFIX);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export { asyncContextStorage, requestCtxStorage, sessionCtxStorage };
|
|
68
|
+
|