@tachybase/module-event-source 1.6.15 → 1.6.17
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/dist/client/custom-event-sources/CustomEventSourcePane.schema.d.ts +3 -0
- package/dist/client/custom-event-sources/collectionCustomEventSources.d.ts +3 -0
- package/dist/client/index.js +3 -3
- package/dist/client/triggers/CustomActionTrigger.d.ts +62 -0
- package/dist/client/triggers/ResourceEventTrigger.d.ts +62 -0
- package/dist/client/triggers/resourceTriggerOptions.d.ts +82 -0
- package/dist/externalVersion.js +2 -2
- package/dist/locale/en-US.json +10 -0
- package/dist/locale/zh-CN.json +10 -0
- package/dist/server/collections/eventSourceQueueJobs.d.ts +2 -0
- package/dist/server/collections/eventSourceQueueJobs.js +48 -0
- package/dist/server/constants.d.ts +8 -0
- package/dist/server/constants.js +12 -0
- package/dist/server/model/EventSourceModel.d.ts +6 -0
- package/dist/server/queue/EventSourceQueueWorker.d.ts +31 -0
- package/dist/server/queue/EventSourceQueueWorker.js +168 -0
- package/dist/server/trigger/CustomctionTrigger.d.ts +8 -0
- package/dist/server/trigger/CustomctionTrigger.js +99 -9
- package/dist/server/trigger/ResourceEventTrigger.d.ts +14 -0
- package/dist/server/trigger/ResourceEventTrigger.js +119 -7
- package/dist/server/webhooks/Plugin.d.ts +2 -0
- package/dist/server/webhooks/Plugin.js +8 -0
- package/dist/server/webhooks/webhooks.js +8 -4
- package/package.json +3 -3
package/dist/server/constants.js
CHANGED
|
@@ -18,13 +18,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
18
18
|
var constants_exports = {};
|
|
19
19
|
__export(constants_exports, {
|
|
20
20
|
EVENT_SOURCE_COLLECTION: () => EVENT_SOURCE_COLLECTION,
|
|
21
|
+
EVENT_SOURCE_QUEUE_COLLECTION: () => EVENT_SOURCE_QUEUE_COLLECTION,
|
|
22
|
+
EVENT_SOURCE_QUEUE_STATUS: () => EVENT_SOURCE_QUEUE_STATUS,
|
|
21
23
|
EVENT_SOURCE_REALTIME: () => EVENT_SOURCE_REALTIME
|
|
22
24
|
});
|
|
23
25
|
module.exports = __toCommonJS(constants_exports);
|
|
24
26
|
const EVENT_SOURCE_COLLECTION = "webhooks";
|
|
25
27
|
const EVENT_SOURCE_REALTIME = process.env.EVENT_SOURCE_REALTIME === "0" ? false : true;
|
|
28
|
+
const EVENT_SOURCE_QUEUE_COLLECTION = "eventSourceQueueJobs";
|
|
29
|
+
const EVENT_SOURCE_QUEUE_STATUS = {
|
|
30
|
+
PENDING: "pending",
|
|
31
|
+
PROCESSING: "processing",
|
|
32
|
+
SUCCESS: "success",
|
|
33
|
+
FAILED: "failed",
|
|
34
|
+
DEAD: "dead"
|
|
35
|
+
};
|
|
26
36
|
// Annotate the CommonJS export names for ESM import in node:
|
|
27
37
|
0 && (module.exports = {
|
|
28
38
|
EVENT_SOURCE_COLLECTION,
|
|
39
|
+
EVENT_SOURCE_QUEUE_COLLECTION,
|
|
40
|
+
EVENT_SOURCE_QUEUE_STATUS,
|
|
29
41
|
EVENT_SOURCE_REALTIME
|
|
30
42
|
});
|
|
@@ -11,6 +11,12 @@ export declare class EventSourceModel extends Model {
|
|
|
11
11
|
actionName?: string;
|
|
12
12
|
eventName?: string;
|
|
13
13
|
triggerOnAssociation?: boolean;
|
|
14
|
+
useHttpContext?: boolean;
|
|
15
|
+
failurePolicy?: 'ignore' | 'block';
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
executionMode?: 'inline' | 'queue';
|
|
18
|
+
maxAttempts?: number;
|
|
19
|
+
retryBackoffMs?: number;
|
|
14
20
|
sort?: number;
|
|
15
21
|
};
|
|
16
22
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Application } from '@tego/server';
|
|
2
|
+
type QueueStage = 'beforeResource' | 'afterResource' | 'customAction';
|
|
3
|
+
type QueueJobInput = {
|
|
4
|
+
sourceId: number;
|
|
5
|
+
stage: QueueStage;
|
|
6
|
+
resourceName?: string;
|
|
7
|
+
actionName?: string;
|
|
8
|
+
workflowKey?: string;
|
|
9
|
+
payload: any;
|
|
10
|
+
contextLite?: {
|
|
11
|
+
userId?: number;
|
|
12
|
+
roleName?: string;
|
|
13
|
+
};
|
|
14
|
+
maxAttempts?: number;
|
|
15
|
+
retryBackoffMs?: number;
|
|
16
|
+
};
|
|
17
|
+
export declare class EventSourceQueueWorker {
|
|
18
|
+
private app;
|
|
19
|
+
private timer;
|
|
20
|
+
private running;
|
|
21
|
+
private readonly pollMs;
|
|
22
|
+
private readonly batchSize;
|
|
23
|
+
constructor(app: Application);
|
|
24
|
+
enqueue(input: QueueJobInput): Promise<any>;
|
|
25
|
+
start(): void;
|
|
26
|
+
stop(): void;
|
|
27
|
+
private tick;
|
|
28
|
+
private processJob;
|
|
29
|
+
private createPseudoContext;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
var EventSourceQueueWorker_exports = {};
|
|
19
|
+
__export(EventSourceQueueWorker_exports, {
|
|
20
|
+
EventSourceQueueWorker: () => EventSourceQueueWorker
|
|
21
|
+
});
|
|
22
|
+
module.exports = __toCommonJS(EventSourceQueueWorker_exports);
|
|
23
|
+
var import_constants = require("../constants");
|
|
24
|
+
var import_webhooks = require("../webhooks/webhooks");
|
|
25
|
+
class EventSourceQueueWorker {
|
|
26
|
+
constructor(app) {
|
|
27
|
+
this.app = app;
|
|
28
|
+
this.timer = null;
|
|
29
|
+
this.running = false;
|
|
30
|
+
this.pollMs = Number(process.env.EVENT_SOURCE_QUEUE_POLL_MS || 1e3);
|
|
31
|
+
this.batchSize = Number(process.env.EVENT_SOURCE_QUEUE_BATCH_SIZE || 20);
|
|
32
|
+
}
|
|
33
|
+
async enqueue(input) {
|
|
34
|
+
const repo = this.app.db.getRepository(import_constants.EVENT_SOURCE_QUEUE_COLLECTION);
|
|
35
|
+
return repo.create({
|
|
36
|
+
values: {
|
|
37
|
+
sourceId: input.sourceId,
|
|
38
|
+
stage: input.stage,
|
|
39
|
+
resourceName: input.resourceName,
|
|
40
|
+
actionName: input.actionName,
|
|
41
|
+
workflowKey: input.workflowKey,
|
|
42
|
+
payload: input.payload,
|
|
43
|
+
contextLite: input.contextLite || {},
|
|
44
|
+
status: import_constants.EVENT_SOURCE_QUEUE_STATUS.PENDING,
|
|
45
|
+
attempt: 0,
|
|
46
|
+
maxAttempts: input.maxAttempts ?? 3,
|
|
47
|
+
retryBackoffMs: input.retryBackoffMs ?? 3e3,
|
|
48
|
+
nextRunAt: /* @__PURE__ */ new Date()
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
start() {
|
|
53
|
+
if (this.timer) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.timer = setInterval(() => {
|
|
57
|
+
this.tick().catch((error) => {
|
|
58
|
+
this.app.logger.error("[event-source-queue] tick failed", error);
|
|
59
|
+
});
|
|
60
|
+
}, this.pollMs);
|
|
61
|
+
}
|
|
62
|
+
stop() {
|
|
63
|
+
if (!this.timer) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
clearInterval(this.timer);
|
|
67
|
+
this.timer = null;
|
|
68
|
+
}
|
|
69
|
+
async tick() {
|
|
70
|
+
if (this.running) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.running = true;
|
|
74
|
+
try {
|
|
75
|
+
const repo = this.app.db.getRepository(import_constants.EVENT_SOURCE_QUEUE_COLLECTION);
|
|
76
|
+
const jobs = await repo.find({
|
|
77
|
+
filter: {
|
|
78
|
+
status: {
|
|
79
|
+
$in: [import_constants.EVENT_SOURCE_QUEUE_STATUS.PENDING, import_constants.EVENT_SOURCE_QUEUE_STATUS.FAILED]
|
|
80
|
+
},
|
|
81
|
+
nextRunAt: {
|
|
82
|
+
$lte: /* @__PURE__ */ new Date()
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
sort: ["createdAt"],
|
|
86
|
+
limit: this.batchSize
|
|
87
|
+
});
|
|
88
|
+
for (const job of jobs) {
|
|
89
|
+
await this.processJob(job);
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
this.running = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async processJob(job) {
|
|
96
|
+
const repo = this.app.db.getRepository(import_constants.EVENT_SOURCE_QUEUE_COLLECTION);
|
|
97
|
+
const attempt = (job.attempt || 0) + 1;
|
|
98
|
+
try {
|
|
99
|
+
await repo.update({
|
|
100
|
+
filter: { id: job.id },
|
|
101
|
+
values: {
|
|
102
|
+
status: import_constants.EVENT_SOURCE_QUEUE_STATUS.PROCESSING,
|
|
103
|
+
lockedAt: /* @__PURE__ */ new Date(),
|
|
104
|
+
lockedBy: this.app.name,
|
|
105
|
+
attempt
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const pseudoCtx = await this.createPseudoContext(job.contextLite);
|
|
109
|
+
const controller = new import_webhooks.WebhookController();
|
|
110
|
+
await controller.triggerWorkflow(
|
|
111
|
+
pseudoCtx,
|
|
112
|
+
{
|
|
113
|
+
workflowKey: job.workflowKey,
|
|
114
|
+
options: {
|
|
115
|
+
useHttpContext: false
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
job.payload
|
|
119
|
+
);
|
|
120
|
+
await repo.update({
|
|
121
|
+
filter: { id: job.id },
|
|
122
|
+
values: {
|
|
123
|
+
status: import_constants.EVENT_SOURCE_QUEUE_STATUS.SUCCESS,
|
|
124
|
+
lastError: null
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const maxAttempts = Number(job.maxAttempts || 3);
|
|
129
|
+
const retryBackoffMs = Number(job.retryBackoffMs || 3e3);
|
|
130
|
+
const dead = attempt >= maxAttempts;
|
|
131
|
+
await repo.update({
|
|
132
|
+
filter: { id: job.id },
|
|
133
|
+
values: {
|
|
134
|
+
status: dead ? import_constants.EVENT_SOURCE_QUEUE_STATUS.DEAD : import_constants.EVENT_SOURCE_QUEUE_STATUS.FAILED,
|
|
135
|
+
nextRunAt: dead ? null : new Date(Date.now() + retryBackoffMs),
|
|
136
|
+
lastError: (error == null ? void 0 : error.stack) || `${error}`
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
this.app.logger.error(
|
|
140
|
+
`[event-source-queue] process failed, jobId=${job.id}, sourceId=${job.sourceId}, attempt=${attempt}, dead=${dead}`,
|
|
141
|
+
error
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async createPseudoContext(contextLite) {
|
|
146
|
+
var _a;
|
|
147
|
+
let currentUser = null;
|
|
148
|
+
if (contextLite == null ? void 0 : contextLite.userId) {
|
|
149
|
+
const userRepo = this.app.db.getRepository("users");
|
|
150
|
+
currentUser = await userRepo.findOne({
|
|
151
|
+
filter: { id: contextLite.userId }
|
|
152
|
+
});
|
|
153
|
+
currentUser = ((_a = currentUser == null ? void 0 : currentUser.toJSON) == null ? void 0 : _a.call(currentUser)) || currentUser;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
db: this.app.db,
|
|
157
|
+
tego: this.app,
|
|
158
|
+
state: {
|
|
159
|
+
currentUser,
|
|
160
|
+
currentRole: (contextLite == null ? void 0 : contextLite.roleName) || null
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
166
|
+
0 && (module.exports = {
|
|
167
|
+
EventSourceQueueWorker
|
|
168
|
+
});
|
|
@@ -3,9 +3,17 @@ import { EventSourceTrigger } from './Trigger';
|
|
|
3
3
|
type IAPITriggerConfig = {
|
|
4
4
|
code: string;
|
|
5
5
|
workflowKey: string;
|
|
6
|
+
options?: EventSourceModel['options'];
|
|
6
7
|
};
|
|
7
8
|
export declare class CustomActionTrigger extends EventSourceTrigger {
|
|
8
9
|
eventMap: Map<number, IAPITriggerConfig>;
|
|
10
|
+
/** 未设置 failurePolicy 时与 main 一致:工作流失败则 500 */
|
|
11
|
+
private getFailurePolicy;
|
|
12
|
+
private getExecutionMode;
|
|
13
|
+
private getMaxAttempts;
|
|
14
|
+
private getRetryBackoffMs;
|
|
15
|
+
private getTimeoutMs;
|
|
16
|
+
private withTimeout;
|
|
9
17
|
load(model: EventSourceModel): void;
|
|
10
18
|
afterCreate(model: EventSourceModel): void;
|
|
11
19
|
afterUpdate(model: EventSourceModel): void;
|
|
@@ -27,6 +27,49 @@ class CustomActionTrigger extends import_Trigger.EventSourceTrigger {
|
|
|
27
27
|
super(...arguments);
|
|
28
28
|
this.eventMap = /* @__PURE__ */ new Map();
|
|
29
29
|
}
|
|
30
|
+
/** 未设置 failurePolicy 时与 main 一致:工作流失败则 500 */
|
|
31
|
+
getFailurePolicy(config) {
|
|
32
|
+
var _a;
|
|
33
|
+
return ((_a = config == null ? void 0 : config.options) == null ? void 0 : _a.failurePolicy) === "ignore" ? "ignore" : "block";
|
|
34
|
+
}
|
|
35
|
+
getExecutionMode(config) {
|
|
36
|
+
var _a;
|
|
37
|
+
return ((_a = config == null ? void 0 : config.options) == null ? void 0 : _a.executionMode) === "queue" ? "queue" : "inline";
|
|
38
|
+
}
|
|
39
|
+
getMaxAttempts(config) {
|
|
40
|
+
var _a;
|
|
41
|
+
const value = Number(((_a = config == null ? void 0 : config.options) == null ? void 0 : _a.maxAttempts) || 3);
|
|
42
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 3;
|
|
43
|
+
}
|
|
44
|
+
getRetryBackoffMs(config) {
|
|
45
|
+
var _a;
|
|
46
|
+
const value = Number(((_a = config == null ? void 0 : config.options) == null ? void 0 : _a.retryBackoffMs) || 3e3);
|
|
47
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 3e3;
|
|
48
|
+
}
|
|
49
|
+
getTimeoutMs(config) {
|
|
50
|
+
var _a;
|
|
51
|
+
const timeoutMs = Number(((_a = config == null ? void 0 : config.options) == null ? void 0 : _a.timeoutMs) || 0);
|
|
52
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
return timeoutMs;
|
|
56
|
+
}
|
|
57
|
+
async withTimeout(executor, timeoutMs, errorMessage) {
|
|
58
|
+
if (!timeoutMs) {
|
|
59
|
+
return executor();
|
|
60
|
+
}
|
|
61
|
+
let timer;
|
|
62
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
63
|
+
timer = setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
return await Promise.race([executor(), timeoutPromise]);
|
|
67
|
+
} finally {
|
|
68
|
+
if (timer) {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
30
73
|
load(model) {
|
|
31
74
|
if (!model.options) {
|
|
32
75
|
return;
|
|
@@ -38,7 +81,7 @@ class CustomActionTrigger extends import_Trigger.EventSourceTrigger {
|
|
|
38
81
|
code,
|
|
39
82
|
workflowKey
|
|
40
83
|
} = model;
|
|
41
|
-
this.eventMap.set(id, { code, workflowKey });
|
|
84
|
+
this.eventMap.set(id, { code, workflowKey, options: model.options });
|
|
42
85
|
if (!app.resourcer.isDefined(resourceName)) {
|
|
43
86
|
app.resourcer.define({ name: resourceName });
|
|
44
87
|
}
|
|
@@ -48,15 +91,62 @@ class CustomActionTrigger extends import_Trigger.EventSourceTrigger {
|
|
|
48
91
|
}
|
|
49
92
|
app.logger.info(`Add ${resourceName}:${actionName} action handler`);
|
|
50
93
|
app.resourcer.getResource(resourceName).addAction(actionName, async (ctx) => {
|
|
94
|
+
var _a, _b, _c, _d, _e;
|
|
51
95
|
if (this.realTimeRefresh && !this.workSet.has(id)) {
|
|
52
96
|
ctx.throw(404, "Not found");
|
|
53
97
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
98
|
+
const config = this.eventMap.get(id);
|
|
99
|
+
if (!config) {
|
|
100
|
+
ctx.throw(404, "Not found");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const failurePolicy = this.getFailurePolicy(config);
|
|
104
|
+
const executionMode = this.getExecutionMode(config);
|
|
105
|
+
const timeoutMs = this.getTimeoutMs(config);
|
|
106
|
+
try {
|
|
107
|
+
if (executionMode === "queue") {
|
|
108
|
+
const body = await new import_webhooks.WebhookController().action(ctx, { code: config.code });
|
|
109
|
+
const queueWorker = this.app.eventSourceQueueWorker;
|
|
110
|
+
if (!queueWorker) {
|
|
111
|
+
throw new Error("eventSourceQueueWorker is not available");
|
|
112
|
+
}
|
|
113
|
+
await queueWorker.enqueue({
|
|
114
|
+
sourceId: id,
|
|
115
|
+
stage: "customAction",
|
|
116
|
+
resourceName: (_a = ctx == null ? void 0 : ctx.action) == null ? void 0 : _a.resourceName,
|
|
117
|
+
actionName: (_b = ctx == null ? void 0 : ctx.action) == null ? void 0 : _b.actionName,
|
|
118
|
+
workflowKey: config.workflowKey,
|
|
119
|
+
payload: body,
|
|
120
|
+
contextLite: {
|
|
121
|
+
userId: (_d = (_c = ctx == null ? void 0 : ctx.state) == null ? void 0 : _c.currentUser) == null ? void 0 : _d.id,
|
|
122
|
+
roleName: (_e = ctx == null ? void 0 : ctx.state) == null ? void 0 : _e.currentRole
|
|
123
|
+
},
|
|
124
|
+
maxAttempts: this.getMaxAttempts(config),
|
|
125
|
+
retryBackoffMs: this.getRetryBackoffMs(config)
|
|
126
|
+
});
|
|
127
|
+
ctx.withoutDataWrapping = true;
|
|
128
|
+
ctx.body = { queued: true };
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const res = await this.withTimeout(
|
|
132
|
+
async () => {
|
|
133
|
+
const body = await new import_webhooks.WebhookController().action(ctx, { code: config.code });
|
|
134
|
+
return await new import_webhooks.WebhookController().triggerWorkflow(ctx, config, body);
|
|
135
|
+
},
|
|
136
|
+
timeoutMs,
|
|
137
|
+
`[event-source] custom action timeout, sourceId=${id}, timeoutMs=${timeoutMs}`
|
|
138
|
+
);
|
|
139
|
+
const lastSavedJob = res == null ? void 0 : res.lastSavedJob;
|
|
140
|
+
if ((lastSavedJob == null ? void 0 : lastSavedJob.get("status")) < 0) {
|
|
141
|
+
throw new Error(`${lastSavedJob.get("result")}`);
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
app.logger.error(
|
|
145
|
+
`[event-source] custom action execution failed. policy=${failurePolicy}, sourceId=${id}, workflowKey=${config == null ? void 0 : config.workflowKey}, error=${(error == null ? void 0 : error.stack) || error}`
|
|
146
|
+
);
|
|
147
|
+
if (failurePolicy === "block") {
|
|
148
|
+
ctx.throw(500, (error == null ? void 0 : error.message) || "Custom action execution failed");
|
|
149
|
+
}
|
|
60
150
|
}
|
|
61
151
|
});
|
|
62
152
|
app.acl.allow(resourceName, actionName, "loggedIn");
|
|
@@ -66,11 +156,11 @@ class CustomActionTrigger extends import_Trigger.EventSourceTrigger {
|
|
|
66
156
|
}
|
|
67
157
|
// TODO 很难修改和删除,目前实时刷新的时候间接修改
|
|
68
158
|
afterUpdate(model) {
|
|
69
|
-
const { enabled, code, workflowKey, id } = model;
|
|
159
|
+
const { enabled, code, workflowKey, id, options } = model;
|
|
70
160
|
if (enabled && !this.workSet.has(id)) {
|
|
71
161
|
this.load(model);
|
|
72
162
|
} else {
|
|
73
|
-
this.eventMap.set(id, { code, workflowKey });
|
|
163
|
+
this.eventMap.set(id, { code, workflowKey, options });
|
|
74
164
|
}
|
|
75
165
|
}
|
|
76
166
|
}
|
|
@@ -5,6 +5,20 @@ export declare class ResourceEventTrigger extends EventSourceTrigger {
|
|
|
5
5
|
private afterList;
|
|
6
6
|
load(model: EventSourceModel): void;
|
|
7
7
|
afterAllLoad(): void;
|
|
8
|
+
/**
|
|
9
|
+
* failurePolicy 未设置:与 main 分支一致(遗留行为)
|
|
10
|
+
* - beforeResource:工作流 ERROR 时阻断(原 400)
|
|
11
|
+
* - afterResource:不因工作流 ERROR 状态阻断(原逻辑未校验)
|
|
12
|
+
* 显式 ignore:失败不阻断主请求
|
|
13
|
+
* 显式 block:任意失败均阻断
|
|
14
|
+
*/
|
|
15
|
+
private getFailurePolicy;
|
|
16
|
+
private getExecutionMode;
|
|
17
|
+
private getMaxAttempts;
|
|
18
|
+
private getRetryBackoffMs;
|
|
19
|
+
private getTimeoutMs;
|
|
20
|
+
private withTimeout;
|
|
21
|
+
private executeByPolicy;
|
|
8
22
|
private getMatchList;
|
|
9
23
|
afterCreate(model: EventSourceModel): void;
|
|
10
24
|
afterUpdate(model: EventSourceModel): void;
|
|
@@ -55,21 +55,133 @@ class ResourceEventTrigger extends import_Trigger.EventSourceTrigger {
|
|
|
55
55
|
return next();
|
|
56
56
|
}
|
|
57
57
|
for (const model of matchBefore) {
|
|
58
|
-
|
|
59
|
-
const result = await new import_webhooks.WebhookController().triggerWorkflow(ctx, model, body);
|
|
60
|
-
if (result && result.lastSavedJob.status === import_module_workflow.JOB_STATUS.ERROR) {
|
|
61
|
-
ctx.throw(400, result.lastSavedJob.result);
|
|
62
|
-
}
|
|
58
|
+
await this.executeByPolicy(ctx, model, "beforeResource");
|
|
63
59
|
}
|
|
64
60
|
await next();
|
|
65
61
|
for (const model of matchAfter) {
|
|
66
|
-
|
|
67
|
-
await new import_webhooks.WebhookController().triggerWorkflow(ctx, model, body);
|
|
62
|
+
await this.executeByPolicy(ctx, model, "afterResource");
|
|
68
63
|
}
|
|
69
64
|
},
|
|
70
65
|
{ tag: "event-source-resource" }
|
|
71
66
|
);
|
|
72
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* failurePolicy 未设置:与 main 分支一致(遗留行为)
|
|
70
|
+
* - beforeResource:工作流 ERROR 时阻断(原 400)
|
|
71
|
+
* - afterResource:不因工作流 ERROR 状态阻断(原逻辑未校验)
|
|
72
|
+
* 显式 ignore:失败不阻断主请求
|
|
73
|
+
* 显式 block:任意失败均阻断
|
|
74
|
+
*/
|
|
75
|
+
getFailurePolicy(model) {
|
|
76
|
+
var _a;
|
|
77
|
+
const fp = (_a = model == null ? void 0 : model.options) == null ? void 0 : _a.failurePolicy;
|
|
78
|
+
if (fp === "ignore") {
|
|
79
|
+
return "ignore";
|
|
80
|
+
}
|
|
81
|
+
if (fp === "block") {
|
|
82
|
+
return "block";
|
|
83
|
+
}
|
|
84
|
+
return "legacy";
|
|
85
|
+
}
|
|
86
|
+
getExecutionMode(model) {
|
|
87
|
+
var _a;
|
|
88
|
+
return ((_a = model == null ? void 0 : model.options) == null ? void 0 : _a.executionMode) === "queue" ? "queue" : "inline";
|
|
89
|
+
}
|
|
90
|
+
getMaxAttempts(model) {
|
|
91
|
+
var _a;
|
|
92
|
+
const value = Number(((_a = model == null ? void 0 : model.options) == null ? void 0 : _a.maxAttempts) || 3);
|
|
93
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 3;
|
|
94
|
+
}
|
|
95
|
+
getRetryBackoffMs(model) {
|
|
96
|
+
var _a;
|
|
97
|
+
const value = Number(((_a = model == null ? void 0 : model.options) == null ? void 0 : _a.retryBackoffMs) || 3e3);
|
|
98
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 3e3;
|
|
99
|
+
}
|
|
100
|
+
getTimeoutMs(model) {
|
|
101
|
+
var _a;
|
|
102
|
+
const timeoutMs = Number(((_a = model == null ? void 0 : model.options) == null ? void 0 : _a.timeoutMs) || 0);
|
|
103
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
return timeoutMs;
|
|
107
|
+
}
|
|
108
|
+
async withTimeout(executor, timeoutMs, errorMessage) {
|
|
109
|
+
if (!timeoutMs) {
|
|
110
|
+
return executor();
|
|
111
|
+
}
|
|
112
|
+
let timer;
|
|
113
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
114
|
+
timer = setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
return await Promise.race([executor(), timeoutPromise]);
|
|
118
|
+
} finally {
|
|
119
|
+
if (timer) {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async executeByPolicy(ctx, model, stage) {
|
|
125
|
+
var _a, _b, _c, _d, _e;
|
|
126
|
+
const failurePolicy = this.getFailurePolicy(model);
|
|
127
|
+
const executionMode = this.getExecutionMode(model);
|
|
128
|
+
const timeoutMs = this.getTimeoutMs(model);
|
|
129
|
+
try {
|
|
130
|
+
if (executionMode === "queue") {
|
|
131
|
+
const body = await new import_webhooks.WebhookController().action(ctx, model);
|
|
132
|
+
const queueWorker = this.app.eventSourceQueueWorker;
|
|
133
|
+
if (!queueWorker) {
|
|
134
|
+
throw new Error("eventSourceQueueWorker is not available");
|
|
135
|
+
}
|
|
136
|
+
await queueWorker.enqueue({
|
|
137
|
+
sourceId: model.id,
|
|
138
|
+
stage,
|
|
139
|
+
resourceName: (_a = model == null ? void 0 : model.options) == null ? void 0 : _a.resourceName,
|
|
140
|
+
actionName: (_b = model == null ? void 0 : model.options) == null ? void 0 : _b.actionName,
|
|
141
|
+
workflowKey: model.workflowKey,
|
|
142
|
+
payload: body,
|
|
143
|
+
contextLite: {
|
|
144
|
+
userId: (_d = (_c = ctx == null ? void 0 : ctx.state) == null ? void 0 : _c.currentUser) == null ? void 0 : _d.id,
|
|
145
|
+
roleName: (_e = ctx == null ? void 0 : ctx.state) == null ? void 0 : _e.currentRole
|
|
146
|
+
},
|
|
147
|
+
maxAttempts: this.getMaxAttempts(model),
|
|
148
|
+
retryBackoffMs: this.getRetryBackoffMs(model)
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const result = await this.withTimeout(
|
|
153
|
+
async () => {
|
|
154
|
+
const body = await new import_webhooks.WebhookController().action(ctx, model);
|
|
155
|
+
return await new import_webhooks.WebhookController().triggerWorkflow(ctx, model, body);
|
|
156
|
+
},
|
|
157
|
+
timeoutMs,
|
|
158
|
+
`[event-source] ${stage} timeout, sourceId=${model.id}, timeoutMs=${timeoutMs}`
|
|
159
|
+
);
|
|
160
|
+
if (result && result.lastSavedJob.status === import_module_workflow.JOB_STATUS.ERROR) {
|
|
161
|
+
if (failurePolicy === "ignore") {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (failurePolicy === "block") {
|
|
165
|
+
throw new Error(`${result.lastSavedJob.result}`);
|
|
166
|
+
}
|
|
167
|
+
if (stage === "beforeResource") {
|
|
168
|
+
ctx.throw(400, result.lastSavedJob.result);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
this.app.logger.error(
|
|
174
|
+
`[event-source] ${stage} execution failed. policy=${failurePolicy}, sourceId=${model.id}, workflowKey=${model.workflowKey}, error=${(error == null ? void 0 : error.stack) || error}`
|
|
175
|
+
);
|
|
176
|
+
if (failurePolicy === "ignore") {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (failurePolicy === "block") {
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
73
185
|
getMatchList(list, resourceName, actionName) {
|
|
74
186
|
const matchList = [];
|
|
75
187
|
list.sort((a, b) => {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Plugin, Registry, WSServer } from '@tego/server';
|
|
2
|
+
import { EventSourceQueueWorker } from '../queue/EventSourceQueueWorker';
|
|
2
3
|
import { EventSourceTrigger } from '../trigger/Trigger';
|
|
3
4
|
export declare class PluginWebhook extends Plugin {
|
|
4
5
|
triggers: Registry<EventSourceTrigger>;
|
|
5
6
|
ws: WSServer;
|
|
7
|
+
queueWorker: EventSourceQueueWorker;
|
|
6
8
|
refreshRealTime: boolean;
|
|
7
9
|
changed: boolean;
|
|
8
10
|
beforeLoad(): Promise<void>;
|
|
@@ -24,6 +24,7 @@ var import_server = require("@tego/server");
|
|
|
24
24
|
var import_constants = require("../constants");
|
|
25
25
|
var import_EventSourceModel = require("../model/EventSourceModel");
|
|
26
26
|
var import_WebhookCategories = require("../model/WebhookCategories");
|
|
27
|
+
var import_EventSourceQueueWorker = require("../queue/EventSourceQueueWorker");
|
|
27
28
|
var import_AppEventTrigger = require("../trigger/AppEventTrigger");
|
|
28
29
|
var import_CustomctionTrigger = require("../trigger/CustomctionTrigger");
|
|
29
30
|
var import_DatabaseEventTrigger = require("../trigger/DatabaseEventTrigger");
|
|
@@ -80,6 +81,13 @@ class PluginWebhook extends import_server.Plugin {
|
|
|
80
81
|
},
|
|
81
82
|
{ tag: "webhooks-show-effect" }
|
|
82
83
|
);
|
|
84
|
+
this.queueWorker = new import_EventSourceQueueWorker.EventSourceQueueWorker(this.app);
|
|
85
|
+
this.app.eventSourceQueueWorker = this.queueWorker;
|
|
86
|
+
this.queueWorker.start();
|
|
87
|
+
this.app.on("beforeStop", async () => {
|
|
88
|
+
var _a;
|
|
89
|
+
(_a = this.queueWorker) == null ? void 0 : _a.stop();
|
|
90
|
+
});
|
|
83
91
|
this.triggers.register("resource", new import_CustomctionTrigger.CustomActionTrigger(this.app, this.refreshRealTime));
|
|
84
92
|
this.triggers.register("beforeResource", new import_ResourceEventTrigger.ResourceEventTrigger(this.app, this.refreshRealTime));
|
|
85
93
|
this.triggers.register("afterResource", new import_ResourceEventTrigger.ResourceEventTrigger(this.app, this.refreshRealTime));
|
|
@@ -253,19 +253,23 @@ class WebhookController {
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
async triggerWorkflow(ctx, action, body) {
|
|
256
|
-
|
|
257
|
-
const {
|
|
256
|
+
var _a;
|
|
257
|
+
const { currentUser, currentRole } = (ctx == null ? void 0 : ctx.state) || {};
|
|
258
|
+
const userCollection = ctx.db.getCollection("users");
|
|
259
|
+
const { model: UserModel } = userCollection;
|
|
258
260
|
if (!action.workflowKey) {
|
|
259
261
|
return;
|
|
260
262
|
}
|
|
261
263
|
const userInfo = {
|
|
262
|
-
user: UserModel.build(currentUser).desensitize(),
|
|
264
|
+
user: currentUser ? UserModel.build(currentUser).desensitize() : null,
|
|
263
265
|
roleName: currentRole
|
|
264
266
|
};
|
|
265
267
|
const pluginWorkflow = ctx.tego.getPlugin(import_module_workflow.PluginWorkflow);
|
|
266
268
|
const wfRepo = ctx.db.getRepository("workflows");
|
|
267
269
|
const wf = await wfRepo.findOne({ filter: { key: action.workflowKey, enabled: true } });
|
|
268
|
-
|
|
270
|
+
const useHttpContext = ((_a = action == null ? void 0 : action.options) == null ? void 0 : _a.useHttpContext) !== false;
|
|
271
|
+
const triggerOptions = useHttpContext ? { httpContext: ctx } : void 0;
|
|
272
|
+
return await pluginWorkflow.trigger(wf, { data: body, ...userInfo }, triggerOptions);
|
|
269
273
|
}
|
|
270
274
|
}
|
|
271
275
|
// Annotate the CommonJS export names for ESM import in node:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tachybase/module-event-source",
|
|
3
3
|
"displayName": "Event Source",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.17",
|
|
5
5
|
"description": "Registering and managing custom event sources.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"System management"
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"dayjs": "1.11.13",
|
|
30
30
|
"lodash": "4.17.21",
|
|
31
31
|
"sequelize": "6.37.5",
|
|
32
|
-
"@tachybase/client": "1.6.
|
|
33
|
-
"@tachybase/module-workflow": "1.6.
|
|
32
|
+
"@tachybase/client": "1.6.17",
|
|
33
|
+
"@tachybase/module-workflow": "1.6.17"
|
|
34
34
|
},
|
|
35
35
|
"description.zh-CN": "注册和管理自定义事件源",
|
|
36
36
|
"displayName.zh-CN": "事件源",
|