@friggframework/admin-scripts 2.0.0--canary.517.179491e.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/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@friggframework/admin-scripts",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.517.179491e.0",
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.517.179491e.0",
8
+ "@friggframework/core": "2.0.0--canary.517.1687d70.0",
9
9
  "express": "^4.18.2",
10
10
  "serverless-http": "^3.2.0"
11
11
  },
12
12
  "devDependencies": {
13
- "@friggframework/eslint-config": "2.0.0--canary.517.179491e.0",
14
- "@friggframework/prettier-config": "2.0.0--canary.517.179491e.0",
15
- "@friggframework/test": "2.0.0--canary.517.179491e.0",
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",
16
16
  "eslint": "^8.22.0",
17
17
  "jest": "^29.7.0",
18
18
  "prettier": "^2.7.1",
@@ -43,5 +43,5 @@
43
43
  "maintenance",
44
44
  "operations"
45
45
  ],
46
- "gitHead": "179491ee820576004873fdfdbe0e8e10a807098d"
46
+ "gitHead": "1687d704a5eed1b0de52275200a579399722813d"
47
47
  }
@@ -206,6 +206,43 @@ describe('AWSSchedulerAdapter', () => {
206
206
  const command = mockSend.mock.calls[0][0];
207
207
  expect(command.params.FlexibleTimeWindow).toEqual({ Mode: 'OFF' });
208
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
+ });
209
246
  });
210
247
 
211
248
  describe('deleteSchedule()', () => {
@@ -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.scriptName)).toContain('script-1');
214
- expect(schedules.map((s) => s.scriptName)).toContain('script-2');
215
- expect(schedules.map((s) => s.scriptName)).toContain('script-3');
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
- scriptName: 'test-script',
230
- cronExpression: '0 0 * * *',
231
- timezone: 'America/New_York',
232
- input: { key: 'value' },
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
 
@@ -60,7 +60,7 @@ class AWSSchedulerAdapter extends SchedulerAdapter {
60
60
  const client = this.getSchedulerClient();
61
61
  const scheduleName = `frigg-script-${scriptName}`;
62
62
 
63
- const command = new CreateScheduleCommand({
63
+ const scheduleParams = {
64
64
  Name: scheduleName,
65
65
  GroupName: this.scheduleGroupName,
66
66
  ScheduleExpression: cronExpression,
@@ -76,13 +76,24 @@ class AWSSchedulerAdapter extends SchedulerAdapter {
76
76
  }),
77
77
  },
78
78
  State: 'ENABLED',
79
- });
80
-
81
- const response = await client.send(command);
82
- return {
83
- scheduleArn: response.ScheduleArn,
84
- scheduleName: scheduleName,
85
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
+ }
86
97
  }
87
98
 
88
99
  async deleteSchedule(scriptName) {
@@ -57,7 +57,12 @@ class LocalSchedulerAdapter extends SchedulerAdapter {
57
57
  }
58
58
 
59
59
  async listSchedules() {
60
- return Array.from(this.schedules.values());
60
+ return Array.from(this.schedules.values()).map((schedule) => ({
61
+ Name: `frigg-script-${schedule.scriptName}`,
62
+ State: schedule.enabled ? 'ENABLED' : 'DISABLED',
63
+ ScheduleExpression: schedule.cronExpression,
64
+ ScheduleExpressionTimezone: schedule.timezone,
65
+ }));
61
66
  }
62
67
 
63
68
  async getSchedule(scriptName) {
@@ -1,4 +1,5 @@
1
1
  const { createScriptRunner } = require('../application/script-runner');
2
+ const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
2
3
 
3
4
  /**
4
5
  * SQS Queue Worker Lambda Handler
@@ -12,11 +13,12 @@ async function handler(event) {
12
13
 
13
14
  for (const record of event.Records) {
14
15
  let scriptName;
16
+ let executionId;
15
17
 
16
18
  try {
17
19
  const message = JSON.parse(record.body);
18
- ({ scriptName } = message);
19
- const { executionId, trigger, params } = message;
20
+ ({ scriptName, executionId } = message);
21
+ const { trigger, params } = message;
20
22
 
21
23
  if (!scriptName || !executionId) {
22
24
  throw new Error(`Invalid SQS message: missing scriptName or executionId`);
@@ -41,6 +43,25 @@ async function handler(event) {
41
43
  // Only reaches here for unexpected failures (message parse errors, runner construction).
42
44
  // Script execution errors are handled by ScriptRunner and returned as { status: 'FAILED' }.
43
45
  console.error(`Unexpected error processing record:`, error);
46
+
47
+ // If we have an executionId, mark the admin process as FAILED
48
+ // so the record doesn't stay stuck in a non-terminal state.
49
+ if (executionId) {
50
+ try {
51
+ const commands = createAdminScriptCommands();
52
+ await commands.completeAdminProcess(executionId, {
53
+ state: 'FAILED',
54
+ error: {
55
+ name: error.name,
56
+ message: error.message,
57
+ stack: error.stack,
58
+ },
59
+ });
60
+ } catch (updateError) {
61
+ console.error(`Failed to update execution ${executionId} state:`, updateError);
62
+ }
63
+ }
64
+
44
65
  results.push({
45
66
  scriptName: scriptName || 'unknown',
46
67
  status: 'FAILED',