@friggframework/admin-scripts 2.0.0--canary.545.b4ca16d.0 → 2.0.0--canary.517.ff03f2c.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 +272 -0
  2. package/index.js +22 -19
  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/infrastructure/__tests__/admin-auth-middleware.test.js +9 -3
  30. package/src/infrastructure/__tests__/admin-script-router.test.js +125 -52
  31. package/src/infrastructure/admin-auth-middleware.js +3 -1
  32. package/src/infrastructure/admin-script-router.js +129 -14
  33. package/src/infrastructure/bootstrap.js +82 -0
  34. package/src/infrastructure/script-executor-handler.js +119 -52
  35. package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
  36. package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
  37. package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
  38. package/src/application/dry-run-http-interceptor.js +0 -296
  39. package/src/application/dry-run-repository-wrapper.js +0 -261
  40. package/src/application/schedule-management-use-case.js +0 -230
  41. package/src/builtins/__tests__/integration-health-check.test.js +0 -607
  42. package/src/builtins/__tests__/oauth-token-refresh.test.js +0 -354
  43. package/src/builtins/index.js +0 -28
  44. package/src/builtins/integration-health-check.js +0 -278
  45. package/src/builtins/oauth-token-refresh.js +0 -220
package/README.md ADDED
@@ -0,0 +1,272 @@
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
+ enableScheduling: true, // provision EventBridge Scheduler resources
40
+ },
41
+ };
42
+
43
+ module.exports = { Definition };
44
+ ```
45
+
46
+ 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 into the script registry.
47
+
48
+ ---
49
+
50
+ ## Writing a script
51
+
52
+ Extend `AdminScriptBase`, declare a static `Definition`, and implement `execute(params)`. The execution context is injected for you and available as `this.context`.
53
+
54
+ ```javascript
55
+ const { AdminScriptBase } = require('@friggframework/admin-scripts');
56
+
57
+ class AttioHealingScript extends AdminScriptBase {
58
+ static Definition = {
59
+ name: 'attio-healing',
60
+ version: '1.0.0',
61
+ description: 'Repairs corrupted Attio integration config',
62
+ source: 'USER_DEFINED',
63
+
64
+ // JSON Schema — validated on /validate and before every execution
65
+ inputSchema: {
66
+ type: 'object',
67
+ required: ['integrationId'],
68
+ properties: {
69
+ integrationId: { type: 'string' },
70
+ dryRun: { type: 'boolean', default: false },
71
+ },
72
+ },
73
+
74
+ config: {
75
+ timeout: 300000, // ms; sync mode is capped (see below)
76
+ requireIntegrationInstance: true, // needs this.context.instantiate()
77
+ },
78
+
79
+ // Optional default schedule (can be overridden at runtime via the API)
80
+ schedule: { enabled: false, cronExpression: 'cron(0 12 * * ? *)' },
81
+
82
+ display: { category: 'maintenance' },
83
+ };
84
+
85
+ async execute(params) {
86
+ const { integrationId } = params;
87
+
88
+ this.context.log('info', 'Starting Attio healing', { integrationId });
89
+
90
+ const integration =
91
+ await this.context.integrationRepository.findIntegrationById(
92
+ integrationId
93
+ );
94
+ if (!integration) {
95
+ throw new Error(`Integration ${integrationId} not found`);
96
+ }
97
+
98
+ // Call the live integration when you need to hit an external API.
99
+ // Modules are attached to the instance by their own name (from the
100
+ // module's getName(), e.g. `instance.attio`) and each module's `.api`
101
+ // is its authenticated client. Iterate `instance.modules` if you don't
102
+ // want to hard-code a module name. The methods on `.api` are defined by
103
+ // that specific API module.
104
+ const instance = await this.context.instantiate(integrationId);
105
+ await instance.attio.api.refreshEntityConfig(); // example — use your module's real method
106
+
107
+ this.context.log('info', 'Healing complete', { integrationId });
108
+ return { healed: true, integrationId };
109
+ }
110
+ }
111
+
112
+ module.exports = { AttioHealingScript };
113
+ ```
114
+
115
+ ### The execution context (`this.context`)
116
+
117
+ | Member | Description |
118
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
119
+ | `log(level, message, data?)` | Records a structured log entry (`level`: `debug`/`info`/`warn`/`error`). Logs are persisted to the execution's `results.logs`. |
120
+ | `integrationRepository` | Read/query integrations. |
121
+ | `userRepository` | Read/query users. |
122
+ | `moduleRepository` | Read/query modules. |
123
+ | `credentialRepository` | Read credentials (decrypted transparently). |
124
+ | `instantiate(integrationId)` | Returns a hydrated integration instance for calling external APIs. Requires `config.requireIntegrationInstance: true`. |
125
+ | `queueScript(name, params?)` | Enqueue another script as a follow-up (tracked as a child of the current execution). |
126
+ | `queueScriptBatch(entries)` | Enqueue many follow-up scripts at once. |
127
+ | `getExecutionId()` | The current `AdminProcess` record id. |
128
+
129
+ Repositories return **plain, decrypted data** — field-level encryption is handled transparently.
130
+
131
+ ---
132
+
133
+ ## HTTP API
134
+
135
+ All routes are mounted under `/admin` and require the `x-frigg-admin-api-key` header.
136
+
137
+ | Method | Path | Description |
138
+ | -------- | ------------------------------------------------ | ------------------------------------------------ |
139
+ | `GET` | `/admin/scripts` | List registered scripts |
140
+ | `GET` | `/admin/scripts/{name}` | Get a script's details/schema |
141
+ | `POST` | `/admin/scripts/{name}` | Execute a script (`sync` or `async`) |
142
+ | `POST` | `/admin/scripts/{name}/validate` | Validate input against the schema (no execution) |
143
+ | `GET` | `/admin/scripts/{name}/executions` | List recent executions (`?status=`, `?limit=`) |
144
+ | `GET` | `/admin/scripts/{name}/executions/{executionId}` | Get one execution |
145
+ | `GET` | `/admin/scripts/{name}/schedule` | Get the effective schedule |
146
+ | `PUT` | `/admin/scripts/{name}/schedule` | Create/update a schedule override |
147
+ | `DELETE` | `/admin/scripts/{name}/schedule` | Remove the schedule override |
148
+
149
+ ### Execute a script
150
+
151
+ **Async (default)** — queued to SQS, returns immediately with an execution id to poll:
152
+
153
+ ```bash
154
+ curl -X POST https://<your-app>/admin/scripts/attio-healing \
155
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY" \
156
+ -H "Content-Type: application/json" \
157
+ -d '{ "params": { "integrationId": "abc123" }, "mode": "async" }'
158
+
159
+ # 202 Accepted
160
+ # { "executionId": "665f...", "status": "QUEUED", "scriptName": "attio-healing" }
161
+ ```
162
+
163
+ **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.
164
+
165
+ ```bash
166
+ curl -X POST https://<your-app>/admin/scripts/attio-healing \
167
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY" \
168
+ -H "Content-Type: application/json" \
169
+ -d '{ "params": { "integrationId": "abc123" }, "mode": "sync" }'
170
+
171
+ # 200 OK
172
+ # { "executionId": "...", "status": "COMPLETED", "output": { ... }, "metrics": { "durationMs": 812 } }
173
+ ```
174
+
175
+ Invalid input is rejected up front:
176
+
177
+ ```bash
178
+ # 400 Bad Request → { "error": "Invalid input: Missing required parameter: integrationId", "code": "INVALID_INPUT" }
179
+ ```
180
+
181
+ ### Validate without executing
182
+
183
+ ```bash
184
+ curl -X POST https://<your-app>/admin/scripts/attio-healing/validate \
185
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY" \
186
+ -H "Content-Type: application/json" \
187
+ -d '{ "params": { "integrationId": "abc123" } }'
188
+
189
+ # { "status": "VALID", "preview": { ... }, "message": "Validation passed. ..." }
190
+ ```
191
+
192
+ ### Poll an execution
193
+
194
+ ```bash
195
+ curl https://<your-app>/admin/scripts/attio-healing/executions/665f... \
196
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
197
+
198
+ curl "https://<your-app>/admin/scripts/attio-healing/executions?status=FAILED&limit=20" \
199
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Scheduling
205
+
206
+ 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.
207
+
208
+ ```bash
209
+ # Enable a daily 6am UTC run
210
+ curl -X PUT https://<your-app>/admin/scripts/attio-healing/schedule \
211
+ -H "x-frigg-admin-api-key: $ADMIN_API_KEY" \
212
+ -H "Content-Type: application/json" \
213
+ -d '{ "enabled": true, "cronExpression": "cron(0 6 * * ? *)", "timezone": "UTC" }'
214
+
215
+ # Inspect / remove
216
+ curl https://<your-app>/admin/scripts/attio-healing/schedule -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
217
+ curl -X DELETE https://<your-app>/admin/scripts/attio-healing/schedule -H "x-frigg-admin-api-key: $ADMIN_API_KEY"
218
+ ```
219
+
220
+ 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.
221
+
222
+ ---
223
+
224
+ ## Script chaining
225
+
226
+ Long or fan-out work can enqueue follow-up scripts. Continuations are tracked with `parentExecutionId` so you can trace the lineage:
227
+
228
+ ```javascript
229
+ async execute(params) {
230
+ const ids = await this.getWorkBatch();
231
+ await this.context.queueScriptBatch(
232
+ ids.map((integrationId) => ({ scriptName: 'attio-healing', params: { integrationId } }))
233
+ );
234
+ return { queued: ids.length };
235
+ }
236
+ ```
237
+
238
+ ---
239
+
240
+ ## Execution modes & reliability
241
+
242
+ - **Sync** (`mode: 'sync'`) — runs in the API Lambda, result returned in the response. Capped by the API timeout.
243
+ - **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.
244
+
245
+ Every execution is persisted as an `AdminProcess` record with its `state` (`PENDING` → `RUNNING` → `COMPLETED`/`FAILED`), input, output, metrics, and logs.
246
+
247
+ ---
248
+
249
+ ## Environment variables
250
+
251
+ | Variable | Set by | Purpose |
252
+ | ---------------------------------- | ---------------------------------- | -------------------------------------------------------------------------- |
253
+ | `ADMIN_API_KEY` | **Operator** (SSM/Secrets Manager) | Shared admin API key checked by the auth middleware. Required. |
254
+ | `ADMIN_SCRIPT_QUEUE_URL` | `AdminScriptBuilder` | SQS queue URL for async execution. |
255
+ | `SCHEDULER_PROVIDER` | `AdminScriptBuilder` (`'aws'`) | Scheduler adapter type. Falls back to `local` only outside AWS (dev/test). |
256
+ | `ADMIN_SCRIPT_EXECUTOR_LAMBDA_ARN` | `AdminScriptBuilder` | Worker Lambda ARN that EventBridge Scheduler invokes. |
257
+ | `ADMIN_SCRIPT_SCHEDULE_GROUP` | `AdminScriptBuilder` | EventBridge Scheduler group name. |
258
+ | `SCHEDULER_ROLE_ARN` | `AdminScriptBuilder` | IAM role EventBridge assumes to invoke the worker. |
259
+
260
+ You must provision `ADMIN_API_KEY` yourself (e.g. via SSM Parameter Store or Secrets Manager) — the builder does not generate it.
261
+
262
+ ---
263
+
264
+ ## Local development
265
+
266
+ 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.
267
+
268
+ ---
269
+
270
+ ## Architecture
271
+
272
+ 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,25 +19,30 @@ 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');
24
-
25
- // Built-in Scripts
26
28
  const {
27
- OAuthTokenRefreshScript,
28
- IntegrationHealthCheckScript,
29
- builtinScripts,
30
- registerBuiltinScripts,
31
- } = require('./src/builtins');
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');
32
39
 
33
40
  // Adapters
34
41
  const { SchedulerAdapter } = require('./src/adapters/scheduler-adapter');
35
42
  const { AWSSchedulerAdapter } = require('./src/adapters/aws-scheduler-adapter');
36
- const { LocalSchedulerAdapter } = require('./src/adapters/local-scheduler-adapter');
43
+ const {
44
+ LocalSchedulerAdapter,
45
+ } = require('./src/adapters/local-scheduler-adapter');
37
46
  const {
38
47
  createSchedulerAdapter,
39
48
  } = require('./src/adapters/scheduler-adapter-factory');
@@ -59,12 +68,6 @@ module.exports = {
59
68
  routerHandler,
60
69
  executorHandler,
61
70
 
62
- // Built-in scripts
63
- OAuthTokenRefreshScript,
64
- IntegrationHealthCheckScript,
65
- builtinScripts,
66
- registerBuiltinScripts,
67
-
68
71
  // Adapters
69
72
  SchedulerAdapter,
70
73
  AWSSchedulerAdapter,
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.ff03f2c.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.ff03f2c.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.ff03f2c.0",
14
+ "@friggframework/prettier-config": "2.0.0--canary.517.ff03f2c.0",
15
+ "@friggframework/test": "2.0.0--canary.517.ff03f2c.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": "ff03f2c9204a318789ed105c09cc9d44deeae6ec"
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
  });