@friggframework/admin-scripts 2.0.0--canary.522.923dfae.0 → 2.0.0--canary.517.1687d70.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/index.js +12 -5
- package/package.json +7 -13
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +95 -35
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +8 -11
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +37 -177
- package/src/adapters/aws-scheduler-adapter.js +30 -12
- package/src/adapters/local-scheduler-adapter.js +6 -1
- package/src/adapters/scheduler-adapter-factory.js +14 -34
- package/src/application/__tests__/admin-frigg-commands.test.js +82 -296
- package/src/application/__tests__/admin-script-base.test.js +52 -127
- package/src/application/__tests__/script-runner.test.js +33 -17
- package/src/application/__tests__/validate-script-input.test.js +196 -0
- package/src/application/admin-frigg-commands.js +24 -99
- package/src/application/admin-script-base.js +12 -111
- package/src/application/script-runner.js +46 -156
- package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +168 -0
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +114 -0
- package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +201 -0
- package/src/application/use-cases/delete-schedule-use-case.js +108 -0
- package/src/application/use-cases/get-effective-schedule-use-case.js +78 -0
- package/src/application/use-cases/index.js +18 -0
- package/src/application/use-cases/upsert-schedule-use-case.js +127 -0
- package/src/application/validate-script-input.js +116 -0
- package/src/builtins/__tests__/integration-health-check.test.js +72 -63
- package/src/builtins/__tests__/oauth-token-refresh.test.js +49 -39
- package/src/builtins/integration-health-check.js +23 -24
- package/src/builtins/oauth-token-refresh.js +19 -20
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
- package/src/infrastructure/__tests__/admin-script-router.test.js +67 -50
- package/src/infrastructure/admin-auth-middleware.js +5 -43
- package/src/infrastructure/admin-script-router.js +77 -44
- package/src/infrastructure/script-executor-handler.js +29 -25
- package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
- package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
- package/src/application/dry-run-http-interceptor.js +0 -296
- package/src/application/dry-run-repository-wrapper.js +0 -261
- package/src/application/schedule-management-use-case.js +0 -230
package/index.js
CHANGED
|
@@ -8,11 +8,17 @@
|
|
|
8
8
|
// Application Services
|
|
9
9
|
const { ScriptFactory, getScriptFactory, createScriptFactory } = require('./src/application/script-factory');
|
|
10
10
|
const { AdminScriptBase } = require('./src/application/admin-script-base');
|
|
11
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
AdminScriptContext,
|
|
13
|
+
createAdminScriptContext,
|
|
14
|
+
// Legacy aliases (deprecated)
|
|
15
|
+
AdminFriggCommands,
|
|
16
|
+
createAdminFriggCommands,
|
|
17
|
+
} = require('./src/application/admin-frigg-commands');
|
|
12
18
|
const { ScriptRunner, createScriptRunner } = require('./src/application/script-runner');
|
|
13
19
|
|
|
14
20
|
// Infrastructure
|
|
15
|
-
const {
|
|
21
|
+
const { validateAdminApiKey } = require('./src/infrastructure/admin-auth-middleware');
|
|
16
22
|
const { router, app, handler: routerHandler } = require('./src/infrastructure/admin-script-router');
|
|
17
23
|
const { handler: executorHandler } = require('./src/infrastructure/script-executor-handler');
|
|
18
24
|
|
|
@@ -30,7 +36,6 @@ const { AWSSchedulerAdapter } = require('./src/adapters/aws-scheduler-adapter');
|
|
|
30
36
|
const { LocalSchedulerAdapter } = require('./src/adapters/local-scheduler-adapter');
|
|
31
37
|
const {
|
|
32
38
|
createSchedulerAdapter,
|
|
33
|
-
detectSchedulerAdapterType,
|
|
34
39
|
} = require('./src/adapters/scheduler-adapter-factory');
|
|
35
40
|
|
|
36
41
|
module.exports = {
|
|
@@ -39,13 +44,16 @@ module.exports = {
|
|
|
39
44
|
ScriptFactory,
|
|
40
45
|
getScriptFactory,
|
|
41
46
|
createScriptFactory,
|
|
47
|
+
AdminScriptContext,
|
|
48
|
+
createAdminScriptContext,
|
|
49
|
+
// Legacy aliases (deprecated)
|
|
42
50
|
AdminFriggCommands,
|
|
43
51
|
createAdminFriggCommands,
|
|
44
52
|
ScriptRunner,
|
|
45
53
|
createScriptRunner,
|
|
46
54
|
|
|
47
55
|
// Infrastructure layer
|
|
48
|
-
|
|
56
|
+
validateAdminApiKey,
|
|
49
57
|
router,
|
|
50
58
|
app,
|
|
51
59
|
routerHandler,
|
|
@@ -62,5 +70,4 @@ module.exports = {
|
|
|
62
70
|
AWSSchedulerAdapter,
|
|
63
71
|
LocalSchedulerAdapter,
|
|
64
72
|
createSchedulerAdapter,
|
|
65
|
-
detectSchedulerAdapterType,
|
|
66
73
|
};
|
package/package.json
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/admin-scripts",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.517.1687d70.0",
|
|
5
5
|
"description": "Admin Script Runner for Frigg - Execute maintenance and operational scripts in hosted environments",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@aws-sdk/client-scheduler": "^3.588.0",
|
|
8
|
-
"@friggframework/core": "2.0.0--canary.
|
|
9
|
-
"bcryptjs": "^2.4.3",
|
|
8
|
+
"@friggframework/core": "2.0.0--canary.517.1687d70.0",
|
|
10
9
|
"express": "^4.18.2",
|
|
11
|
-
"
|
|
12
|
-
"mongoose": "6.11.6",
|
|
13
|
-
"serverless-http": "^3.2.0",
|
|
14
|
-
"uuid": "^9.0.1"
|
|
10
|
+
"serverless-http": "^3.2.0"
|
|
15
11
|
},
|
|
16
12
|
"devDependencies": {
|
|
17
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
18
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
19
|
-
"@friggframework/test": "2.0.0--canary.
|
|
20
|
-
"chai": "^4.3.6",
|
|
13
|
+
"@friggframework/eslint-config": "2.0.0--canary.517.1687d70.0",
|
|
14
|
+
"@friggframework/prettier-config": "2.0.0--canary.517.1687d70.0",
|
|
15
|
+
"@friggframework/test": "2.0.0--canary.517.1687d70.0",
|
|
21
16
|
"eslint": "^8.22.0",
|
|
22
17
|
"jest": "^29.7.0",
|
|
23
18
|
"prettier": "^2.7.1",
|
|
24
|
-
"sinon": "^16.1.1",
|
|
25
19
|
"supertest": "^7.1.4"
|
|
26
20
|
},
|
|
27
21
|
"scripts": {
|
|
@@ -49,5 +43,5 @@
|
|
|
49
43
|
"maintenance",
|
|
50
44
|
"operations"
|
|
51
45
|
],
|
|
52
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "1687d704a5eed1b0de52275200a579399722813d"
|
|
53
47
|
}
|
|
@@ -18,34 +18,28 @@ jest.mock('@aws-sdk/client-scheduler', () => {
|
|
|
18
18
|
};
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
const defaultParams = {
|
|
22
|
+
targetLambdaArn: 'arn:aws:lambda:us-east-1:123456789012:function:admin-script-executor',
|
|
23
|
+
scheduleGroupName: 'frigg-admin-scripts',
|
|
24
|
+
roleArn: 'arn:aws:iam::123456789012:role/test-role',
|
|
25
|
+
};
|
|
26
|
+
|
|
21
27
|
describe('AWSSchedulerAdapter', () => {
|
|
22
28
|
let adapter;
|
|
23
29
|
let mockSend;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
beforeAll(() => {
|
|
27
|
-
originalEnv = { ...process.env };
|
|
28
|
-
});
|
|
30
|
+
const originalEnv = process.env;
|
|
29
31
|
|
|
30
32
|
beforeEach(() => {
|
|
31
33
|
jest.clearAllMocks();
|
|
32
|
-
|
|
33
|
-
// Reset environment variables
|
|
34
|
-
process.env.AWS_REGION = 'us-east-1';
|
|
35
|
-
process.env.SCHEDULE_GROUP_NAME = 'test-schedule-group';
|
|
36
|
-
process.env.SCHEDULER_ROLE_ARN = 'arn:aws:iam::123456789012:role/test-role';
|
|
37
|
-
process.env.ADMIN_SCRIPT_LAMBDA_ARN = 'arn:aws:lambda:us-east-1:123456789012:function:test-executor';
|
|
34
|
+
process.env = { ...originalEnv, AWS_REGION: 'us-east-1' };
|
|
38
35
|
|
|
39
36
|
const sdk = require('@aws-sdk/client-scheduler');
|
|
40
37
|
mockSend = sdk._mockSend;
|
|
41
38
|
|
|
42
|
-
adapter = new AWSSchedulerAdapter({
|
|
43
|
-
targetLambdaArn: 'arn:aws:lambda:us-east-1:123456789012:function:admin-script-executor',
|
|
44
|
-
scheduleGroupName: 'frigg-admin-scripts',
|
|
45
|
-
});
|
|
39
|
+
adapter = new AWSSchedulerAdapter({ ...defaultParams });
|
|
46
40
|
});
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
afterEach(() => {
|
|
49
43
|
process.env = originalEnv;
|
|
50
44
|
});
|
|
51
45
|
|
|
@@ -60,35 +54,46 @@ describe('AWSSchedulerAdapter', () => {
|
|
|
60
54
|
});
|
|
61
55
|
|
|
62
56
|
describe('Constructor', () => {
|
|
63
|
-
it('should use provided configuration', () => {
|
|
57
|
+
it('should use provided configuration and AWS_REGION from env', () => {
|
|
58
|
+
process.env.AWS_REGION = 'eu-west-1';
|
|
64
59
|
const customAdapter = new AWSSchedulerAdapter({
|
|
65
|
-
region: 'eu-west-1',
|
|
66
60
|
targetLambdaArn: 'arn:aws:lambda:eu-west-1:123456789012:function:custom',
|
|
67
61
|
scheduleGroupName: 'custom-group',
|
|
62
|
+
roleArn: 'arn:aws:iam::123456789012:role/custom-role',
|
|
68
63
|
});
|
|
69
64
|
|
|
70
65
|
expect(customAdapter.region).toBe('eu-west-1');
|
|
71
66
|
expect(customAdapter.targetLambdaArn).toBe('arn:aws:lambda:eu-west-1:123456789012:function:custom');
|
|
72
67
|
expect(customAdapter.scheduleGroupName).toBe('custom-group');
|
|
68
|
+
expect(customAdapter.roleArn).toBe('arn:aws:iam::123456789012:role/custom-role');
|
|
73
69
|
});
|
|
74
70
|
|
|
75
|
-
it('should
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
expect(envAdapter.scheduleGroupName).toBe('test-schedule-group');
|
|
71
|
+
it('should throw if AWS_REGION is not set', () => {
|
|
72
|
+
delete process.env.AWS_REGION;
|
|
73
|
+
expect(() => new AWSSchedulerAdapter({
|
|
74
|
+
...defaultParams,
|
|
75
|
+
})).toThrow('AWSSchedulerAdapter requires AWS_REGION environment variable');
|
|
81
76
|
});
|
|
82
77
|
|
|
83
|
-
it('should
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
it('should throw if targetLambdaArn is missing', () => {
|
|
79
|
+
expect(() => new AWSSchedulerAdapter({
|
|
80
|
+
scheduleGroupName: defaultParams.scheduleGroupName,
|
|
81
|
+
roleArn: defaultParams.roleArn,
|
|
82
|
+
})).toThrow('AWSSchedulerAdapter requires targetLambdaArn');
|
|
83
|
+
});
|
|
87
84
|
|
|
88
|
-
|
|
85
|
+
it('should throw if scheduleGroupName is missing', () => {
|
|
86
|
+
expect(() => new AWSSchedulerAdapter({
|
|
87
|
+
targetLambdaArn: defaultParams.targetLambdaArn,
|
|
88
|
+
roleArn: defaultParams.roleArn,
|
|
89
|
+
})).toThrow('AWSSchedulerAdapter requires scheduleGroupName');
|
|
90
|
+
});
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
expect(
|
|
92
|
+
it('should throw if roleArn is missing', () => {
|
|
93
|
+
expect(() => new AWSSchedulerAdapter({
|
|
94
|
+
targetLambdaArn: defaultParams.targetLambdaArn,
|
|
95
|
+
scheduleGroupName: defaultParams.scheduleGroupName,
|
|
96
|
+
})).toThrow('AWSSchedulerAdapter requires roleArn');
|
|
92
97
|
});
|
|
93
98
|
});
|
|
94
99
|
|
|
@@ -139,7 +144,7 @@ describe('AWSSchedulerAdapter', () => {
|
|
|
139
144
|
});
|
|
140
145
|
});
|
|
141
146
|
|
|
142
|
-
it('should configure target with Lambda ARN and
|
|
147
|
+
it('should configure target with Lambda ARN and constructor roleArn', async () => {
|
|
143
148
|
mockSend.mockResolvedValue({
|
|
144
149
|
ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
145
150
|
});
|
|
@@ -154,6 +159,26 @@ describe('AWSSchedulerAdapter', () => {
|
|
|
154
159
|
expect(command.params.Target.RoleArn).toBe('arn:aws:iam::123456789012:role/test-role');
|
|
155
160
|
});
|
|
156
161
|
|
|
162
|
+
it('should use roleArn from constructor, not process.env', async () => {
|
|
163
|
+
const customRoleArn = 'arn:aws:iam::999999999999:role/custom-scheduler-role';
|
|
164
|
+
const customAdapter = new AWSSchedulerAdapter({
|
|
165
|
+
...defaultParams,
|
|
166
|
+
roleArn: customRoleArn,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
mockSend.mockResolvedValue({
|
|
170
|
+
ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await customAdapter.createSchedule({
|
|
174
|
+
scriptName: 'test-script',
|
|
175
|
+
cronExpression: 'cron(0 0 * * ? *)',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const command = mockSend.mock.calls[0][0];
|
|
179
|
+
expect(command.params.Target.RoleArn).toBe(customRoleArn);
|
|
180
|
+
});
|
|
181
|
+
|
|
157
182
|
it('should enable schedule by default', async () => {
|
|
158
183
|
mockSend.mockResolvedValue({
|
|
159
184
|
ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
@@ -181,6 +206,43 @@ describe('AWSSchedulerAdapter', () => {
|
|
|
181
206
|
const command = mockSend.mock.calls[0][0];
|
|
182
207
|
expect(command.params.FlexibleTimeWindow).toEqual({ Mode: 'OFF' });
|
|
183
208
|
});
|
|
209
|
+
|
|
210
|
+
it('should fall back to UpdateScheduleCommand on ConflictException', async () => {
|
|
211
|
+
const conflictError = new Error('Schedule already exists');
|
|
212
|
+
conflictError.name = 'ConflictException';
|
|
213
|
+
|
|
214
|
+
mockSend
|
|
215
|
+
.mockRejectedValueOnce(conflictError)
|
|
216
|
+
.mockResolvedValueOnce({
|
|
217
|
+
ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const result = await adapter.createSchedule({
|
|
221
|
+
scriptName: 'test-script',
|
|
222
|
+
cronExpression: 'cron(0 0 * * ? *)',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(result).toEqual({
|
|
226
|
+
scheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
227
|
+
scheduleName: 'frigg-script-test-script',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(mockSend).toHaveBeenCalledTimes(2);
|
|
231
|
+
expect(mockSend.mock.calls[0][0]._type).toBe('CreateScheduleCommand');
|
|
232
|
+
expect(mockSend.mock.calls[1][0]._type).toBe('UpdateScheduleCommand');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should rethrow non-conflict errors', async () => {
|
|
236
|
+
const otherError = new Error('Access denied');
|
|
237
|
+
otherError.name = 'AccessDeniedException';
|
|
238
|
+
|
|
239
|
+
mockSend.mockRejectedValue(otherError);
|
|
240
|
+
|
|
241
|
+
await expect(adapter.createSchedule({
|
|
242
|
+
scriptName: 'test-script',
|
|
243
|
+
cronExpression: 'cron(0 0 * * ? *)',
|
|
244
|
+
})).rejects.toThrow('Access denied');
|
|
245
|
+
});
|
|
184
246
|
});
|
|
185
247
|
|
|
186
248
|
describe('deleteSchedule()', () => {
|
|
@@ -301,9 +363,7 @@ describe('AWSSchedulerAdapter', () => {
|
|
|
301
363
|
|
|
302
364
|
describe('Lazy SDK loading', () => {
|
|
303
365
|
it('should load AWS SDK on first client access', () => {
|
|
304
|
-
const newAdapter = new AWSSchedulerAdapter({
|
|
305
|
-
targetLambdaArn: 'arn:aws:lambda:us-east-1:123456789012:function:test',
|
|
306
|
-
});
|
|
366
|
+
const newAdapter = new AWSSchedulerAdapter({ ...defaultParams });
|
|
307
367
|
|
|
308
368
|
expect(newAdapter.scheduler).toBeNull();
|
|
309
369
|
|
|
@@ -210,12 +210,12 @@ describe('LocalSchedulerAdapter', () => {
|
|
|
210
210
|
const schedules = await adapter.listSchedules();
|
|
211
211
|
|
|
212
212
|
expect(schedules).toHaveLength(3);
|
|
213
|
-
expect(schedules.map((s) => s.
|
|
214
|
-
expect(schedules.map((s) => s.
|
|
215
|
-
expect(schedules.map((s) => s.
|
|
213
|
+
expect(schedules.map((s) => s.Name)).toContain('frigg-script-script-1');
|
|
214
|
+
expect(schedules.map((s) => s.Name)).toContain('frigg-script-script-2');
|
|
215
|
+
expect(schedules.map((s) => s.Name)).toContain('frigg-script-script-3');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
it('should include all schedule properties', async () => {
|
|
218
|
+
it('should include all schedule properties in normalized format', async () => {
|
|
219
219
|
await adapter.createSchedule({
|
|
220
220
|
scriptName: 'test-script',
|
|
221
221
|
cronExpression: '0 0 * * *',
|
|
@@ -226,14 +226,11 @@ describe('LocalSchedulerAdapter', () => {
|
|
|
226
226
|
const schedules = await adapter.listSchedules();
|
|
227
227
|
|
|
228
228
|
expect(schedules[0]).toMatchObject({
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
enabled: true,
|
|
229
|
+
Name: 'frigg-script-test-script',
|
|
230
|
+
State: 'ENABLED',
|
|
231
|
+
ScheduleExpression: '0 0 * * *',
|
|
232
|
+
ScheduleExpressionTimezone: 'America/New_York',
|
|
234
233
|
});
|
|
235
|
-
expect(schedules[0]).toHaveProperty('createdAt');
|
|
236
|
-
expect(schedules[0]).toHaveProperty('updatedAt');
|
|
237
234
|
});
|
|
238
235
|
});
|
|
239
236
|
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
const {
|
|
2
|
-
createSchedulerAdapter,
|
|
3
|
-
detectSchedulerAdapterType,
|
|
4
|
-
} = require('../scheduler-adapter-factory');
|
|
1
|
+
const { createSchedulerAdapter } = require('../scheduler-adapter-factory');
|
|
5
2
|
const { AWSSchedulerAdapter } = require('../aws-scheduler-adapter');
|
|
6
3
|
const { LocalSchedulerAdapter } = require('../local-scheduler-adapter');
|
|
7
4
|
|
|
@@ -17,71 +14,56 @@ jest.mock('@aws-sdk/client-scheduler', () => ({
|
|
|
17
14
|
ListSchedulesCommand: jest.fn(),
|
|
18
15
|
}));
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
const awsAdapterParams = {
|
|
18
|
+
targetLambdaArn: 'arn:aws:lambda:us-east-1:123456789012:function:test',
|
|
19
|
+
scheduleGroupName: 'test-group',
|
|
20
|
+
roleArn: 'arn:aws:iam::123456789012:role/test-role',
|
|
21
|
+
};
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
23
|
+
describe('Scheduler Adapter Factory', () => {
|
|
24
|
+
const originalEnv = process.env;
|
|
26
25
|
|
|
27
26
|
beforeEach(() => {
|
|
28
|
-
|
|
29
|
-
delete process.env.SCHEDULER_ADAPTER;
|
|
30
|
-
delete process.env.STAGE;
|
|
31
|
-
delete process.env.NODE_ENV;
|
|
27
|
+
process.env = { ...originalEnv, AWS_REGION: 'us-east-1' };
|
|
32
28
|
});
|
|
33
29
|
|
|
34
|
-
|
|
30
|
+
afterEach(() => {
|
|
35
31
|
process.env = originalEnv;
|
|
36
32
|
});
|
|
37
33
|
|
|
38
34
|
describe('createSchedulerAdapter()', () => {
|
|
39
|
-
it('should
|
|
40
|
-
|
|
35
|
+
it('should throw if type is not provided', () => {
|
|
36
|
+
expect(() => createSchedulerAdapter()).toThrow();
|
|
37
|
+
});
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
expect(
|
|
39
|
+
it('should throw if type is not provided in options object', () => {
|
|
40
|
+
expect(() => createSchedulerAdapter({})).toThrow();
|
|
44
41
|
});
|
|
45
42
|
|
|
46
|
-
it('should create local adapter when
|
|
43
|
+
it('should create local adapter when type is "local"', () => {
|
|
47
44
|
const adapter = createSchedulerAdapter({ type: 'local' });
|
|
48
45
|
|
|
49
46
|
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
47
|
+
expect(adapter.getName()).toBe('local-cron');
|
|
50
48
|
});
|
|
51
49
|
|
|
52
50
|
it('should create AWS adapter when type is "aws"', () => {
|
|
53
|
-
const adapter = createSchedulerAdapter({ type: 'aws' });
|
|
51
|
+
const adapter = createSchedulerAdapter({ type: 'aws', ...awsAdapterParams });
|
|
54
52
|
|
|
55
53
|
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
56
54
|
expect(adapter.getName()).toBe('aws-eventbridge-scheduler');
|
|
57
55
|
});
|
|
58
56
|
|
|
59
57
|
it('should create AWS adapter when type is "eventbridge"', () => {
|
|
60
|
-
const adapter = createSchedulerAdapter({ type: 'eventbridge' });
|
|
61
|
-
|
|
62
|
-
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should use SCHEDULER_ADAPTER env variable', () => {
|
|
66
|
-
process.env.SCHEDULER_ADAPTER = 'aws';
|
|
67
|
-
|
|
68
|
-
const adapter = createSchedulerAdapter();
|
|
58
|
+
const adapter = createSchedulerAdapter({ type: 'eventbridge', ...awsAdapterParams });
|
|
69
59
|
|
|
70
60
|
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
71
61
|
});
|
|
72
62
|
|
|
73
|
-
it('should allow explicit type to override env variable', () => {
|
|
74
|
-
process.env.SCHEDULER_ADAPTER = 'aws';
|
|
75
|
-
|
|
76
|
-
const adapter = createSchedulerAdapter({ type: 'local' });
|
|
77
|
-
|
|
78
|
-
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
63
|
it('should handle case-insensitive type values', () => {
|
|
82
|
-
const adapter1 = createSchedulerAdapter({ type: 'AWS' });
|
|
64
|
+
const adapter1 = createSchedulerAdapter({ type: 'AWS', ...awsAdapterParams });
|
|
83
65
|
const adapter2 = createSchedulerAdapter({ type: 'LOCAL' });
|
|
84
|
-
const adapter3 = createSchedulerAdapter({ type: 'EventBridge' });
|
|
66
|
+
const adapter3 = createSchedulerAdapter({ type: 'EventBridge', ...awsAdapterParams });
|
|
85
67
|
|
|
86
68
|
expect(adapter1).toBeInstanceOf(AWSSchedulerAdapter);
|
|
87
69
|
expect(adapter2).toBeInstanceOf(LocalSchedulerAdapter);
|
|
@@ -91,17 +73,29 @@ describe('Scheduler Adapter Factory', () => {
|
|
|
91
73
|
it('should pass AWS configuration to AWS adapter', () => {
|
|
92
74
|
const config = {
|
|
93
75
|
type: 'aws',
|
|
94
|
-
region: 'eu-west-1',
|
|
95
76
|
targetLambdaArn: 'arn:aws:lambda:eu-west-1:123456789012:function:test',
|
|
96
77
|
scheduleGroupName: 'custom-group',
|
|
78
|
+
roleArn: 'arn:aws:iam::123456789012:role/custom-role',
|
|
97
79
|
};
|
|
98
80
|
|
|
99
81
|
const adapter = createSchedulerAdapter(config);
|
|
100
82
|
|
|
101
83
|
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
102
|
-
expect(adapter.region).toBe('
|
|
84
|
+
expect(adapter.region).toBe('us-east-1'); // From process.env.AWS_REGION
|
|
103
85
|
expect(adapter.targetLambdaArn).toBe('arn:aws:lambda:eu-west-1:123456789012:function:test');
|
|
104
86
|
expect(adapter.scheduleGroupName).toBe('custom-group');
|
|
87
|
+
expect(adapter.roleArn).toBe('arn:aws:iam::123456789012:role/custom-role');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should pass roleArn through to AWS adapter', () => {
|
|
91
|
+
const adapter = createSchedulerAdapter({
|
|
92
|
+
type: 'aws',
|
|
93
|
+
...awsAdapterParams,
|
|
94
|
+
roleArn: 'arn:aws:iam::999999999999:role/scheduler-role',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
98
|
+
expect(adapter.roleArn).toBe('arn:aws:iam::999999999999:role/scheduler-role');
|
|
105
99
|
});
|
|
106
100
|
|
|
107
101
|
it('should ignore AWS config for local adapter', () => {
|
|
@@ -116,142 +110,8 @@ describe('Scheduler Adapter Factory', () => {
|
|
|
116
110
|
expect(adapter.region).toBeUndefined();
|
|
117
111
|
});
|
|
118
112
|
|
|
119
|
-
it('should
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe('detectSchedulerAdapterType()', () => {
|
|
127
|
-
it('should return "local" by default', () => {
|
|
128
|
-
const type = detectSchedulerAdapterType();
|
|
129
|
-
|
|
130
|
-
expect(type).toBe('local');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('should return env SCHEDULER_ADAPTER when set', () => {
|
|
134
|
-
process.env.SCHEDULER_ADAPTER = 'aws';
|
|
135
|
-
|
|
136
|
-
const type = detectSchedulerAdapterType();
|
|
137
|
-
|
|
138
|
-
expect(type).toBe('aws');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should return "aws" for production stage', () => {
|
|
142
|
-
process.env.STAGE = 'production';
|
|
143
|
-
|
|
144
|
-
const type = detectSchedulerAdapterType();
|
|
145
|
-
|
|
146
|
-
expect(type).toBe('aws');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should return "aws" for prod stage', () => {
|
|
150
|
-
process.env.STAGE = 'prod';
|
|
151
|
-
|
|
152
|
-
const type = detectSchedulerAdapterType();
|
|
153
|
-
|
|
154
|
-
expect(type).toBe('aws');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should return "aws" for staging stage', () => {
|
|
158
|
-
process.env.STAGE = 'staging';
|
|
159
|
-
|
|
160
|
-
const type = detectSchedulerAdapterType();
|
|
161
|
-
|
|
162
|
-
expect(type).toBe('aws');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should return "aws" for stage stage', () => {
|
|
166
|
-
process.env.STAGE = 'stage';
|
|
167
|
-
|
|
168
|
-
const type = detectSchedulerAdapterType();
|
|
169
|
-
|
|
170
|
-
expect(type).toBe('aws');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('should handle case-insensitive stage values', () => {
|
|
174
|
-
process.env.STAGE = 'PRODUCTION';
|
|
175
|
-
|
|
176
|
-
const type = detectSchedulerAdapterType();
|
|
177
|
-
|
|
178
|
-
expect(type).toBe('aws');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('should return "local" for dev stage', () => {
|
|
182
|
-
process.env.STAGE = 'dev';
|
|
183
|
-
|
|
184
|
-
const type = detectSchedulerAdapterType();
|
|
185
|
-
|
|
186
|
-
expect(type).toBe('local');
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('should return "local" for development stage', () => {
|
|
190
|
-
process.env.STAGE = 'development';
|
|
191
|
-
|
|
192
|
-
const type = detectSchedulerAdapterType();
|
|
193
|
-
|
|
194
|
-
expect(type).toBe('local');
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('should return "local" for test stage', () => {
|
|
198
|
-
process.env.STAGE = 'test';
|
|
199
|
-
|
|
200
|
-
const type = detectSchedulerAdapterType();
|
|
201
|
-
|
|
202
|
-
expect(type).toBe('local');
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should return "local" for local stage', () => {
|
|
206
|
-
process.env.STAGE = 'local';
|
|
207
|
-
|
|
208
|
-
const type = detectSchedulerAdapterType();
|
|
209
|
-
|
|
210
|
-
expect(type).toBe('local');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('should use NODE_ENV as fallback for STAGE', () => {
|
|
214
|
-
delete process.env.STAGE;
|
|
215
|
-
process.env.NODE_ENV = 'production';
|
|
216
|
-
|
|
217
|
-
const type = detectSchedulerAdapterType();
|
|
218
|
-
|
|
219
|
-
expect(type).toBe('aws');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should prioritize explicit SCHEDULER_ADAPTER over auto-detection', () => {
|
|
223
|
-
process.env.SCHEDULER_ADAPTER = 'local';
|
|
224
|
-
process.env.STAGE = 'production';
|
|
225
|
-
|
|
226
|
-
const type = detectSchedulerAdapterType();
|
|
227
|
-
|
|
228
|
-
expect(type).toBe('local');
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
describe('Integration with createSchedulerAdapter', () => {
|
|
233
|
-
it('should auto-detect and create AWS adapter in production', () => {
|
|
234
|
-
process.env.STAGE = 'production';
|
|
235
|
-
|
|
236
|
-
const adapter = createSchedulerAdapter();
|
|
237
|
-
|
|
238
|
-
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('should auto-detect and create local adapter in development', () => {
|
|
242
|
-
process.env.STAGE = 'development';
|
|
243
|
-
|
|
244
|
-
const adapter = createSchedulerAdapter();
|
|
245
|
-
|
|
246
|
-
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('should allow explicit override of auto-detection', () => {
|
|
250
|
-
process.env.STAGE = 'production';
|
|
251
|
-
|
|
252
|
-
const adapter = createSchedulerAdapter({ type: 'local' });
|
|
253
|
-
|
|
254
|
-
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
113
|
+
it('should throw for unknown adapter type', () => {
|
|
114
|
+
expect(() => createSchedulerAdapter({ type: 'unknown-type' })).toThrow();
|
|
255
115
|
});
|
|
256
116
|
});
|
|
257
117
|
});
|
|
@@ -25,12 +25,19 @@ function loadSchedulerSDK() {
|
|
|
25
25
|
* Supports cron expressions, timezone configuration, and Lambda invocation.
|
|
26
26
|
*/
|
|
27
27
|
class AWSSchedulerAdapter extends SchedulerAdapter {
|
|
28
|
-
constructor({
|
|
28
|
+
constructor({ credentials, targetLambdaArn, scheduleGroupName, roleArn } = {}) {
|
|
29
29
|
super();
|
|
30
|
-
|
|
30
|
+
if (!targetLambdaArn) throw new Error('AWSSchedulerAdapter requires targetLambdaArn');
|
|
31
|
+
if (!scheduleGroupName) throw new Error('AWSSchedulerAdapter requires scheduleGroupName');
|
|
32
|
+
if (!roleArn) throw new Error('AWSSchedulerAdapter requires roleArn');
|
|
33
|
+
// Region inherits from the service (set by Lambda runtime, same for all AWS resources)
|
|
34
|
+
const region = process.env.AWS_REGION;
|
|
35
|
+
if (!region) throw new Error('AWSSchedulerAdapter requires AWS_REGION environment variable');
|
|
36
|
+
this.region = region;
|
|
31
37
|
this.credentials = credentials;
|
|
32
|
-
this.targetLambdaArn = targetLambdaArn
|
|
33
|
-
this.scheduleGroupName = scheduleGroupName
|
|
38
|
+
this.targetLambdaArn = targetLambdaArn;
|
|
39
|
+
this.scheduleGroupName = scheduleGroupName;
|
|
40
|
+
this.roleArn = roleArn;
|
|
34
41
|
this.scheduler = null;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -53,7 +60,7 @@ class AWSSchedulerAdapter extends SchedulerAdapter {
|
|
|
53
60
|
const client = this.getSchedulerClient();
|
|
54
61
|
const scheduleName = `frigg-script-${scriptName}`;
|
|
55
62
|
|
|
56
|
-
const
|
|
63
|
+
const scheduleParams = {
|
|
57
64
|
Name: scheduleName,
|
|
58
65
|
GroupName: this.scheduleGroupName,
|
|
59
66
|
ScheduleExpression: cronExpression,
|
|
@@ -61,7 +68,7 @@ class AWSSchedulerAdapter extends SchedulerAdapter {
|
|
|
61
68
|
FlexibleTimeWindow: { Mode: 'OFF' },
|
|
62
69
|
Target: {
|
|
63
70
|
Arn: this.targetLambdaArn,
|
|
64
|
-
RoleArn:
|
|
71
|
+
RoleArn: this.roleArn,
|
|
65
72
|
Input: JSON.stringify({
|
|
66
73
|
scriptName,
|
|
67
74
|
trigger: 'SCHEDULED',
|
|
@@ -69,13 +76,24 @@ class AWSSchedulerAdapter extends SchedulerAdapter {
|
|
|
69
76
|
}),
|
|
70
77
|
},
|
|
71
78
|
State: 'ENABLED',
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const response = await client.send(command);
|
|
75
|
-
return {
|
|
76
|
-
scheduleArn: response.ScheduleArn,
|
|
77
|
-
scheduleName: scheduleName,
|
|
78
79
|
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const response = await client.send(new CreateScheduleCommand(scheduleParams));
|
|
83
|
+
return {
|
|
84
|
+
scheduleArn: response.ScheduleArn,
|
|
85
|
+
scheduleName: scheduleName,
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error.name === 'ConflictException') {
|
|
89
|
+
const response = await client.send(new UpdateScheduleCommand(scheduleParams));
|
|
90
|
+
return {
|
|
91
|
+
scheduleArn: response.ScheduleArn,
|
|
92
|
+
scheduleName: scheduleName,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
async deleteSchedule(scriptName) {
|