@friggframework/admin-scripts 2.0.0--canary.517.aeeef23.0 → 2.0.0--canary.517.7e78259.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 +0 -2
- package/package.json +6 -6
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +58 -35
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +37 -177
- package/src/adapters/aws-scheduler-adapter.js +12 -5
- package/src/adapters/scheduler-adapter-factory.js +14 -34
- package/src/application/__tests__/script-runner.test.js +20 -1
- package/src/application/script-runner.js +5 -1
- package/src/infrastructure/script-executor-handler.js +1 -1
- package/PR_517_REVIEW_TRACKER.md +0 -56
package/index.js
CHANGED
|
@@ -36,7 +36,6 @@ const { AWSSchedulerAdapter } = require('./src/adapters/aws-scheduler-adapter');
|
|
|
36
36
|
const { LocalSchedulerAdapter } = require('./src/adapters/local-scheduler-adapter');
|
|
37
37
|
const {
|
|
38
38
|
createSchedulerAdapter,
|
|
39
|
-
detectSchedulerAdapterType,
|
|
40
39
|
} = require('./src/adapters/scheduler-adapter-factory');
|
|
41
40
|
|
|
42
41
|
module.exports = {
|
|
@@ -71,5 +70,4 @@ module.exports = {
|
|
|
71
70
|
AWSSchedulerAdapter,
|
|
72
71
|
LocalSchedulerAdapter,
|
|
73
72
|
createSchedulerAdapter,
|
|
74
|
-
detectSchedulerAdapterType,
|
|
75
73
|
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/admin-scripts",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.517.
|
|
4
|
+
"version": "2.0.0--canary.517.7e78259.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.517.
|
|
8
|
+
"@friggframework/core": "2.0.0--canary.517.7e78259.0",
|
|
9
9
|
"bcryptjs": "^2.4.3",
|
|
10
10
|
"express": "^4.18.2",
|
|
11
11
|
"lodash": "4.17.21",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"uuid": "^9.0.1"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@friggframework/eslint-config": "2.0.0--canary.517.
|
|
17
|
-
"@friggframework/prettier-config": "2.0.0--canary.517.
|
|
18
|
-
"@friggframework/test": "2.0.0--canary.517.
|
|
16
|
+
"@friggframework/eslint-config": "2.0.0--canary.517.7e78259.0",
|
|
17
|
+
"@friggframework/prettier-config": "2.0.0--canary.517.7e78259.0",
|
|
18
|
+
"@friggframework/test": "2.0.0--canary.517.7e78259.0",
|
|
19
19
|
"eslint": "^8.22.0",
|
|
20
20
|
"jest": "^29.7.0",
|
|
21
21
|
"prettier": "^2.7.1",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"maintenance",
|
|
47
47
|
"operations"
|
|
48
48
|
],
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "7e782599075f468b2defbb00cd243dcce243702f"
|
|
50
50
|
}
|
|
@@ -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',
|
|
@@ -301,9 +326,7 @@ describe('AWSSchedulerAdapter', () => {
|
|
|
301
326
|
|
|
302
327
|
describe('Lazy SDK loading', () => {
|
|
303
328
|
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
|
-
});
|
|
329
|
+
const newAdapter = new AWSSchedulerAdapter({ ...defaultParams });
|
|
307
330
|
|
|
308
331
|
expect(newAdapter.scheduler).toBeNull();
|
|
309
332
|
|
|
@@ -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
|
|
|
@@ -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',
|
|
@@ -6,64 +6,44 @@ const { LocalSchedulerAdapter } = require('./local-scheduler-adapter');
|
|
|
6
6
|
*
|
|
7
7
|
* Application Layer - Hexagonal Architecture
|
|
8
8
|
*
|
|
9
|
-
* Creates the appropriate scheduler adapter based on configuration
|
|
10
|
-
*
|
|
9
|
+
* Creates the appropriate scheduler adapter based on explicit configuration
|
|
10
|
+
* from appDefinition. Does not auto-detect or read environment variables.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Create a scheduler adapter instance
|
|
15
15
|
*
|
|
16
|
-
* @param {Object} options - Configuration options
|
|
17
|
-
* @param {string}
|
|
18
|
-
* @param {string} [options.region] - AWS region (for AWS adapter)
|
|
16
|
+
* @param {Object} options - Configuration options (from appDefinition.adminScripts.scheduler)
|
|
17
|
+
* @param {string} options.type - Adapter type ('aws', 'eventbridge', 'local') - required
|
|
19
18
|
* @param {Object} [options.credentials] - AWS credentials (for AWS adapter)
|
|
20
|
-
* @param {string} [options.targetLambdaArn] - Lambda ARN to invoke (for AWS adapter)
|
|
21
|
-
* @param {string} [options.scheduleGroupName] - EventBridge schedule group name (for AWS adapter)
|
|
19
|
+
* @param {string} [options.targetLambdaArn] - Lambda ARN to invoke (required for AWS adapter)
|
|
20
|
+
* @param {string} [options.scheduleGroupName] - EventBridge schedule group name (required for AWS adapter)
|
|
21
|
+
* @param {string} [options.roleArn] - IAM role ARN for scheduler (required for AWS adapter)
|
|
22
22
|
* @returns {SchedulerAdapter} Configured scheduler adapter
|
|
23
23
|
*/
|
|
24
24
|
function createSchedulerAdapter(options = {}) {
|
|
25
|
-
|
|
25
|
+
if (!options.type) {
|
|
26
|
+
throw new Error('Scheduler adapter type is required. Configure in appDefinition.adminScripts.scheduler.type');
|
|
27
|
+
}
|
|
26
28
|
|
|
27
|
-
switch (
|
|
29
|
+
switch (options.type.toLowerCase()) {
|
|
28
30
|
case 'aws':
|
|
29
31
|
case 'eventbridge':
|
|
30
32
|
return new AWSSchedulerAdapter({
|
|
31
|
-
region: options.region,
|
|
32
33
|
credentials: options.credentials,
|
|
33
34
|
targetLambdaArn: options.targetLambdaArn,
|
|
34
35
|
scheduleGroupName: options.scheduleGroupName,
|
|
36
|
+
roleArn: options.roleArn,
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
case 'local':
|
|
38
|
-
default:
|
|
39
40
|
return new LocalSchedulerAdapter();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Determine the appropriate scheduler adapter type based on environment
|
|
45
|
-
*
|
|
46
|
-
* @returns {string} Adapter type ('aws' or 'local')
|
|
47
|
-
*/
|
|
48
|
-
function detectSchedulerAdapterType() {
|
|
49
|
-
// If explicitly set, use that
|
|
50
|
-
if (process.env.SCHEDULER_ADAPTER) {
|
|
51
|
-
return process.env.SCHEDULER_ADAPTER;
|
|
52
|
-
}
|
|
53
41
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// Use AWS adapter in production/staging environments
|
|
58
|
-
if (['production', 'prod', 'staging', 'stage'].includes(stage.toLowerCase())) {
|
|
59
|
-
return 'aws';
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(`Unknown scheduler adapter type: ${options.type}`);
|
|
60
44
|
}
|
|
61
|
-
|
|
62
|
-
// Use local adapter for dev/test/local
|
|
63
|
-
return 'local';
|
|
64
45
|
}
|
|
65
46
|
|
|
66
47
|
module.exports = {
|
|
67
48
|
createSchedulerAdapter,
|
|
68
|
-
detectSchedulerAdapterType,
|
|
69
49
|
};
|
|
@@ -27,7 +27,7 @@ describe('ScriptRunner', () => {
|
|
|
27
27
|
},
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
async execute(
|
|
30
|
+
async execute(params) {
|
|
31
31
|
return { success: true, params };
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -102,6 +102,22 @@ describe('ScriptRunner', () => {
|
|
|
102
102
|
);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
it('should throw error if trigger is not provided', async () => {
|
|
106
|
+
const runner = new ScriptRunner({ scriptFactory, commands: mockCommands });
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
runner.execute('test-script', { foo: 'bar' }, {})
|
|
110
|
+
).rejects.toThrow('options.trigger is required');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should throw error if options are omitted entirely', async () => {
|
|
114
|
+
const runner = new ScriptRunner({ scriptFactory, commands: mockCommands });
|
|
115
|
+
|
|
116
|
+
await expect(
|
|
117
|
+
runner.execute('test-script', { foo: 'bar' })
|
|
118
|
+
).rejects.toThrow('options.trigger is required');
|
|
119
|
+
});
|
|
120
|
+
|
|
105
121
|
it('should handle script execution failure', async () => {
|
|
106
122
|
class FailingScript extends AdminScriptBase {
|
|
107
123
|
static Definition = {
|
|
@@ -235,6 +251,7 @@ describe('ScriptRunner', () => {
|
|
|
235
251
|
|
|
236
252
|
// Missing required parameter
|
|
237
253
|
const result = await runner.execute('schema-script', {}, {
|
|
254
|
+
trigger: 'MANUAL',
|
|
238
255
|
dryRun: true,
|
|
239
256
|
});
|
|
240
257
|
|
|
@@ -272,6 +289,7 @@ describe('ScriptRunner', () => {
|
|
|
272
289
|
name: 123,
|
|
273
290
|
enabled: 'true',
|
|
274
291
|
}, {
|
|
292
|
+
trigger: 'MANUAL',
|
|
275
293
|
dryRun: true,
|
|
276
294
|
});
|
|
277
295
|
|
|
@@ -307,6 +325,7 @@ describe('ScriptRunner', () => {
|
|
|
307
325
|
name: 'test',
|
|
308
326
|
count: 42,
|
|
309
327
|
}, {
|
|
328
|
+
trigger: 'MANUAL',
|
|
310
329
|
dryRun: true,
|
|
311
330
|
});
|
|
312
331
|
|
|
@@ -32,7 +32,11 @@ class ScriptRunner {
|
|
|
32
32
|
* @param {boolean} options.dryRun - Dry-run mode: validate and preview without executing
|
|
33
33
|
*/
|
|
34
34
|
async execute(scriptName, params = {}, options = {}) {
|
|
35
|
-
const { trigger
|
|
35
|
+
const { trigger, audit = {}, executionId: existingExecutionId, dryRun = false } = options;
|
|
36
|
+
|
|
37
|
+
if (!trigger) {
|
|
38
|
+
throw new Error('options.trigger is required (MANUAL | SCHEDULED | QUEUE)');
|
|
39
|
+
}
|
|
36
40
|
|
|
37
41
|
// Get script class
|
|
38
42
|
const scriptClass = this.scriptFactory.get(scriptName);
|
package/PR_517_REVIEW_TRACKER.md
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# PR #517 Comment Tracker
|
|
2
|
-
|
|
3
|
-
## ✅ ADDRESSED - Ready to Reply
|
|
4
|
-
|
|
5
|
-
| # | File:Line | Original Comment | What We Did | Reply |
|
|
6
|
-
|---|-----------|------------------|-------------|-------|
|
|
7
|
-
| 1 | `admin-frigg-commands.js:15` | "I don't think this should even exist, we're just duplicating methods" | Renamed to `AdminScriptContext`, clarified as facade pattern | Renamed to `AdminScriptContext`. It's a facade - wraps repositories so scripts have one API instead of multiple imports. Open to discussing alternatives. |
|
|
8
|
-
| 2 | `admin-script-base.js:94` | "Commands should come via constructor" | Changed to constructor injection | Done. Context now passed via constructor, scripts access via `this.context`. |
|
|
9
|
-
| 3 | `admin-script-base.js:109` | "logging does not belong here" | Removed logging from base class | Removed. Scripts use `this.context.log()` which persists to admin process record. |
|
|
10
|
-
| 4 | `admin-script-base.js:54` | "I would rename to requireIntegrationInstance" | Already renamed | Done - already renamed in current code. |
|
|
11
|
-
| 5 | `admin-script-base.js:56` | "This is just a duplication of the static Definition" | Cleaned up display object | Cleaned up. `display` now only holds UI overrides (category, icon). Label/description fall back to top-level via static methods. |
|
|
12
|
-
| 6 | `schedule-management-use-case.js:1` | "A use case should have single entry point" | Already split in previous session | Already split into `UpsertScheduleUseCase`, `DeleteScheduleUseCase`, `GetEffectiveScheduleUseCase`. |
|
|
13
|
-
| 7 | `package.json:20` | "chai and sinon should slowly be pushed away" | Sinon removed in previous session | Sinon already removed. Will remove chai too. |
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## 📝 NEEDS RESPONSE ONLY - No Code Change Required
|
|
18
|
-
|
|
19
|
-
| # | File:Line | Original Comment | Reply |
|
|
20
|
-
|---|-----------|------------------|-------|
|
|
21
|
-
| 8 | `admin-frigg-commands.js:160` | "this does not belong here" (queueScript) | queueScript enables self-queuing pattern (fan-out, pagination, retries). It's here so scripts don't need queue internals. Could move to separate utility if preferred. |
|
|
22
|
-
| 9 | `admin-script-base.js:39` | "why do we need the source?" | Distinguishes builtin vs user-defined scripts. UI can filter differently, builtins could have special handling. Could remove if not needed. |
|
|
23
|
-
| 10 | `admin-script-base.js:42` | "what's the idea with these schemas?" | Optional JSON Schema for validation/documentation. Could wire to OpenAPI or dynamic UI forms. Not critical for v1 - could remove and add later. |
|
|
24
|
-
| 11 | `admin-script-base.js:46` | "enabled property confusion" | Agreed the matrix is confusing. Intent: `schedule.enabled` controls auto-trigger independent of registration. Could simplify to just use presence in appDefinition. |
|
|
25
|
-
| 12 | `admin-script-base.js:52` | "Do we have retry logic in place already?" | Not yet - placeholder for Phase 2. Could remove until we build it. |
|
|
26
|
-
| 13 | `admin-script-base.js:81` | "What is the executionId?" | ID of AdminProcess record tracking this execution. Used to persist logs and update status. Created before script runs, passed to constructor. |
|
|
27
|
-
| 14 | `schedule-management-use-case.js:89` | "why save to database if EventBridge is source of truth?" | Database stores user's config override. EventBridge is execution engine. On deploy, we sync DB to EventBridge. Tracks user config vs code default. |
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## 🔧 OUTSTANDING - Needs Code Changes
|
|
32
|
-
|
|
33
|
-
| # | File:Line | Original Comment | Task |
|
|
34
|
-
|---|-----------|------------------|------|
|
|
35
|
-
| 15 | `docs/architecture-decisions/005-admin-script-runner.md:71` | "What is 'frigg' in this parameter?" | Update ADR - rename `frigg` to `context` throughout |
|
|
36
|
-
| 16 | `package.json:22` | "We already use nock for http request mocking" | Remove msw if present, use nock consistently |
|
|
37
|
-
| 17 | `package.json:12` | "why mongoose?" | Check if mongoose needed or can be removed |
|
|
38
|
-
| 18 | `schedule-management-use-case.js:109` | "leaking AWS specifics" | Abstract behind SchedulerAdapter, remove EventBridge references from use case |
|
|
39
|
-
| 19 | `schedule-management-use-case.js:138` | "should not mention EventBridge here" | Same as above |
|
|
40
|
-
| 20 | `adapters/aws-scheduler-adapter.js:30` | "should not infer/guess/default any variable" | Remove defaults, require explicit config |
|
|
41
|
-
| 21 | `adapters/scheduler-adapter-factory.js:48` | "env var confusion, prefer appDefinition" | Move scheduler config to appDefinition |
|
|
42
|
-
| 22 | `script-runner.js:36` | "we should not assume default values" | Remove defaults, require explicit values |
|
|
43
|
-
| 23 | `dry-run-http-interceptor.js:1` | "I don't understand why this is needed" | Explain or remove - was for intercepting HTTP in dry-run mode |
|
|
44
|
-
| 24 | `dry-run-repository-wrapper.js:1` | "This is smelly" | Review/remove - was for wrapping repos in dry-run mode |
|
|
45
|
-
| 25 | `.github/workflows/release.yml:11` | "why do we need those?" | Check release workflow changes |
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
## ❓ NEEDS DISCUSSION - Architectural Decisions
|
|
50
|
-
|
|
51
|
-
| # | File:Line | Original Comment | Decision Needed |
|
|
52
|
-
|---|-----------|------------------|-----------------|
|
|
53
|
-
| 26 | `admin-script-base.js:39` | source field | Keep BUILTIN/USER_DEFINED or remove? |
|
|
54
|
-
| 27 | `admin-script-base.js:42` | inputSchema/outputSchema | Keep for future or remove for now? |
|
|
55
|
-
| 28 | `admin-script-base.js:46` | schedule.enabled | Simplify to just appDefinition presence? |
|
|
56
|
-
| 29 | `admin-script-base.js:52` | maxRetries | Remove placeholder or keep? |
|