@friggframework/admin-scripts 2.0.0--canary.517.9066dd6.0 → 2.0.0--canary.522.923dfae.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.
Files changed (36) hide show
  1. package/index.js +5 -12
  2. package/package.json +9 -6
  3. package/src/adapters/__tests__/aws-scheduler-adapter.test.js +35 -58
  4. package/src/adapters/__tests__/scheduler-adapter-factory.test.js +177 -37
  5. package/src/adapters/aws-scheduler-adapter.js +5 -12
  6. package/src/adapters/scheduler-adapter-factory.js +34 -14
  7. package/src/application/__tests__/admin-frigg-commands.test.js +19 -19
  8. package/src/application/__tests__/admin-script-base.test.js +84 -100
  9. package/src/application/__tests__/dry-run-http-interceptor.test.js +313 -0
  10. package/src/application/__tests__/dry-run-repository-wrapper.test.js +257 -0
  11. package/src/application/__tests__/schedule-management-use-case.test.js +276 -0
  12. package/src/application/__tests__/script-runner.test.js +17 -33
  13. package/src/application/admin-frigg-commands.js +32 -20
  14. package/src/application/admin-script-base.js +99 -20
  15. package/src/application/dry-run-http-interceptor.js +296 -0
  16. package/src/application/dry-run-repository-wrapper.js +261 -0
  17. package/src/application/schedule-management-use-case.js +230 -0
  18. package/src/application/script-runner.js +156 -46
  19. package/src/builtins/__tests__/integration-health-check.test.js +60 -67
  20. package/src/builtins/__tests__/oauth-token-refresh.test.js +37 -45
  21. package/src/builtins/integration-health-check.js +24 -23
  22. package/src/builtins/oauth-token-refresh.js +20 -19
  23. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +95 -32
  24. package/src/infrastructure/__tests__/admin-script-router.test.js +49 -48
  25. package/src/infrastructure/admin-auth-middleware.js +43 -5
  26. package/src/infrastructure/admin-script-router.js +44 -64
  27. package/src/infrastructure/script-executor-handler.js +36 -15
  28. package/src/application/__tests__/validate-script-input.test.js +0 -196
  29. package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +0 -168
  30. package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +0 -114
  31. package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +0 -201
  32. package/src/application/use-cases/delete-schedule-use-case.js +0 -108
  33. package/src/application/use-cases/get-effective-schedule-use-case.js +0 -78
  34. package/src/application/use-cases/index.js +0 -18
  35. package/src/application/use-cases/upsert-schedule-use-case.js +0 -127
  36. package/src/application/validate-script-input.js +0 -116
package/index.js CHANGED
@@ -8,17 +8,11 @@
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 {
12
- AdminScriptContext,
13
- createAdminScriptContext,
14
- // Legacy aliases (deprecated)
15
- AdminFriggCommands,
16
- createAdminFriggCommands,
17
- } = require('./src/application/admin-frigg-commands');
11
+ const { AdminFriggCommands, createAdminFriggCommands } = require('./src/application/admin-frigg-commands');
18
12
  const { ScriptRunner, createScriptRunner } = require('./src/application/script-runner');
19
13
 
20
14
  // Infrastructure
21
- const { validateAdminApiKey } = require('./src/infrastructure/admin-auth-middleware');
15
+ const { adminAuthMiddleware } = require('./src/infrastructure/admin-auth-middleware');
22
16
  const { router, app, handler: routerHandler } = require('./src/infrastructure/admin-script-router');
23
17
  const { handler: executorHandler } = require('./src/infrastructure/script-executor-handler');
24
18
 
@@ -36,6 +30,7 @@ const { AWSSchedulerAdapter } = require('./src/adapters/aws-scheduler-adapter');
36
30
  const { LocalSchedulerAdapter } = require('./src/adapters/local-scheduler-adapter');
37
31
  const {
38
32
  createSchedulerAdapter,
33
+ detectSchedulerAdapterType,
39
34
  } = require('./src/adapters/scheduler-adapter-factory');
40
35
 
41
36
  module.exports = {
@@ -44,16 +39,13 @@ module.exports = {
44
39
  ScriptFactory,
45
40
  getScriptFactory,
46
41
  createScriptFactory,
47
- AdminScriptContext,
48
- createAdminScriptContext,
49
- // Legacy aliases (deprecated)
50
42
  AdminFriggCommands,
51
43
  createAdminFriggCommands,
52
44
  ScriptRunner,
53
45
  createScriptRunner,
54
46
 
55
47
  // Infrastructure layer
56
- validateAdminApiKey,
48
+ adminAuthMiddleware,
57
49
  router,
58
50
  app,
59
51
  routerHandler,
@@ -70,4 +62,5 @@ module.exports = {
70
62
  AWSSchedulerAdapter,
71
63
  LocalSchedulerAdapter,
72
64
  createSchedulerAdapter,
65
+ detectSchedulerAdapterType,
73
66
  };
package/package.json CHANGED
@@ -1,24 +1,27 @@
1
1
  {
2
2
  "name": "@friggframework/admin-scripts",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.517.9066dd6.0",
4
+ "version": "2.0.0--canary.522.923dfae.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.9066dd6.0",
8
+ "@friggframework/core": "2.0.0--canary.522.923dfae.0",
9
9
  "bcryptjs": "^2.4.3",
10
10
  "express": "^4.18.2",
11
11
  "lodash": "4.17.21",
12
+ "mongoose": "6.11.6",
12
13
  "serverless-http": "^3.2.0",
13
14
  "uuid": "^9.0.1"
14
15
  },
15
16
  "devDependencies": {
16
- "@friggframework/eslint-config": "2.0.0--canary.517.9066dd6.0",
17
- "@friggframework/prettier-config": "2.0.0--canary.517.9066dd6.0",
18
- "@friggframework/test": "2.0.0--canary.517.9066dd6.0",
17
+ "@friggframework/eslint-config": "2.0.0--canary.522.923dfae.0",
18
+ "@friggframework/prettier-config": "2.0.0--canary.522.923dfae.0",
19
+ "@friggframework/test": "2.0.0--canary.522.923dfae.0",
20
+ "chai": "^4.3.6",
19
21
  "eslint": "^8.22.0",
20
22
  "jest": "^29.7.0",
21
23
  "prettier": "^2.7.1",
24
+ "sinon": "^16.1.1",
22
25
  "supertest": "^7.1.4"
23
26
  },
24
27
  "scripts": {
@@ -46,5 +49,5 @@
46
49
  "maintenance",
47
50
  "operations"
48
51
  ],
49
- "gitHead": "9066dd6181d4fb9a1ec3d046c3f0de4ebba651a8"
52
+ "gitHead": "923dfaebed139e79450165c5e2ecf11ef7cbcd40"
50
53
  }
@@ -18,28 +18,34 @@ 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
-
27
21
  describe('AWSSchedulerAdapter', () => {
28
22
  let adapter;
29
23
  let mockSend;
30
- const originalEnv = process.env;
24
+ let originalEnv;
25
+
26
+ beforeAll(() => {
27
+ originalEnv = { ...process.env };
28
+ });
31
29
 
32
30
  beforeEach(() => {
33
31
  jest.clearAllMocks();
34
- process.env = { ...originalEnv, AWS_REGION: 'us-east-1' };
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';
35
38
 
36
39
  const sdk = require('@aws-sdk/client-scheduler');
37
40
  mockSend = sdk._mockSend;
38
41
 
39
- adapter = new AWSSchedulerAdapter({ ...defaultParams });
42
+ adapter = new AWSSchedulerAdapter({
43
+ targetLambdaArn: 'arn:aws:lambda:us-east-1:123456789012:function:admin-script-executor',
44
+ scheduleGroupName: 'frigg-admin-scripts',
45
+ });
40
46
  });
41
47
 
42
- afterEach(() => {
48
+ afterAll(() => {
43
49
  process.env = originalEnv;
44
50
  });
45
51
 
@@ -54,46 +60,35 @@ describe('AWSSchedulerAdapter', () => {
54
60
  });
55
61
 
56
62
  describe('Constructor', () => {
57
- it('should use provided configuration and AWS_REGION from env', () => {
58
- process.env.AWS_REGION = 'eu-west-1';
63
+ it('should use provided configuration', () => {
59
64
  const customAdapter = new AWSSchedulerAdapter({
65
+ region: 'eu-west-1',
60
66
  targetLambdaArn: 'arn:aws:lambda:eu-west-1:123456789012:function:custom',
61
67
  scheduleGroupName: 'custom-group',
62
- roleArn: 'arn:aws:iam::123456789012:role/custom-role',
63
68
  });
64
69
 
65
70
  expect(customAdapter.region).toBe('eu-west-1');
66
71
  expect(customAdapter.targetLambdaArn).toBe('arn:aws:lambda:eu-west-1:123456789012:function:custom');
67
72
  expect(customAdapter.scheduleGroupName).toBe('custom-group');
68
- expect(customAdapter.roleArn).toBe('arn:aws:iam::123456789012:role/custom-role');
69
73
  });
70
74
 
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');
76
- });
75
+ it('should use environment variables as fallback', () => {
76
+ const envAdapter = new AWSSchedulerAdapter();
77
77
 
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');
78
+ expect(envAdapter.region).toBe('us-east-1');
79
+ expect(envAdapter.targetLambdaArn).toBe('arn:aws:lambda:us-east-1:123456789012:function:test-executor');
80
+ expect(envAdapter.scheduleGroupName).toBe('test-schedule-group');
83
81
  });
84
82
 
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
- });
83
+ it('should use defaults when no config or env vars', () => {
84
+ delete process.env.AWS_REGION;
85
+ delete process.env.SCHEDULE_GROUP_NAME;
86
+ delete process.env.ADMIN_SCRIPT_LAMBDA_ARN;
87
+
88
+ const defaultAdapter = new AWSSchedulerAdapter();
91
89
 
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');
90
+ expect(defaultAdapter.region).toBe('us-east-1');
91
+ expect(defaultAdapter.scheduleGroupName).toBe('frigg-admin-scripts');
97
92
  });
98
93
  });
99
94
 
@@ -144,7 +139,7 @@ describe('AWSSchedulerAdapter', () => {
144
139
  });
145
140
  });
146
141
 
147
- it('should configure target with Lambda ARN and constructor roleArn', async () => {
142
+ it('should configure target with Lambda ARN and role', async () => {
148
143
  mockSend.mockResolvedValue({
149
144
  ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
150
145
  });
@@ -159,26 +154,6 @@ describe('AWSSchedulerAdapter', () => {
159
154
  expect(command.params.Target.RoleArn).toBe('arn:aws:iam::123456789012:role/test-role');
160
155
  });
161
156
 
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
-
182
157
  it('should enable schedule by default', async () => {
183
158
  mockSend.mockResolvedValue({
184
159
  ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
@@ -326,7 +301,9 @@ describe('AWSSchedulerAdapter', () => {
326
301
 
327
302
  describe('Lazy SDK loading', () => {
328
303
  it('should load AWS SDK on first client access', () => {
329
- const newAdapter = new AWSSchedulerAdapter({ ...defaultParams });
304
+ const newAdapter = new AWSSchedulerAdapter({
305
+ targetLambdaArn: 'arn:aws:lambda:us-east-1:123456789012:function:test',
306
+ });
330
307
 
331
308
  expect(newAdapter.scheduler).toBeNull();
332
309
 
@@ -1,4 +1,7 @@
1
- const { createSchedulerAdapter } = require('../scheduler-adapter-factory');
1
+ const {
2
+ createSchedulerAdapter,
3
+ detectSchedulerAdapterType,
4
+ } = require('../scheduler-adapter-factory');
2
5
  const { AWSSchedulerAdapter } = require('../aws-scheduler-adapter');
3
6
  const { LocalSchedulerAdapter } = require('../local-scheduler-adapter');
4
7
 
@@ -14,56 +17,71 @@ jest.mock('@aws-sdk/client-scheduler', () => ({
14
17
  ListSchedulesCommand: jest.fn(),
15
18
  }));
16
19
 
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
-
23
20
  describe('Scheduler Adapter Factory', () => {
24
- const originalEnv = process.env;
21
+ let originalEnv;
22
+
23
+ beforeAll(() => {
24
+ originalEnv = { ...process.env };
25
+ });
25
26
 
26
27
  beforeEach(() => {
27
- process.env = { ...originalEnv, AWS_REGION: 'us-east-1' };
28
+ // Reset environment variables
29
+ delete process.env.SCHEDULER_ADAPTER;
30
+ delete process.env.STAGE;
31
+ delete process.env.NODE_ENV;
28
32
  });
29
33
 
30
- afterEach(() => {
34
+ afterAll(() => {
31
35
  process.env = originalEnv;
32
36
  });
33
37
 
34
38
  describe('createSchedulerAdapter()', () => {
35
- it('should throw if type is not provided', () => {
36
- expect(() => createSchedulerAdapter()).toThrow();
37
- });
39
+ it('should create local adapter by default', () => {
40
+ const adapter = createSchedulerAdapter();
38
41
 
39
- it('should throw if type is not provided in options object', () => {
40
- expect(() => createSchedulerAdapter({})).toThrow();
42
+ expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
43
+ expect(adapter.getName()).toBe('local-cron');
41
44
  });
42
45
 
43
- it('should create local adapter when type is "local"', () => {
46
+ it('should create local adapter when explicitly specified', () => {
44
47
  const adapter = createSchedulerAdapter({ type: 'local' });
45
48
 
46
49
  expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
47
- expect(adapter.getName()).toBe('local-cron');
48
50
  });
49
51
 
50
52
  it('should create AWS adapter when type is "aws"', () => {
51
- const adapter = createSchedulerAdapter({ type: 'aws', ...awsAdapterParams });
53
+ const adapter = createSchedulerAdapter({ type: 'aws' });
52
54
 
53
55
  expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
54
56
  expect(adapter.getName()).toBe('aws-eventbridge-scheduler');
55
57
  });
56
58
 
57
59
  it('should create AWS adapter when type is "eventbridge"', () => {
58
- const adapter = createSchedulerAdapter({ type: 'eventbridge', ...awsAdapterParams });
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();
59
69
 
60
70
  expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
61
71
  });
62
72
 
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
+
63
81
  it('should handle case-insensitive type values', () => {
64
- const adapter1 = createSchedulerAdapter({ type: 'AWS', ...awsAdapterParams });
82
+ const adapter1 = createSchedulerAdapter({ type: 'AWS' });
65
83
  const adapter2 = createSchedulerAdapter({ type: 'LOCAL' });
66
- const adapter3 = createSchedulerAdapter({ type: 'EventBridge', ...awsAdapterParams });
84
+ const adapter3 = createSchedulerAdapter({ type: 'EventBridge' });
67
85
 
68
86
  expect(adapter1).toBeInstanceOf(AWSSchedulerAdapter);
69
87
  expect(adapter2).toBeInstanceOf(LocalSchedulerAdapter);
@@ -73,29 +91,17 @@ describe('Scheduler Adapter Factory', () => {
73
91
  it('should pass AWS configuration to AWS adapter', () => {
74
92
  const config = {
75
93
  type: 'aws',
94
+ region: 'eu-west-1',
76
95
  targetLambdaArn: 'arn:aws:lambda:eu-west-1:123456789012:function:test',
77
96
  scheduleGroupName: 'custom-group',
78
- roleArn: 'arn:aws:iam::123456789012:role/custom-role',
79
97
  };
80
98
 
81
99
  const adapter = createSchedulerAdapter(config);
82
100
 
83
101
  expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
84
- expect(adapter.region).toBe('us-east-1'); // From process.env.AWS_REGION
102
+ expect(adapter.region).toBe('eu-west-1');
85
103
  expect(adapter.targetLambdaArn).toBe('arn:aws:lambda:eu-west-1:123456789012:function:test');
86
104
  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');
99
105
  });
100
106
 
101
107
  it('should ignore AWS config for local adapter', () => {
@@ -110,8 +116,142 @@ describe('Scheduler Adapter Factory', () => {
110
116
  expect(adapter.region).toBeUndefined();
111
117
  });
112
118
 
113
- it('should throw for unknown adapter type', () => {
114
- expect(() => createSchedulerAdapter({ type: 'unknown-type' })).toThrow();
119
+ it('should handle unknown adapter type by creating local adapter', () => {
120
+ const adapter = createSchedulerAdapter({ type: 'unknown-type' });
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);
115
255
  });
116
256
  });
117
257
  });
@@ -25,19 +25,12 @@ function loadSchedulerSDK() {
25
25
  * Supports cron expressions, timezone configuration, and Lambda invocation.
26
26
  */
27
27
  class AWSSchedulerAdapter extends SchedulerAdapter {
28
- constructor({ credentials, targetLambdaArn, scheduleGroupName, roleArn } = {}) {
28
+ constructor({ region, credentials, targetLambdaArn, scheduleGroupName } = {}) {
29
29
  super();
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;
30
+ this.region = region || process.env.AWS_REGION || 'us-east-1';
37
31
  this.credentials = credentials;
38
- this.targetLambdaArn = targetLambdaArn;
39
- this.scheduleGroupName = scheduleGroupName;
40
- this.roleArn = roleArn;
32
+ this.targetLambdaArn = targetLambdaArn || process.env.ADMIN_SCRIPT_LAMBDA_ARN;
33
+ this.scheduleGroupName = scheduleGroupName || process.env.SCHEDULE_GROUP_NAME || 'frigg-admin-scripts';
41
34
  this.scheduler = null;
42
35
  }
43
36
 
@@ -68,7 +61,7 @@ class AWSSchedulerAdapter extends SchedulerAdapter {
68
61
  FlexibleTimeWindow: { Mode: 'OFF' },
69
62
  Target: {
70
63
  Arn: this.targetLambdaArn,
71
- RoleArn: this.roleArn,
64
+ RoleArn: process.env.SCHEDULER_ROLE_ARN,
72
65
  Input: JSON.stringify({
73
66
  scriptName,
74
67
  trigger: 'SCHEDULED',
@@ -6,44 +6,64 @@ const { LocalSchedulerAdapter } = require('./local-scheduler-adapter');
6
6
  *
7
7
  * Application Layer - Hexagonal Architecture
8
8
  *
9
- * Creates the appropriate scheduler adapter based on explicit configuration
10
- * from appDefinition. Does not auto-detect or read environment variables.
9
+ * Creates the appropriate scheduler adapter based on configuration.
10
+ * Supports environment-based auto-detection and explicit configuration.
11
11
  */
12
12
 
13
13
  /**
14
14
  * Create a scheduler adapter instance
15
15
  *
16
- * @param {Object} options - Configuration options (from appDefinition.adminScripts.scheduler)
17
- * @param {string} options.type - Adapter type ('aws', 'eventbridge', 'local') - required
16
+ * @param {Object} options - Configuration options
17
+ * @param {string} [options.type] - Adapter type ('aws', 'eventbridge', 'local')
18
+ * @param {string} [options.region] - AWS region (for AWS adapter)
18
19
  * @param {Object} [options.credentials] - AWS credentials (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)
20
+ * @param {string} [options.targetLambdaArn] - Lambda ARN to invoke (for AWS adapter)
21
+ * @param {string} [options.scheduleGroupName] - EventBridge schedule group name (for AWS adapter)
22
22
  * @returns {SchedulerAdapter} Configured scheduler adapter
23
23
  */
24
24
  function createSchedulerAdapter(options = {}) {
25
- if (!options.type) {
26
- throw new Error('Scheduler adapter type is required. Configure in appDefinition.adminScripts.scheduler.type');
27
- }
25
+ const adapterType = options.type || detectSchedulerAdapterType();
28
26
 
29
- switch (options.type.toLowerCase()) {
27
+ switch (adapterType.toLowerCase()) {
30
28
  case 'aws':
31
29
  case 'eventbridge':
32
30
  return new AWSSchedulerAdapter({
31
+ region: options.region,
33
32
  credentials: options.credentials,
34
33
  targetLambdaArn: options.targetLambdaArn,
35
34
  scheduleGroupName: options.scheduleGroupName,
36
- roleArn: options.roleArn,
37
35
  });
38
36
 
39
37
  case 'local':
38
+ default:
40
39
  return new LocalSchedulerAdapter();
40
+ }
41
+ }
41
42
 
42
- default:
43
- throw new Error(`Unknown scheduler adapter type: ${options.type}`);
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
+
54
+ // Auto-detect based on environment
55
+ const stage = process.env.STAGE || process.env.NODE_ENV || 'local';
56
+
57
+ // Use AWS adapter in production/staging environments
58
+ if (['production', 'prod', 'staging', 'stage'].includes(stage.toLowerCase())) {
59
+ return 'aws';
44
60
  }
61
+
62
+ // Use local adapter for dev/test/local
63
+ return 'local';
45
64
  }
46
65
 
47
66
  module.exports = {
48
67
  createSchedulerAdapter,
68
+ detectSchedulerAdapterType,
49
69
  };