@clairejs/server 3.26.2 → 3.27.0
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
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
import { AbstractLogger } from "@clairejs/core";
|
|
2
2
|
import { AbstractDbAdapter } from "@clairejs/orm";
|
|
3
|
-
import
|
|
3
|
+
import { SchedulerClient } from "@aws-sdk/client-scheduler";
|
|
4
4
|
import { AbstractJob } from "./interfaces";
|
|
5
5
|
import { AbstractJobScheduler } from "./AbstractJobScheduler";
|
|
6
6
|
import { AbstractJobRepository } from "./AbstractJobRepository";
|
|
7
|
-
/**
|
|
8
|
-
* Following EventBrige permissions is required: listRules, putRule, deleteRule, listTargetsByRule, putTargets, removeTargets.
|
|
9
|
-
* Also update Lambda function to alow function invocation from events.amazonaws.com.
|
|
10
|
-
*/
|
|
11
7
|
export declare class AwsJobScheduler extends AbstractJobScheduler {
|
|
12
8
|
protected readonly logger: AbstractLogger;
|
|
13
9
|
protected readonly db: AbstractDbAdapter;
|
|
14
10
|
protected readonly jobRepo: AbstractJobRepository;
|
|
15
11
|
protected readonly apiLambdaFunctionArn: string;
|
|
12
|
+
protected readonly apiLambdaFunctionRoleArn: string;
|
|
16
13
|
protected readonly jobNamespace: string;
|
|
17
|
-
protected readonly
|
|
18
|
-
|
|
19
|
-
constructor(logger: AbstractLogger, db: AbstractDbAdapter, jobRepo: AbstractJobRepository, apiLambdaFunctionArn: string, jobNamespace: string, eventBusName?: string);
|
|
14
|
+
protected readonly scheduler: SchedulerClient;
|
|
15
|
+
constructor(logger: AbstractLogger, db: AbstractDbAdapter, jobRepo: AbstractJobRepository, apiLambdaFunctionArn: string, apiLambdaFunctionRoleArn: string, jobNamespace: string);
|
|
20
16
|
handleCron(jobInfo: AbstractJob): Promise<void>;
|
|
21
|
-
private
|
|
17
|
+
private convertCronToSchedulerExpression;
|
|
22
18
|
protected cleanupJob(job: AbstractJob): Promise<void>;
|
|
23
19
|
protected getScheduledJobs(): Promise<AbstractJob[]>;
|
|
24
20
|
protected _scheduleJob(jobInfo: AbstractJob): Promise<void>;
|
|
@@ -9,151 +9,173 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
import { AbstractLogger, Errors, LogContext } from "@clairejs/core";
|
|
11
11
|
import { AbstractDbAdapter } from "@clairejs/orm";
|
|
12
|
-
import
|
|
13
|
-
import aws from "aws-sdk";
|
|
12
|
+
import { SchedulerClient, CreateScheduleCommand, DeleteScheduleCommand, GetScheduleCommand, ListSchedulesCommand, ScheduleState } from "@aws-sdk/client-scheduler";
|
|
14
13
|
import { AbstractJobScheduler } from "./AbstractJobScheduler";
|
|
15
14
|
import { AbstractJobRepository } from "./AbstractJobRepository";
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* AWS Scheduler API permissions required:
|
|
17
|
+
* scheduler:CreateSchedule, scheduler:DeleteSchedule, scheduler:GetSchedule,
|
|
18
|
+
* scheduler:ListSchedules, scheduler:UpdateSchedule
|
|
19
|
+
* Also update Lambda function to allow function invocation from scheduler.amazonaws.com.
|
|
19
20
|
*/
|
|
21
|
+
const schedulerGroupName = "default";
|
|
20
22
|
let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
|
|
21
23
|
logger;
|
|
22
24
|
db;
|
|
23
25
|
jobRepo;
|
|
24
26
|
apiLambdaFunctionArn;
|
|
27
|
+
apiLambdaFunctionRoleArn;
|
|
25
28
|
jobNamespace;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
constructor(logger, db, jobRepo, apiLambdaFunctionArn, jobNamespace, eventBusName = "default") {
|
|
29
|
+
scheduler = new SchedulerClient();
|
|
30
|
+
constructor(logger, db, jobRepo, apiLambdaFunctionArn, apiLambdaFunctionRoleArn, jobNamespace) {
|
|
29
31
|
super(logger, db, jobRepo);
|
|
30
32
|
this.logger = logger;
|
|
31
33
|
this.db = db;
|
|
32
34
|
this.jobRepo = jobRepo;
|
|
33
35
|
this.apiLambdaFunctionArn = apiLambdaFunctionArn;
|
|
36
|
+
this.apiLambdaFunctionRoleArn = apiLambdaFunctionRoleArn;
|
|
34
37
|
this.jobNamespace = jobNamespace;
|
|
35
|
-
this.eventBusName = eventBusName;
|
|
36
38
|
}
|
|
37
39
|
async handleCron(jobInfo) {
|
|
38
40
|
this.logger.debug(`Handle cron`, jobInfo);
|
|
39
41
|
await this.executeJob(jobInfo);
|
|
40
42
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
async cleanupJob(job) {
|
|
53
|
-
if (job.cron) {
|
|
54
|
-
//-- do nothing
|
|
43
|
+
convertCronToSchedulerExpression(cron) {
|
|
44
|
+
// AWS Scheduler uses a different cron format than standard cron
|
|
45
|
+
// Standard cron: minute hour day month day-of-week
|
|
46
|
+
// AWS Scheduler: minute hour day month day-of-week year
|
|
47
|
+
const parts = cron.split(" ");
|
|
48
|
+
if (parts.length === 5) {
|
|
49
|
+
// Add year field if not present
|
|
50
|
+
return `${cron} *`;
|
|
51
|
+
}
|
|
52
|
+
else if (parts.length === 6) {
|
|
53
|
+
return cron;
|
|
55
54
|
}
|
|
56
55
|
else {
|
|
56
|
+
throw Errors.SYSTEM_ERROR(`Invalid cron expression: ${cron}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async cleanupJob(job) {
|
|
60
|
+
if (job.at) {
|
|
57
61
|
await this.cancelJob(job.id);
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
async getScheduledJobs() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
65
|
+
try {
|
|
66
|
+
const listCommand = new ListSchedulesCommand({
|
|
67
|
+
GroupName: schedulerGroupName,
|
|
68
|
+
NamePrefix: this.jobNamespace,
|
|
69
|
+
});
|
|
70
|
+
const allSchedules = await this.scheduler.send(listCommand);
|
|
71
|
+
const allJobs = await Promise.all((allSchedules.Schedules || []).map(async (schedule) => {
|
|
72
|
+
const scheduleName = schedule.Name;
|
|
73
|
+
try {
|
|
74
|
+
const getCommand = new GetScheduleCommand({
|
|
75
|
+
Name: scheduleName,
|
|
76
|
+
GroupName: schedulerGroupName,
|
|
77
|
+
});
|
|
78
|
+
const scheduleDetails = await this.scheduler.send(getCommand);
|
|
79
|
+
if (!scheduleDetails.Target?.Arn || !scheduleDetails.Target?.Input) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const jobInfo = JSON.parse(scheduleDetails.Target.Input).requestContext.cronScheduler.data;
|
|
83
|
+
return jobInfo;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.logger.warn(`Failed to get schedule details for ${scheduleName}`, error);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
return allJobs.flatMap((job) => (job ? [job] : []));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
this.logger.error("Failed to get scheduled jobs", error);
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
84
96
|
}
|
|
85
97
|
async _scheduleJob(jobInfo) {
|
|
86
98
|
this.logger.debug("Scheduling job: ", jobInfo);
|
|
87
99
|
if (jobInfo.cron || jobInfo.at) {
|
|
88
100
|
const jobId = `${this.jobNamespace}${jobInfo.id}`;
|
|
89
|
-
let
|
|
101
|
+
let scheduleExpression;
|
|
102
|
+
let flexibleTimeWindow;
|
|
90
103
|
if (jobInfo.at) {
|
|
91
|
-
|
|
104
|
+
// For one-time jobs, use at() expression
|
|
105
|
+
const date = new Date(jobInfo.at);
|
|
106
|
+
scheduleExpression = `at(${date.toISOString()})`;
|
|
107
|
+
// Add flexible time window for one-time jobs to handle slight delays
|
|
108
|
+
flexibleTimeWindow = {
|
|
109
|
+
Mode: "OFF",
|
|
110
|
+
};
|
|
92
111
|
}
|
|
93
112
|
else if (jobInfo.cron) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
// For recurring jobs, use cron() expression
|
|
114
|
+
const schedulerCron = this.convertCronToSchedulerExpression(jobInfo.cron);
|
|
115
|
+
scheduleExpression = `cron(${schedulerCron})`;
|
|
116
|
+
// Add flexible time window for recurring jobs
|
|
117
|
+
flexibleTimeWindow = {
|
|
118
|
+
Mode: "FLEXIBLE",
|
|
119
|
+
MaximumWindowInMinutes: 5, // Allow 5-minute window for execution
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${jobInfo.jobName}`);
|
|
99
124
|
}
|
|
100
|
-
this.logger.debug("
|
|
101
|
-
|
|
102
|
-
await this.eventbridge
|
|
103
|
-
.putRule({
|
|
125
|
+
this.logger.debug("Schedule expression", scheduleExpression);
|
|
126
|
+
const scheduleParams = {
|
|
104
127
|
Name: jobId,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
ScheduleExpression:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
{
|
|
117
|
-
Arn: this.apiLambdaFunctionArn,
|
|
118
|
-
Id: jobId,
|
|
119
|
-
//-- this json will be passed to lambda function as event input
|
|
120
|
-
Input: JSON.stringify({
|
|
121
|
-
requestContext: {
|
|
122
|
-
cronScheduler: {
|
|
123
|
-
data: { ...jobInfo, jobId },
|
|
124
|
-
},
|
|
128
|
+
GroupName: schedulerGroupName,
|
|
129
|
+
Description: `${jobInfo.jobName} - ${scheduleExpression}`,
|
|
130
|
+
ScheduleExpression: scheduleExpression,
|
|
131
|
+
FlexibleTimeWindow: flexibleTimeWindow,
|
|
132
|
+
Target: {
|
|
133
|
+
Arn: this.apiLambdaFunctionArn,
|
|
134
|
+
RoleArn: this.apiLambdaFunctionRoleArn,
|
|
135
|
+
Input: JSON.stringify({
|
|
136
|
+
requestContext: {
|
|
137
|
+
cronScheduler: {
|
|
138
|
+
data: { ...jobInfo, jobId },
|
|
125
139
|
},
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
State: ScheduleState.ENABLED,
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
const createCommand = new CreateScheduleCommand(scheduleParams);
|
|
147
|
+
await this.scheduler.send(createCommand);
|
|
148
|
+
this.logger.debug(`Successfully created schedule: ${jobId}`);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
this.logger.error(`Failed to create schedule: ${jobId}`, error);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
131
154
|
}
|
|
132
155
|
else {
|
|
133
156
|
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${jobInfo.jobName}`);
|
|
134
157
|
}
|
|
135
158
|
}
|
|
136
159
|
async cancelJob(jobId) {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
.promise();
|
|
160
|
+
const scheduleName = `${this.jobNamespace}${jobId}`;
|
|
161
|
+
try {
|
|
162
|
+
const deleteCommand = new DeleteScheduleCommand({
|
|
163
|
+
Name: scheduleName,
|
|
164
|
+
GroupName: schedulerGroupName,
|
|
165
|
+
});
|
|
166
|
+
await this.scheduler.send(deleteCommand);
|
|
167
|
+
this.logger.debug(`Successfully deleted schedule: ${scheduleName}`);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
this.logger.error(`Failed to delete schedule: ${scheduleName}`, error);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
151
173
|
}
|
|
152
174
|
};
|
|
153
175
|
AwsJobScheduler = __decorate([
|
|
154
176
|
LogContext(),
|
|
155
177
|
__metadata("design:paramtypes", [AbstractLogger,
|
|
156
178
|
AbstractDbAdapter,
|
|
157
|
-
AbstractJobRepository, String, String,
|
|
179
|
+
AbstractJobRepository, String, String, String])
|
|
158
180
|
], AwsJobScheduler);
|
|
159
181
|
export { AwsJobScheduler };
|
|
@@ -106,6 +106,7 @@ export class LambdaWrapper {
|
|
|
106
106
|
return toApiGatewayFormat({ code: 200, headers: corsHeaders, cookies: {} });
|
|
107
107
|
}
|
|
108
108
|
if (requestOptions.method === CRON_REQUEST_METHOD) {
|
|
109
|
+
console.log("handle cron", requestOptions.body);
|
|
109
110
|
await this.jobScheduler?.handleCron(requestOptions.body);
|
|
110
111
|
return toApiGatewayFormat({ code: 200, headers: corsHeaders, cookies: {} });
|
|
111
112
|
}
|
|
@@ -11,6 +11,7 @@ const parseCookie = (cookieArray) => {
|
|
|
11
11
|
return result;
|
|
12
12
|
};
|
|
13
13
|
export const lambdaRequestMapper = (event) => {
|
|
14
|
+
console.log("lambda event", event);
|
|
14
15
|
if (event.requestContext.eventType) {
|
|
15
16
|
//-- socket
|
|
16
17
|
const method = event.requestContext.eventType.toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clairejs/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.27.0",
|
|
4
4
|
"description": "Claire server NodeJs framework written in Typescript.",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"author": "immort",
|
|
16
16
|
"license": "ISC",
|
|
17
17
|
"dependencies": {
|
|
18
|
+
"@aws-sdk/client-scheduler": "^3.848.0",
|
|
18
19
|
"aws-sdk": "^2.841.0",
|
|
19
20
|
"axios": "^1.6.2",
|
|
20
21
|
"cookie-parser": "^1.4.6",
|