@friggframework/admin-scripts 2.0.0--canary.517.300ded3.0 → 2.0.0--canary.517.35ee143.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.
@@ -1,276 +0,0 @@
1
- const { ScheduleManagementUseCase } = require('../schedule-management-use-case');
2
-
3
- describe('ScheduleManagementUseCase', () => {
4
- let useCase;
5
- let mockCommands;
6
- let mockSchedulerAdapter;
7
- let mockScriptFactory;
8
-
9
- beforeEach(() => {
10
- mockCommands = {
11
- getScheduleByScriptName: jest.fn(),
12
- upsertSchedule: jest.fn(),
13
- updateScheduleAwsInfo: jest.fn(),
14
- deleteSchedule: jest.fn(),
15
- };
16
-
17
- mockSchedulerAdapter = {
18
- createSchedule: jest.fn(),
19
- deleteSchedule: jest.fn(),
20
- };
21
-
22
- mockScriptFactory = {
23
- has: jest.fn(),
24
- get: jest.fn(),
25
- };
26
-
27
- useCase = new ScheduleManagementUseCase({
28
- commands: mockCommands,
29
- schedulerAdapter: mockSchedulerAdapter,
30
- scriptFactory: mockScriptFactory,
31
- });
32
- });
33
-
34
- describe('getEffectiveSchedule', () => {
35
- it('should return database schedule when override exists', async () => {
36
- const dbSchedule = {
37
- scriptName: 'test-script',
38
- enabled: true,
39
- cronExpression: '0 9 * * *',
40
- timezone: 'UTC',
41
- };
42
-
43
- mockScriptFactory.has.mockReturnValue(true);
44
- mockScriptFactory.get.mockReturnValue({ Definition: {} });
45
- mockCommands.getScheduleByScriptName.mockResolvedValue(dbSchedule);
46
-
47
- const result = await useCase.getEffectiveSchedule('test-script');
48
-
49
- expect(result.source).toBe('database');
50
- expect(result.schedule).toEqual(dbSchedule);
51
- });
52
-
53
- it('should return definition schedule when no database override', async () => {
54
- const definitionSchedule = {
55
- enabled: true,
56
- cronExpression: '0 12 * * *',
57
- timezone: 'America/New_York',
58
- };
59
-
60
- mockScriptFactory.has.mockReturnValue(true);
61
- mockScriptFactory.get.mockReturnValue({
62
- Definition: { schedule: definitionSchedule },
63
- });
64
- mockCommands.getScheduleByScriptName.mockResolvedValue(null);
65
-
66
- const result = await useCase.getEffectiveSchedule('test-script');
67
-
68
- expect(result.source).toBe('definition');
69
- expect(result.schedule.enabled).toBe(true);
70
- expect(result.schedule.cronExpression).toBe('0 12 * * *');
71
- });
72
-
73
- it('should return none when no schedule configured', async () => {
74
- mockScriptFactory.has.mockReturnValue(true);
75
- mockScriptFactory.get.mockReturnValue({ Definition: {} });
76
- mockCommands.getScheduleByScriptName.mockResolvedValue(null);
77
-
78
- const result = await useCase.getEffectiveSchedule('test-script');
79
-
80
- expect(result.source).toBe('none');
81
- expect(result.schedule.enabled).toBe(false);
82
- });
83
-
84
- it('should throw error when script not found', async () => {
85
- mockScriptFactory.has.mockReturnValue(false);
86
-
87
- await expect(useCase.getEffectiveSchedule('non-existent'))
88
- .rejects.toThrow('Script "non-existent" not found');
89
- });
90
- });
91
-
92
- describe('upsertSchedule', () => {
93
- it('should create schedule and provision EventBridge when enabled', async () => {
94
- const savedSchedule = {
95
- scriptName: 'test-script',
96
- enabled: true,
97
- cronExpression: '0 12 * * *',
98
- timezone: 'UTC',
99
- };
100
-
101
- mockScriptFactory.has.mockReturnValue(true);
102
- mockCommands.upsertSchedule.mockResolvedValue(savedSchedule);
103
- mockSchedulerAdapter.createSchedule.mockResolvedValue({
104
- scheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
105
- scheduleName: 'frigg-script-test-script',
106
- });
107
- mockCommands.updateScheduleAwsInfo.mockResolvedValue({
108
- ...savedSchedule,
109
- awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
110
- });
111
-
112
- const result = await useCase.upsertSchedule('test-script', {
113
- enabled: true,
114
- cronExpression: '0 12 * * *',
115
- timezone: 'UTC',
116
- });
117
-
118
- expect(result.success).toBe(true);
119
- expect(result.schedule.scriptName).toBe('test-script');
120
- expect(mockSchedulerAdapter.createSchedule).toHaveBeenCalledWith({
121
- scriptName: 'test-script',
122
- cronExpression: '0 12 * * *',
123
- timezone: 'UTC',
124
- });
125
- expect(mockCommands.updateScheduleAwsInfo).toHaveBeenCalled();
126
- });
127
-
128
- it('should delete EventBridge schedule when disabling', async () => {
129
- const existingSchedule = {
130
- scriptName: 'test-script',
131
- enabled: false,
132
- cronExpression: null,
133
- timezone: 'UTC',
134
- awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
135
- };
136
-
137
- mockScriptFactory.has.mockReturnValue(true);
138
- mockCommands.upsertSchedule.mockResolvedValue(existingSchedule);
139
- mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
140
- mockCommands.updateScheduleAwsInfo.mockResolvedValue({
141
- ...existingSchedule,
142
- awsScheduleArn: null,
143
- });
144
-
145
- const result = await useCase.upsertSchedule('test-script', {
146
- enabled: false,
147
- });
148
-
149
- expect(result.success).toBe(true);
150
- expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
151
- });
152
-
153
- it('should handle scheduler errors gracefully', async () => {
154
- const savedSchedule = {
155
- scriptName: 'test-script',
156
- enabled: true,
157
- cronExpression: '0 12 * * *',
158
- timezone: 'UTC',
159
- };
160
-
161
- mockScriptFactory.has.mockReturnValue(true);
162
- mockCommands.upsertSchedule.mockResolvedValue(savedSchedule);
163
- mockSchedulerAdapter.createSchedule.mockRejectedValue(
164
- new Error('AWS Scheduler API error')
165
- );
166
-
167
- const result = await useCase.upsertSchedule('test-script', {
168
- enabled: true,
169
- cronExpression: '0 12 * * *',
170
- });
171
-
172
- // Should succeed with warning, not fail
173
- expect(result.success).toBe(true);
174
- expect(result.schedulerWarning).toBe('AWS Scheduler API error');
175
- });
176
-
177
- it('should throw error when script not found', async () => {
178
- mockScriptFactory.has.mockReturnValue(false);
179
-
180
- await expect(useCase.upsertSchedule('non-existent', { enabled: true }))
181
- .rejects.toThrow('Script "non-existent" not found');
182
- });
183
-
184
- it('should throw error when enabled without cronExpression', async () => {
185
- mockScriptFactory.has.mockReturnValue(true);
186
-
187
- await expect(useCase.upsertSchedule('test-script', { enabled: true }))
188
- .rejects.toThrow('cronExpression is required when enabled is true');
189
- });
190
- });
191
-
192
- describe('deleteSchedule', () => {
193
- it('should delete schedule and EventBridge rule', async () => {
194
- const deletedSchedule = {
195
- scriptName: 'test-script',
196
- awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
197
- };
198
-
199
- mockScriptFactory.has.mockReturnValue(true);
200
- mockScriptFactory.get.mockReturnValue({ Definition: {} });
201
- mockCommands.deleteSchedule.mockResolvedValue({
202
- deletedCount: 1,
203
- deleted: deletedSchedule,
204
- });
205
- mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
206
-
207
- const result = await useCase.deleteSchedule('test-script');
208
-
209
- expect(result.success).toBe(true);
210
- expect(result.deletedCount).toBe(1);
211
- expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
212
- });
213
-
214
- it('should not call scheduler when no AWS rule exists', async () => {
215
- mockScriptFactory.has.mockReturnValue(true);
216
- mockScriptFactory.get.mockReturnValue({ Definition: {} });
217
- mockCommands.deleteSchedule.mockResolvedValue({
218
- deletedCount: 1,
219
- deleted: { scriptName: 'test-script' }, // No awsScheduleArn
220
- });
221
-
222
- const result = await useCase.deleteSchedule('test-script');
223
-
224
- expect(result.success).toBe(true);
225
- expect(mockSchedulerAdapter.deleteSchedule).not.toHaveBeenCalled();
226
- });
227
-
228
- it('should handle scheduler delete errors gracefully', async () => {
229
- mockScriptFactory.has.mockReturnValue(true);
230
- mockScriptFactory.get.mockReturnValue({ Definition: {} });
231
- mockCommands.deleteSchedule.mockResolvedValue({
232
- deletedCount: 1,
233
- deleted: {
234
- scriptName: 'test-script',
235
- awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
236
- },
237
- });
238
- mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
239
- new Error('AWS delete failed')
240
- );
241
-
242
- const result = await useCase.deleteSchedule('test-script');
243
-
244
- expect(result.success).toBe(true);
245
- expect(result.schedulerWarning).toBe('AWS delete failed');
246
- });
247
-
248
- it('should return effective schedule after deletion', async () => {
249
- const definitionSchedule = {
250
- enabled: true,
251
- cronExpression: '0 6 * * *',
252
- };
253
-
254
- mockScriptFactory.has.mockReturnValue(true);
255
- mockScriptFactory.get.mockReturnValue({
256
- Definition: { schedule: definitionSchedule },
257
- });
258
- mockCommands.deleteSchedule.mockResolvedValue({
259
- deletedCount: 1,
260
- deleted: { scriptName: 'test-script' },
261
- });
262
-
263
- const result = await useCase.deleteSchedule('test-script');
264
-
265
- expect(result.effectiveSchedule.source).toBe('definition');
266
- expect(result.effectiveSchedule.enabled).toBe(true);
267
- });
268
-
269
- it('should throw error when script not found', async () => {
270
- mockScriptFactory.has.mockReturnValue(false);
271
-
272
- await expect(useCase.deleteSchedule('non-existent'))
273
- .rejects.toThrow('Script "non-existent" not found');
274
- });
275
- });
276
- });
@@ -1,230 +0,0 @@
1
- /**
2
- * Schedule Management Use Case
3
- *
4
- * Application Layer - Hexagonal Architecture
5
- *
6
- * Orchestrates schedule management operations:
7
- * - Get effective schedule (DB override > Definition > none)
8
- * - Upsert schedule with EventBridge provisioning
9
- * - Delete schedule with EventBridge cleanup
10
- *
11
- * This use case encapsulates the business logic that was previously
12
- * embedded in the router, reducing cognitive complexity and improving testability.
13
- */
14
- class ScheduleManagementUseCase {
15
- constructor({ commands, schedulerAdapter, scriptFactory }) {
16
- this.commands = commands;
17
- this.schedulerAdapter = schedulerAdapter;
18
- this.scriptFactory = scriptFactory;
19
- }
20
-
21
- /**
22
- * Validate that a script exists
23
- * @private
24
- */
25
- _validateScriptExists(scriptName) {
26
- if (!this.scriptFactory.has(scriptName)) {
27
- const error = new Error(`Script "${scriptName}" not found`);
28
- error.code = 'SCRIPT_NOT_FOUND';
29
- throw error;
30
- }
31
- }
32
-
33
- /**
34
- * Get the definition schedule from a script class
35
- * @private
36
- */
37
- _getDefinitionSchedule(scriptName) {
38
- const scriptClass = this.scriptFactory.get(scriptName);
39
- return scriptClass.Definition?.schedule || null;
40
- }
41
-
42
- /**
43
- * Get effective schedule (DB override > Definition default > none)
44
- */
45
- async getEffectiveSchedule(scriptName) {
46
- this._validateScriptExists(scriptName);
47
-
48
- // Check database override first
49
- const dbSchedule = await this.commands.getScheduleByScriptName(scriptName);
50
- if (dbSchedule) {
51
- return {
52
- source: 'database',
53
- schedule: dbSchedule,
54
- };
55
- }
56
-
57
- // Check definition default
58
- const definitionSchedule = this._getDefinitionSchedule(scriptName);
59
- if (definitionSchedule?.enabled) {
60
- return {
61
- source: 'definition',
62
- schedule: {
63
- scriptName,
64
- enabled: definitionSchedule.enabled,
65
- cronExpression: definitionSchedule.cronExpression,
66
- timezone: definitionSchedule.timezone || 'UTC',
67
- },
68
- };
69
- }
70
-
71
- // No schedule configured
72
- return {
73
- source: 'none',
74
- schedule: {
75
- scriptName,
76
- enabled: false,
77
- },
78
- };
79
- }
80
-
81
- /**
82
- * Create or update schedule with EventBridge provisioning
83
- */
84
- async upsertSchedule(scriptName, { enabled, cronExpression, timezone }) {
85
- this._validateScriptExists(scriptName);
86
- this._validateScheduleInput(enabled, cronExpression);
87
-
88
- // Save to database
89
- const schedule = await this.commands.upsertSchedule({
90
- scriptName,
91
- enabled,
92
- cronExpression: cronExpression || null,
93
- timezone: timezone || 'UTC',
94
- });
95
-
96
- // Provision/deprovision EventBridge
97
- const schedulerResult = await this._syncEventBridgeSchedule(
98
- scriptName,
99
- enabled,
100
- cronExpression,
101
- timezone,
102
- schedule.awsScheduleArn
103
- );
104
-
105
- return {
106
- success: true,
107
- schedule: {
108
- ...schedule,
109
- awsScheduleArn: schedulerResult.awsScheduleArn || schedule.awsScheduleArn,
110
- awsScheduleName: schedulerResult.awsScheduleName || schedule.awsScheduleName,
111
- },
112
- ...(schedulerResult.warning && { schedulerWarning: schedulerResult.warning }),
113
- };
114
- }
115
-
116
- /**
117
- * Validate schedule input
118
- * @private
119
- */
120
- _validateScheduleInput(enabled, cronExpression) {
121
- if (typeof enabled !== 'boolean') {
122
- const error = new Error('enabled must be a boolean');
123
- error.code = 'INVALID_INPUT';
124
- throw error;
125
- }
126
-
127
- if (enabled && !cronExpression) {
128
- const error = new Error('cronExpression is required when enabled is true');
129
- error.code = 'INVALID_INPUT';
130
- throw error;
131
- }
132
- }
133
-
134
- /**
135
- * Sync EventBridge schedule based on enabled state
136
- * @private
137
- */
138
- async _syncEventBridgeSchedule(scriptName, enabled, cronExpression, timezone, existingArn) {
139
- const result = { awsScheduleArn: null, awsScheduleName: null, warning: null };
140
-
141
- try {
142
- if (enabled && cronExpression) {
143
- // Create/update EventBridge schedule
144
- const awsInfo = await this.schedulerAdapter.createSchedule({
145
- scriptName,
146
- cronExpression,
147
- timezone: timezone || 'UTC',
148
- });
149
-
150
- if (awsInfo?.scheduleArn) {
151
- await this.commands.updateScheduleAwsInfo(scriptName, {
152
- awsScheduleArn: awsInfo.scheduleArn,
153
- awsScheduleName: awsInfo.scheduleName,
154
- });
155
- result.awsScheduleArn = awsInfo.scheduleArn;
156
- result.awsScheduleName = awsInfo.scheduleName;
157
- }
158
- } else if (!enabled && existingArn) {
159
- // Delete EventBridge schedule
160
- await this.schedulerAdapter.deleteSchedule(scriptName);
161
- await this.commands.updateScheduleAwsInfo(scriptName, {
162
- awsScheduleArn: null,
163
- awsScheduleName: null,
164
- });
165
- }
166
- } catch (error) {
167
- // Non-fatal: DB schedule is saved, AWS can be retried
168
- result.warning = error.message;
169
- }
170
-
171
- return result;
172
- }
173
-
174
- /**
175
- * Delete schedule override and cleanup EventBridge
176
- */
177
- async deleteSchedule(scriptName) {
178
- this._validateScriptExists(scriptName);
179
-
180
- // Delete from database
181
- const deleteResult = await this.commands.deleteSchedule(scriptName);
182
-
183
- // Cleanup EventBridge if needed
184
- const schedulerWarning = await this._cleanupEventBridgeSchedule(
185
- scriptName,
186
- deleteResult.deleted?.awsScheduleArn
187
- );
188
-
189
- // Get effective schedule after deletion
190
- const definitionSchedule = this._getDefinitionSchedule(scriptName);
191
- const effectiveSchedule = definitionSchedule?.enabled
192
- ? {
193
- source: 'definition',
194
- enabled: definitionSchedule.enabled,
195
- cronExpression: definitionSchedule.cronExpression,
196
- timezone: definitionSchedule.timezone || 'UTC',
197
- }
198
- : { source: 'none', enabled: false };
199
-
200
- return {
201
- success: true,
202
- deletedCount: deleteResult.deletedCount,
203
- message: deleteResult.deletedCount > 0
204
- ? 'Schedule override removed'
205
- : 'No schedule override found',
206
- effectiveSchedule,
207
- ...(schedulerWarning && { schedulerWarning }),
208
- };
209
- }
210
-
211
- /**
212
- * Cleanup EventBridge schedule if it exists
213
- * @private
214
- */
215
- async _cleanupEventBridgeSchedule(scriptName, awsScheduleArn) {
216
- if (!awsScheduleArn) {
217
- return null;
218
- }
219
-
220
- try {
221
- await this.schedulerAdapter.deleteSchedule(scriptName);
222
- return null;
223
- } catch (error) {
224
- // Non-fatal: DB is cleaned up, AWS can be retried
225
- return error.message;
226
- }
227
- }
228
- }
229
-
230
- module.exports = { ScheduleManagementUseCase };