@friggframework/admin-scripts 2.0.0--canary.545.b4ca16d.0 → 2.0.0--canary.517.8eaf5df.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 (45) hide show
  1. package/README.md +277 -0
  2. package/index.js +23 -6
  3. package/package.json +6 -6
  4. package/src/adapters/__tests__/aws-scheduler-adapter.test.js +106 -45
  5. package/src/adapters/__tests__/local-scheduler-adapter.test.js +24 -10
  6. package/src/adapters/__tests__/scheduler-adapter-factory.test.js +30 -9
  7. package/src/adapters/__tests__/scheduler-adapter.test.js +6 -2
  8. package/src/adapters/aws-scheduler-adapter.js +55 -28
  9. package/src/adapters/local-scheduler-adapter.js +2 -2
  10. package/src/adapters/scheduler-adapter-factory.js +3 -1
  11. package/src/adapters/scheduler-adapter.js +9 -3
  12. package/src/application/__tests__/admin-frigg-commands.test.js +73 -30
  13. package/src/application/__tests__/admin-script-base.test.js +23 -6
  14. package/src/application/__tests__/script-factory.test.js +30 -6
  15. package/src/application/__tests__/script-runner.test.js +113 -24
  16. package/src/application/__tests__/validate-script-input.test.js +54 -15
  17. package/src/application/admin-frigg-commands.js +21 -9
  18. package/src/application/admin-script-base.js +3 -2
  19. package/src/application/script-factory.js +3 -1
  20. package/src/application/script-runner.js +90 -48
  21. package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +16 -7
  22. package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +9 -4
  23. package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +21 -10
  24. package/src/application/use-cases/delete-schedule-use-case.js +6 -4
  25. package/src/application/use-cases/get-effective-schedule-use-case.js +3 -1
  26. package/src/application/use-cases/index.js +3 -1
  27. package/src/application/use-cases/upsert-schedule-use-case.js +30 -11
  28. package/src/application/validate-script-input.js +10 -3
  29. package/src/builtins/__tests__/integration-health-check.test.js +232 -127
  30. package/src/builtins/__tests__/oauth-token-refresh.test.js +128 -75
  31. package/src/builtins/index.js +1 -4
  32. package/src/builtins/integration-health-check.js +63 -30
  33. package/src/builtins/oauth-token-refresh.js +64 -29
  34. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +9 -3
  35. package/src/infrastructure/__tests__/admin-script-router.test.js +125 -52
  36. package/src/infrastructure/admin-auth-middleware.js +3 -1
  37. package/src/infrastructure/admin-script-router.js +129 -14
  38. package/src/infrastructure/bootstrap.js +87 -0
  39. package/src/infrastructure/script-executor-handler.js +119 -52
  40. package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
  41. package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
  42. package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
  43. package/src/application/dry-run-http-interceptor.js +0 -296
  44. package/src/application/dry-run-repository-wrapper.js +0 -261
  45. package/src/application/schedule-management-use-case.js +0 -230
package/README.md ADDED
@@ -0,0 +1,277 @@
1
+ # @friggframework/admin-scripts
2
+
3
+ Admin Script Runner for Frigg — write and run operational/maintenance scripts inside your deployed Frigg app, with VPC/KMS-secured database access, the same repositories your integrations use, sync or async (queued) execution, dry-run validation, and optional cron scheduling via AWS EventBridge Scheduler.
4
+
5
+ Typical use cases:
6
+
7
+ - **Healing scripts** — repair broken integration state (e.g. corrupted config).
8
+ - **Recurring maintenance** — refresh webhooks/subscriptions before they expire.
9
+ - **Built-in utilities** — OAuth token refresh, integration health checks.
10
+
11
+ > Admin scripts are a **high-privilege** surface. Every endpoint is protected by an admin API key (`x-frigg-admin-api-key`), scripts run in your private VPC subnets, and every execution is tracked in the `AdminProcess` table. Never expose the admin API key to browsers or end users.
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @friggframework/admin-scripts
19
+ ```
20
+
21
+ Then register scripts in your app definition (`backend/index.js`):
22
+
23
+ ```javascript
24
+ const {
25
+ Definition: HubSpotIntegration,
26
+ } = require('./src/integrations/HubSpotIntegration');
27
+ const {
28
+ AttioHealingScript,
29
+ } = require('./src/admin-scripts/AttioHealingScript');
30
+
31
+ const Definition = {
32
+ name: 'my-frigg-app',
33
+ integrations: [HubSpotIntegration],
34
+
35
+ // Admin scripts (optional)
36
+ adminScripts: [AttioHealingScript],
37
+
38
+ admin: {
39
+ includeBuiltinScripts: true, // register oauth-token-refresh + integration-health-check
40
+ enableScheduling: true, // provision EventBridge Scheduler resources
41
+ },
42
+ };
43
+
44
+ module.exports = { Definition };
45
+ ```
46
+
47
+ At deploy time the framework's `AdminScriptBuilder` provisions the SQS queue, the router + worker Lambdas, and (when `enableScheduling` is set) the EventBridge Scheduler group and IAM role. At runtime the router/worker load this app definition and register your scripts (plus the built-ins) into the script registry.
48
+
49
+ ---
50
+
51
+ ## Writing a script
52
+
53
+ Extend `AdminScriptBase`, declare a static `Definition`, and implement `execute(params)`. The execution context is injected for you and available as `this.context`.
54
+
55
+ ```javascript
56
+ const { AdminScriptBase } = require('@friggframework/admin-scripts');
57
+
58
+ class AttioHealingScript extends AdminScriptBase {
59
+ static Definition = {
60
+ name: 'attio-healing',
61
+ version: '1.0.0',
62
+ description: 'Repairs corrupted Attio integration config',
63
+ source: 'USER_DEFINED',
64
+
65
+ // JSON Schema — validated on /validate and before every execution
66
+ inputSchema: {
67
+ type: 'object',
68
+ required: ['integrationId'],
69
+ properties: {
70
+ integrationId: { type: 'string' },
71
+ dryRun: { type: 'boolean', default: false },
72
+ },
73
+ },
74
+
75
+ config: {
76
+ timeout: 300000, // ms; sync mode is capped (see below)
77
+ requireIntegrationInstance: true, // needs this.context.instantiate()
78
+ },
79
+
80
+ // Optional default schedule (can be overridden at runtime via the API)
81
+ schedule: { enabled: false, cronExpression: 'cron(0 12 * * ? *)' },
82
+
83
+ display: { category: 'maintenance' },
84
+ };
85
+
86
+ async execute(params) {
87
+ const { integrationId } = params;
88
+
89
+ this.context.log('info', 'Starting Attio healing', { integrationId });
90
+
91
+ const integration =
92
+ await this.context.integrationRepository.findIntegrationById(
93
+ integrationId
94
+ );
95
+ if (!integration) {
96
+ throw new Error(`Integration ${integrationId} not found`);
97
+ }
98
+
99
+ // Call the live integration/API when you need it
100
+ const instance = await this.context.instantiate(integrationId);
101
+ await instance.primary.api.refreshMetadata();
102
+
103
+ this.context.log('info', 'Healing complete', { integrationId });
104
+ return { healed: true, integrationId };
105
+ }
106
+ }
107
+
108
+ module.exports = { AttioHealingScript };
109
+ ```
110
+
111
+ ### The execution context (`this.context`)
112
+
113
+ | Member | Description |
114
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
115
+ | `log(level, message, data?)` | Records a structured log entry (`level`: `debug`/`info`/`warn`/`error`). Logs are persisted to the execution's `results.logs`. |
116
+ | `integrationRepository` | Read/query integrations. |
117
+ | `userRepository` | Read/query users. |
118
+ | `moduleRepository` | Read/query modules. |
119
+ | `credentialRepository` | Read credentials (decrypted transparently). |
120
+ | `instantiate(integrationId)` | Returns a hydrated integration instance for calling external APIs. Requires `config.requireIntegrationInstance: true`. |
121
+ | `queueScript(name, params?)` | Enqueue another script as a follow-up (tracked as a child of the current execution). |
122
+ | `queueScriptBatch(entries)` | Enqueue many follow-up scripts at once. |
123
+ | `getExecutionId()` | The current `AdminProcess` record id. |
124
+
125
+ Repositories return **plain, decrypted data** — field-level encryption is handled transparently.
126
+
127
+ ---
128
+
129
+ ## HTTP API
130
+
131
+ All routes are mounted under `/admin` and require the `x-frigg-admin-api-key` header.
132
+
133
+ | Method | Path | Description |
134
+ | -------- | ------------------------------------------------ | ------------------------------------------------ |
135
+ | `GET` | `/admin/scripts` | List registered scripts |
136
+ | `GET` | `/admin/scripts/{name}` | Get a script's details/schema |
137
+ | `POST` | `/admin/scripts/{name}` | Execute a script (`sync` or `async`) |
138
+ | `POST` | `/admin/scripts/{name}/validate` | Validate input against the schema (no execution) |
139
+ | `GET` | `/admin/scripts/{name}/executions` | List recent executions (`?status=`, `?limit=`) |
140
+ | `GET` | `/admin/scripts/{name}/executions/{executionId}` | Get one execution |
141
+ | `GET` | `/admin/scripts/{name}/schedule` | Get the effective schedule |
142
+ | `PUT` | `/admin/scripts/{name}/schedule` | Create/update a schedule override |
143
+ | `DELETE` | `/admin/scripts/{name}/schedule` | Remove the schedule override |
144
+
145
+ ### Execute a script
146
+
147
+ **Async (default)** — queued to SQS, returns immediately with an execution id to poll:
148
+
149
+ ```bash
150
+ curl -X POST https://<your-app>/admin/scripts/attio-healing \
151
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY" \
152
+ -H "Content-Type: application/json" \
153
+ -d '{ "params": { "integrationId": "abc123" }, "mode": "async" }'
154
+
155
+ # 202 Accepted
156
+ # { "executionId": "665f...", "status": "QUEUED", "scriptName": "attio-healing" }
157
+ ```
158
+
159
+ **Sync** — runs inline and returns the result. Only for fast scripts: in a deployed environment, sync is rejected (`400 SYNC_TIMEOUT_TOO_LONG`) when the script's `config.timeout` exceeds the API Lambda budget (~25s). Use `async` for anything longer.
160
+
161
+ ```bash
162
+ curl -X POST https://<your-app>/admin/scripts/integration-health-check \
163
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY" \
164
+ -H "Content-Type: application/json" \
165
+ -d '{ "params": {}, "mode": "sync" }'
166
+
167
+ # 200 OK
168
+ # { "executionId": "...", "status": "COMPLETED", "output": { ... }, "metrics": { "durationMs": 812 } }
169
+ ```
170
+
171
+ Invalid input is rejected up front:
172
+
173
+ ```bash
174
+ # 400 Bad Request → { "error": "Invalid input: Missing required parameter: integrationId", "code": "INVALID_INPUT" }
175
+ ```
176
+
177
+ ### Validate without executing
178
+
179
+ ```bash
180
+ curl -X POST https://<your-app>/admin/scripts/attio-healing/validate \
181
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY" \
182
+ -H "Content-Type: application/json" \
183
+ -d '{ "params": { "integrationId": "abc123" } }'
184
+
185
+ # { "status": "VALID", "preview": { ... }, "message": "Validation passed. ..." }
186
+ ```
187
+
188
+ ### Poll an execution
189
+
190
+ ```bash
191
+ curl https://<your-app>/admin/scripts/attio-healing/executions/665f... \
192
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
193
+
194
+ curl "https://<your-app>/admin/scripts/attio-healing/executions?status=FAILED&limit=20" \
195
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Scheduling
201
+
202
+ A script can ship a default schedule in its `Definition.schedule`, and operators can override it at runtime. The **effective** schedule resolves as: runtime override (DB) → definition default → none.
203
+
204
+ ```bash
205
+ # Enable a daily 6am UTC run
206
+ curl -X PUT https://<your-app>/admin/scripts/integration-health-check/schedule \
207
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY" \
208
+ -H "Content-Type: application/json" \
209
+ -d '{ "enabled": true, "cronExpression": "cron(0 6 * * ? *)", "timezone": "UTC" }'
210
+
211
+ # Inspect / remove
212
+ curl https://<your-app>/admin/scripts/integration-health-check/schedule -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
213
+ curl -X DELETE https://<your-app>/admin/scripts/integration-health-check/schedule -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
214
+ ```
215
+
216
+ Scheduling requires `admin.enableScheduling: true` in the app definition (so the EventBridge Scheduler group, IAM role, and env vars are provisioned). In a deployed environment the router refuses to fall back to the in-memory local scheduler, returning `503 SCHEDULER_NOT_CONFIGURED` if the provider isn't wired.
217
+
218
+ ---
219
+
220
+ ## Built-in scripts
221
+
222
+ Set `admin.includeBuiltinScripts: true` to register:
223
+
224
+ - **`oauth-token-refresh`** — refreshes OAuth tokens for integrations nearing expiry. Params: `integrationIds?`, `expiryThresholdHours` (default 24), `dryRun`.
225
+ - **`integration-health-check`** — checks credential validity and API connectivity. Params: `integrationIds?`, `checkCredentials` (default true), `checkConnectivity` (default true), `updateStatus` (default false).
226
+
227
+ ---
228
+
229
+ ## Script chaining
230
+
231
+ Long or fan-out work can enqueue follow-up scripts. Continuations are tracked with `parentExecutionId` so you can trace the lineage:
232
+
233
+ ```javascript
234
+ async execute(params) {
235
+ const ids = await this.getWorkBatch();
236
+ await this.context.queueScriptBatch(
237
+ ids.map((integrationId) => ({ scriptName: 'attio-healing', params: { integrationId } }))
238
+ );
239
+ return { queued: ids.length };
240
+ }
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Execution modes & reliability
246
+
247
+ - **Sync** (`mode: 'sync'`) — runs in the API Lambda, result returned in the response. Capped by the API timeout.
248
+ - **Async** (`mode: 'async'`, default) — queued to SQS and run by the worker Lambda (15-min budget). The SQS queue has a redrive policy (up to 3 receives) to a dead-letter queue, so a crashed invocation is retried at the infrastructure level. There is no application-level per-script retry — model idempotency accordingly.
249
+
250
+ Every execution is persisted as an `AdminProcess` record with its `state` (`PENDING` → `RUNNING` → `COMPLETED`/`FAILED`), input, output, metrics, and logs.
251
+
252
+ ---
253
+
254
+ ## Environment variables
255
+
256
+ | Variable | Set by | Purpose |
257
+ | ---------------------------------- | ---------------------------------- | -------------------------------------------------------------------------- |
258
+ | `ADMIN_API_KEY` | **Operator** (SSM/Secrets Manager) | Shared admin API key checked by the auth middleware. Required. |
259
+ | `ADMIN_SCRIPT_QUEUE_URL` | `AdminScriptBuilder` | SQS queue URL for async execution. |
260
+ | `SCHEDULER_PROVIDER` | `AdminScriptBuilder` (`'aws'`) | Scheduler adapter type. Falls back to `local` only outside AWS (dev/test). |
261
+ | `ADMIN_SCRIPT_EXECUTOR_LAMBDA_ARN` | `AdminScriptBuilder` | Worker Lambda ARN that EventBridge Scheduler invokes. |
262
+ | `ADMIN_SCRIPT_SCHEDULE_GROUP` | `AdminScriptBuilder` | EventBridge Scheduler group name. |
263
+ | `SCHEDULER_ROLE_ARN` | `AdminScriptBuilder` | IAM role EventBridge assumes to invoke the worker. |
264
+
265
+ You must provision `ADMIN_API_KEY` yourself (e.g. via SSM Parameter Store or Secrets Manager) — the builder does not generate it.
266
+
267
+ ---
268
+
269
+ ## Local development
270
+
271
+ Without AWS, the scheduler uses an in-memory `LocalSchedulerAdapter` (schedules do not persist across restarts) and async execution needs a queue URL. Set `ADMIN_API_KEY` in your local env to exercise the endpoints.
272
+
273
+ ---
274
+
275
+ ## Architecture
276
+
277
+ See [ADR-005: Admin Script Runner Service](../../docs/architecture-decisions/005-admin-script-runner.md) for the full design (layering, security model, scheduling, and validation). The package follows Frigg's hexagonal architecture: HTTP handlers → `ScriptRunner` (application) → command layer → repositories, with the scheduler as a swappable port (`SchedulerAdapter` → AWS/local adapters).
package/index.js CHANGED
@@ -6,7 +6,11 @@
6
6
  */
7
7
 
8
8
  // Application Services
9
- const { ScriptFactory, getScriptFactory, createScriptFactory } = require('./src/application/script-factory');
9
+ const {
10
+ ScriptFactory,
11
+ getScriptFactory,
12
+ createScriptFactory,
13
+ } = require('./src/application/script-factory');
10
14
  const { AdminScriptBase } = require('./src/application/admin-script-base');
11
15
  const {
12
16
  AdminScriptContext,
@@ -15,12 +19,23 @@ const {
15
19
  AdminFriggCommands,
16
20
  createAdminFriggCommands,
17
21
  } = require('./src/application/admin-frigg-commands');
18
- const { ScriptRunner, createScriptRunner } = require('./src/application/script-runner');
22
+ const {
23
+ ScriptRunner,
24
+ createScriptRunner,
25
+ } = require('./src/application/script-runner');
19
26
 
20
27
  // Infrastructure
21
- const { validateAdminApiKey } = require('./src/infrastructure/admin-auth-middleware');
22
- const { router, app, handler: routerHandler } = require('./src/infrastructure/admin-script-router');
23
- const { handler: executorHandler } = require('./src/infrastructure/script-executor-handler');
28
+ const {
29
+ validateAdminApiKey,
30
+ } = require('./src/infrastructure/admin-auth-middleware');
31
+ const {
32
+ router,
33
+ app,
34
+ handler: routerHandler,
35
+ } = require('./src/infrastructure/admin-script-router');
36
+ const {
37
+ handler: executorHandler,
38
+ } = require('./src/infrastructure/script-executor-handler');
24
39
 
25
40
  // Built-in Scripts
26
41
  const {
@@ -33,7 +48,9 @@ const {
33
48
  // Adapters
34
49
  const { SchedulerAdapter } = require('./src/adapters/scheduler-adapter');
35
50
  const { AWSSchedulerAdapter } = require('./src/adapters/aws-scheduler-adapter');
36
- const { LocalSchedulerAdapter } = require('./src/adapters/local-scheduler-adapter');
51
+ const {
52
+ LocalSchedulerAdapter,
53
+ } = require('./src/adapters/local-scheduler-adapter');
37
54
  const {
38
55
  createSchedulerAdapter,
39
56
  } = require('./src/adapters/scheduler-adapter-factory');
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.545.b4ca16d.0",
4
+ "version": "2.0.0--canary.517.8eaf5df.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.545.b4ca16d.0",
8
+ "@friggframework/core": "2.0.0--canary.517.8eaf5df.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.545.b4ca16d.0",
14
- "@friggframework/prettier-config": "2.0.0--canary.545.b4ca16d.0",
15
- "@friggframework/test": "2.0.0--canary.545.b4ca16d.0",
13
+ "@friggframework/eslint-config": "2.0.0--canary.517.8eaf5df.0",
14
+ "@friggframework/prettier-config": "2.0.0--canary.517.8eaf5df.0",
15
+ "@friggframework/test": "2.0.0--canary.517.8eaf5df.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": "b4ca16d156483c8b9fc926b049364659f0adacc1"
46
+ "gitHead": "8eaf5df0746c979fc5cb7877d51a861becd5fbce"
47
47
  }
@@ -9,17 +9,33 @@ jest.mock('@aws-sdk/client-scheduler', () => {
9
9
  SchedulerClient: jest.fn(() => ({
10
10
  send: mockSend,
11
11
  })),
12
- CreateScheduleCommand: jest.fn((params) => ({ _type: 'CreateScheduleCommand', params })),
13
- DeleteScheduleCommand: jest.fn((params) => ({ _type: 'DeleteScheduleCommand', params })),
14
- GetScheduleCommand: jest.fn((params) => ({ _type: 'GetScheduleCommand', params })),
15
- UpdateScheduleCommand: jest.fn((params) => ({ _type: 'UpdateScheduleCommand', params })),
16
- ListSchedulesCommand: jest.fn((params) => ({ _type: 'ListSchedulesCommand', params })),
12
+ CreateScheduleCommand: jest.fn((params) => ({
13
+ _type: 'CreateScheduleCommand',
14
+ params,
15
+ })),
16
+ DeleteScheduleCommand: jest.fn((params) => ({
17
+ _type: 'DeleteScheduleCommand',
18
+ params,
19
+ })),
20
+ GetScheduleCommand: jest.fn((params) => ({
21
+ _type: 'GetScheduleCommand',
22
+ params,
23
+ })),
24
+ UpdateScheduleCommand: jest.fn((params) => ({
25
+ _type: 'UpdateScheduleCommand',
26
+ params,
27
+ })),
28
+ ListSchedulesCommand: jest.fn((params) => ({
29
+ _type: 'ListSchedulesCommand',
30
+ params,
31
+ })),
17
32
  _mockSend: mockSend,
18
33
  };
19
34
  });
20
35
 
21
36
  const defaultParams = {
22
- targetLambdaArn: 'arn:aws:lambda:us-east-1:123456789012:function:admin-script-executor',
37
+ targetLambdaArn:
38
+ 'arn:aws:lambda:us-east-1:123456789012:function:admin-script-executor',
23
39
  scheduleGroupName: 'frigg-admin-scripts',
24
40
  roleArn: 'arn:aws:iam::123456789012:role/test-role',
25
41
  };
@@ -57,50 +73,70 @@ describe('AWSSchedulerAdapter', () => {
57
73
  it('should use provided configuration and AWS_REGION from env', () => {
58
74
  process.env.AWS_REGION = 'eu-west-1';
59
75
  const customAdapter = new AWSSchedulerAdapter({
60
- targetLambdaArn: 'arn:aws:lambda:eu-west-1:123456789012:function:custom',
76
+ targetLambdaArn:
77
+ 'arn:aws:lambda:eu-west-1:123456789012:function:custom',
61
78
  scheduleGroupName: 'custom-group',
62
79
  roleArn: 'arn:aws:iam::123456789012:role/custom-role',
63
80
  });
64
81
 
65
82
  expect(customAdapter.region).toBe('eu-west-1');
66
- expect(customAdapter.targetLambdaArn).toBe('arn:aws:lambda:eu-west-1:123456789012:function:custom');
83
+ expect(customAdapter.targetLambdaArn).toBe(
84
+ 'arn:aws:lambda:eu-west-1:123456789012:function:custom'
85
+ );
67
86
  expect(customAdapter.scheduleGroupName).toBe('custom-group');
68
- expect(customAdapter.roleArn).toBe('arn:aws:iam::123456789012:role/custom-role');
87
+ expect(customAdapter.roleArn).toBe(
88
+ 'arn:aws:iam::123456789012:role/custom-role'
89
+ );
69
90
  });
70
91
 
71
92
  it('should throw if AWS_REGION is not set', () => {
72
93
  delete process.env.AWS_REGION;
73
- expect(() => new AWSSchedulerAdapter({
74
- ...defaultParams,
75
- })).toThrow('AWSSchedulerAdapter requires AWS_REGION environment variable');
94
+ expect(
95
+ () =>
96
+ new AWSSchedulerAdapter({
97
+ ...defaultParams,
98
+ })
99
+ ).toThrow(
100
+ 'AWSSchedulerAdapter requires AWS_REGION environment variable'
101
+ );
76
102
  });
77
103
 
78
104
  it('should throw if targetLambdaArn is missing', () => {
79
- expect(() => new AWSSchedulerAdapter({
80
- scheduleGroupName: defaultParams.scheduleGroupName,
81
- roleArn: defaultParams.roleArn,
82
- })).toThrow('AWSSchedulerAdapter requires targetLambdaArn');
105
+ expect(
106
+ () =>
107
+ new AWSSchedulerAdapter({
108
+ scheduleGroupName: defaultParams.scheduleGroupName,
109
+ roleArn: defaultParams.roleArn,
110
+ })
111
+ ).toThrow('AWSSchedulerAdapter requires targetLambdaArn');
83
112
  });
84
113
 
85
114
  it('should throw if scheduleGroupName is missing', () => {
86
- expect(() => new AWSSchedulerAdapter({
87
- targetLambdaArn: defaultParams.targetLambdaArn,
88
- roleArn: defaultParams.roleArn,
89
- })).toThrow('AWSSchedulerAdapter requires scheduleGroupName');
115
+ expect(
116
+ () =>
117
+ new AWSSchedulerAdapter({
118
+ targetLambdaArn: defaultParams.targetLambdaArn,
119
+ roleArn: defaultParams.roleArn,
120
+ })
121
+ ).toThrow('AWSSchedulerAdapter requires scheduleGroupName');
90
122
  });
91
123
 
92
124
  it('should throw if roleArn is missing', () => {
93
- expect(() => new AWSSchedulerAdapter({
94
- targetLambdaArn: defaultParams.targetLambdaArn,
95
- scheduleGroupName: defaultParams.scheduleGroupName,
96
- })).toThrow('AWSSchedulerAdapter requires roleArn');
125
+ expect(
126
+ () =>
127
+ new AWSSchedulerAdapter({
128
+ targetLambdaArn: defaultParams.targetLambdaArn,
129
+ scheduleGroupName: defaultParams.scheduleGroupName,
130
+ })
131
+ ).toThrow('AWSSchedulerAdapter requires roleArn');
97
132
  });
98
133
  });
99
134
 
100
135
  describe('createSchedule()', () => {
101
136
  it('should create a schedule with required fields', async () => {
102
137
  mockSend.mockResolvedValue({
103
- ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
138
+ ScheduleArn:
139
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
104
140
  });
105
141
 
106
142
  const result = await adapter.createSchedule({
@@ -109,7 +145,8 @@ describe('AWSSchedulerAdapter', () => {
109
145
  });
110
146
 
111
147
  expect(result).toEqual({
112
- scheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
148
+ scheduleArn:
149
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
113
150
  scheduleName: 'frigg-script-test-script',
114
151
  });
115
152
 
@@ -123,7 +160,8 @@ describe('AWSSchedulerAdapter', () => {
123
160
 
124
161
  it('should create a schedule with all optional fields', async () => {
125
162
  mockSend.mockResolvedValue({
126
- ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
163
+ ScheduleArn:
164
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
127
165
  });
128
166
 
129
167
  await adapter.createSchedule({
@@ -134,7 +172,9 @@ describe('AWSSchedulerAdapter', () => {
134
172
  });
135
173
 
136
174
  const command = mockSend.mock.calls[0][0];
137
- expect(command.params.ScheduleExpressionTimezone).toBe('America/New_York');
175
+ expect(command.params.ScheduleExpressionTimezone).toBe(
176
+ 'America/New_York'
177
+ );
138
178
 
139
179
  const targetInput = JSON.parse(command.params.Target.Input);
140
180
  expect(targetInput).toEqual({
@@ -146,7 +186,8 @@ describe('AWSSchedulerAdapter', () => {
146
186
 
147
187
  it('should configure target with Lambda ARN and constructor roleArn', async () => {
148
188
  mockSend.mockResolvedValue({
149
- ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
189
+ ScheduleArn:
190
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
150
191
  });
151
192
 
152
193
  await adapter.createSchedule({
@@ -155,19 +196,25 @@ describe('AWSSchedulerAdapter', () => {
155
196
  });
156
197
 
157
198
  const command = mockSend.mock.calls[0][0];
158
- expect(command.params.Target.Arn).toBe('arn:aws:lambda:us-east-1:123456789012:function:admin-script-executor');
159
- expect(command.params.Target.RoleArn).toBe('arn:aws:iam::123456789012:role/test-role');
199
+ expect(command.params.Target.Arn).toBe(
200
+ 'arn:aws:lambda:us-east-1:123456789012:function:admin-script-executor'
201
+ );
202
+ expect(command.params.Target.RoleArn).toBe(
203
+ 'arn:aws:iam::123456789012:role/test-role'
204
+ );
160
205
  });
161
206
 
162
207
  it('should use roleArn from constructor, not process.env', async () => {
163
- const customRoleArn = 'arn:aws:iam::999999999999:role/custom-scheduler-role';
208
+ const customRoleArn =
209
+ 'arn:aws:iam::999999999999:role/custom-scheduler-role';
164
210
  const customAdapter = new AWSSchedulerAdapter({
165
211
  ...defaultParams,
166
212
  roleArn: customRoleArn,
167
213
  });
168
214
 
169
215
  mockSend.mockResolvedValue({
170
- ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
216
+ ScheduleArn:
217
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
171
218
  });
172
219
 
173
220
  await customAdapter.createSchedule({
@@ -181,7 +228,8 @@ describe('AWSSchedulerAdapter', () => {
181
228
 
182
229
  it('should enable schedule by default', async () => {
183
230
  mockSend.mockResolvedValue({
184
- ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
231
+ ScheduleArn:
232
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
185
233
  });
186
234
 
187
235
  await adapter.createSchedule({
@@ -195,7 +243,8 @@ describe('AWSSchedulerAdapter', () => {
195
243
 
196
244
  it('should set flexible time window to OFF', async () => {
197
245
  mockSend.mockResolvedValue({
198
- ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
246
+ ScheduleArn:
247
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
199
248
  });
200
249
 
201
250
  await adapter.createSchedule({
@@ -214,7 +263,8 @@ describe('AWSSchedulerAdapter', () => {
214
263
  mockSend
215
264
  .mockRejectedValueOnce(conflictError)
216
265
  .mockResolvedValueOnce({
217
- ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
266
+ ScheduleArn:
267
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
218
268
  });
219
269
 
220
270
  const result = await adapter.createSchedule({
@@ -223,13 +273,18 @@ describe('AWSSchedulerAdapter', () => {
223
273
  });
224
274
 
225
275
  expect(result).toEqual({
226
- scheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
276
+ scheduleArn:
277
+ 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
227
278
  scheduleName: 'frigg-script-test-script',
228
279
  });
229
280
 
230
281
  expect(mockSend).toHaveBeenCalledTimes(2);
231
- expect(mockSend.mock.calls[0][0]._type).toBe('CreateScheduleCommand');
232
- expect(mockSend.mock.calls[1][0]._type).toBe('UpdateScheduleCommand');
282
+ expect(mockSend.mock.calls[0][0]._type).toBe(
283
+ 'CreateScheduleCommand'
284
+ );
285
+ expect(mockSend.mock.calls[1][0]._type).toBe(
286
+ 'UpdateScheduleCommand'
287
+ );
233
288
  });
234
289
 
235
290
  it('should rethrow non-conflict errors', async () => {
@@ -238,10 +293,12 @@ describe('AWSSchedulerAdapter', () => {
238
293
 
239
294
  mockSend.mockRejectedValue(otherError);
240
295
 
241
- await expect(adapter.createSchedule({
242
- scriptName: 'test-script',
243
- cronExpression: 'cron(0 0 * * ? *)',
244
- })).rejects.toThrow('Access denied');
296
+ await expect(
297
+ adapter.createSchedule({
298
+ scriptName: 'test-script',
299
+ cronExpression: 'cron(0 0 * * ? *)',
300
+ })
301
+ ).rejects.toThrow('Access denied');
245
302
  });
246
303
  });
247
304
 
@@ -304,9 +361,13 @@ describe('AWSSchedulerAdapter', () => {
304
361
  await adapter.setScheduleEnabled('test-script', false);
305
362
 
306
363
  const updateCommand = mockSend.mock.calls[1][0];
307
- expect(updateCommand.params.ScheduleExpression).toBe('cron(0 0 * * ? *)');
364
+ expect(updateCommand.params.ScheduleExpression).toBe(
365
+ 'cron(0 0 * * ? *)'
366
+ );
308
367
  expect(updateCommand.params.ScheduleExpressionTimezone).toBe('UTC');
309
- expect(updateCommand.params.FlexibleTimeWindow).toEqual({ Mode: 'OFF' });
368
+ expect(updateCommand.params.FlexibleTimeWindow).toEqual({
369
+ Mode: 'OFF',
370
+ });
310
371
  expect(updateCommand.params.Target).toBeDefined();
311
372
  });
312
373
  });