@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.
- package/README.md +277 -0
- package/index.js +23 -6
- package/package.json +6 -6
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +106 -45
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +24 -10
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +30 -9
- package/src/adapters/__tests__/scheduler-adapter.test.js +6 -2
- package/src/adapters/aws-scheduler-adapter.js +55 -28
- package/src/adapters/local-scheduler-adapter.js +2 -2
- package/src/adapters/scheduler-adapter-factory.js +3 -1
- package/src/adapters/scheduler-adapter.js +9 -3
- package/src/application/__tests__/admin-frigg-commands.test.js +73 -30
- package/src/application/__tests__/admin-script-base.test.js +23 -6
- package/src/application/__tests__/script-factory.test.js +30 -6
- package/src/application/__tests__/script-runner.test.js +113 -24
- package/src/application/__tests__/validate-script-input.test.js +54 -15
- package/src/application/admin-frigg-commands.js +21 -9
- package/src/application/admin-script-base.js +3 -2
- package/src/application/script-factory.js +3 -1
- package/src/application/script-runner.js +90 -48
- package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +16 -7
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +9 -4
- package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +21 -10
- package/src/application/use-cases/delete-schedule-use-case.js +6 -4
- package/src/application/use-cases/get-effective-schedule-use-case.js +3 -1
- package/src/application/use-cases/index.js +3 -1
- package/src/application/use-cases/upsert-schedule-use-case.js +30 -11
- package/src/application/validate-script-input.js +10 -3
- package/src/builtins/__tests__/integration-health-check.test.js +232 -127
- package/src/builtins/__tests__/oauth-token-refresh.test.js +128 -75
- package/src/builtins/index.js +1 -4
- package/src/builtins/integration-health-check.js +63 -30
- package/src/builtins/oauth-token-refresh.js +64 -29
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +9 -3
- package/src/infrastructure/__tests__/admin-script-router.test.js +125 -52
- package/src/infrastructure/admin-auth-middleware.js +3 -1
- package/src/infrastructure/admin-script-router.js +129 -14
- package/src/infrastructure/bootstrap.js +87 -0
- package/src/infrastructure/script-executor-handler.js +119 -52
- package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
- package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
- package/src/application/dry-run-http-interceptor.js +0 -296
- package/src/application/dry-run-repository-wrapper.js +0 -261
- package/src/application/schedule-management-use-case.js +0 -230
|
@@ -3,10 +3,18 @@ const serverless = require('serverless-http');
|
|
|
3
3
|
const { validateAdminApiKey } = require('./admin-auth-middleware');
|
|
4
4
|
const { getScriptFactory } = require('../application/script-factory');
|
|
5
5
|
const { createScriptRunner } = require('../application/script-runner');
|
|
6
|
-
const {
|
|
7
|
-
|
|
6
|
+
const {
|
|
7
|
+
validateScriptInput,
|
|
8
|
+
validateParams,
|
|
9
|
+
} = require('../application/validate-script-input');
|
|
10
|
+
const {
|
|
11
|
+
createAdminScriptCommands,
|
|
12
|
+
} = require('@friggframework/core/application/commands/admin-script-commands');
|
|
8
13
|
const { QueuerUtil } = require('@friggframework/core/queues');
|
|
9
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
createSchedulerAdapter,
|
|
16
|
+
} = require('../adapters/scheduler-adapter-factory');
|
|
17
|
+
const { bootstrapAdminScripts } = require('./bootstrap');
|
|
10
18
|
const {
|
|
11
19
|
GetEffectiveScheduleUseCase,
|
|
12
20
|
UpsertScheduleUseCase,
|
|
@@ -18,14 +26,51 @@ const router = express.Router();
|
|
|
18
26
|
// Apply auth middleware to all admin routes
|
|
19
27
|
router.use(validateAdminApiKey);
|
|
20
28
|
|
|
29
|
+
// Register the host app's admin scripts (and built-ins) before handling requests.
|
|
30
|
+
// Memoized, so this only does work on the first request per process.
|
|
31
|
+
router.use((_req, _res, next) => {
|
|
32
|
+
bootstrapAdminScripts();
|
|
33
|
+
next();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build audit metadata for an execution from the request.
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
function buildAudit(req) {
|
|
41
|
+
const apiKey = req.headers['x-frigg-admin-api-key'];
|
|
42
|
+
const forwardedFor = (req.headers['x-forwarded-for'] || '')
|
|
43
|
+
.split(',')[0]
|
|
44
|
+
.trim();
|
|
45
|
+
return {
|
|
46
|
+
ipAddress: forwardedFor || req.ip || null,
|
|
47
|
+
apiKeyLast4: apiKey ? String(apiKey).slice(-4) : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
/**
|
|
22
52
|
* Create schedule use case instances
|
|
23
53
|
* @private
|
|
24
54
|
*/
|
|
25
55
|
function createScheduleUseCases() {
|
|
26
56
|
const commands = createAdminScriptCommands();
|
|
57
|
+
|
|
58
|
+
// The local adapter is in-memory only (schedules vanish on cold start), so it
|
|
59
|
+
// must never be the silent default in a deployed Lambda. Require an explicit
|
|
60
|
+
// provider when running on AWS; fall back to 'local' only for local dev/tests.
|
|
61
|
+
const schedulerType =
|
|
62
|
+
process.env.SCHEDULER_PROVIDER ||
|
|
63
|
+
(process.env.AWS_LAMBDA_FUNCTION_NAME ? null : 'local');
|
|
64
|
+
if (!schedulerType) {
|
|
65
|
+
const error = new Error(
|
|
66
|
+
'SCHEDULER_PROVIDER is not configured. Set it (e.g. "aws") via appDefinition.admin.enableScheduling.'
|
|
67
|
+
);
|
|
68
|
+
error.code = 'SCHEDULER_NOT_CONFIGURED';
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
|
|
27
72
|
const schedulerAdapter = createSchedulerAdapter({
|
|
28
|
-
type:
|
|
73
|
+
type: schedulerType,
|
|
29
74
|
targetLambdaArn: process.env.ADMIN_SCRIPT_EXECUTOR_LAMBDA_ARN,
|
|
30
75
|
scheduleGroupName: process.env.ADMIN_SCRIPT_SCHEDULE_GROUP,
|
|
31
76
|
roleArn: process.env.SCHEDULER_ROLE_ARN,
|
|
@@ -33,9 +78,20 @@ function createScheduleUseCases() {
|
|
|
33
78
|
const scriptFactory = getScriptFactory();
|
|
34
79
|
|
|
35
80
|
return {
|
|
36
|
-
getEffectiveSchedule: new GetEffectiveScheduleUseCase({
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
getEffectiveSchedule: new GetEffectiveScheduleUseCase({
|
|
82
|
+
commands,
|
|
83
|
+
scriptFactory,
|
|
84
|
+
}),
|
|
85
|
+
upsertSchedule: new UpsertScheduleUseCase({
|
|
86
|
+
commands,
|
|
87
|
+
schedulerAdapter,
|
|
88
|
+
scriptFactory,
|
|
89
|
+
}),
|
|
90
|
+
deleteSchedule: new DeleteScheduleUseCase({
|
|
91
|
+
commands,
|
|
92
|
+
schedulerAdapter,
|
|
93
|
+
scriptFactory,
|
|
94
|
+
}),
|
|
39
95
|
};
|
|
40
96
|
}
|
|
41
97
|
|
|
@@ -142,11 +198,40 @@ router.post('/scripts/:scriptName', async (req, res) => {
|
|
|
142
198
|
});
|
|
143
199
|
}
|
|
144
200
|
|
|
201
|
+
const definition = factory.get(scriptName).Definition;
|
|
202
|
+
|
|
203
|
+
// Fail fast on invalid input instead of starting an execution that will error
|
|
204
|
+
const validation = validateParams(definition, params);
|
|
205
|
+
if (!validation.valid) {
|
|
206
|
+
return res.status(400).json({
|
|
207
|
+
error: `Invalid input: ${validation.errors.join(', ')}`,
|
|
208
|
+
code: 'INVALID_INPUT',
|
|
209
|
+
details: validation.errors,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const audit = buildAudit(req);
|
|
214
|
+
|
|
145
215
|
if (mode === 'sync') {
|
|
146
|
-
|
|
216
|
+
// Sync runs inside the 30s API Lambda; long scripts must use async
|
|
217
|
+
const timeout = definition.config?.timeout;
|
|
218
|
+
if (
|
|
219
|
+
process.env.AWS_LAMBDA_FUNCTION_NAME &&
|
|
220
|
+
timeout &&
|
|
221
|
+
timeout > 25000
|
|
222
|
+
) {
|
|
223
|
+
return res.status(400).json({
|
|
224
|
+
error: `Script "${scriptName}" timeout (${timeout}ms) exceeds the sync API limit. Use mode: "async".`,
|
|
225
|
+
code: 'SYNC_TIMEOUT_TOO_LONG',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const { integrationFactory } = bootstrapAdminScripts();
|
|
230
|
+
const runner = createScriptRunner({ integrationFactory });
|
|
147
231
|
const result = await runner.execute(scriptName, params, {
|
|
148
232
|
trigger: 'MANUAL',
|
|
149
233
|
mode: 'sync',
|
|
234
|
+
audit,
|
|
150
235
|
});
|
|
151
236
|
return res.json(result);
|
|
152
237
|
}
|
|
@@ -163,12 +248,21 @@ router.post('/scripts/:scriptName', async (req, res) => {
|
|
|
163
248
|
const commands = createAdminScriptCommands();
|
|
164
249
|
const execution = await commands.createAdminProcess({
|
|
165
250
|
scriptName,
|
|
166
|
-
scriptVersion:
|
|
251
|
+
scriptVersion: definition.version,
|
|
167
252
|
trigger: 'MANUAL',
|
|
168
253
|
mode: 'async',
|
|
169
254
|
input: params,
|
|
255
|
+
audit,
|
|
170
256
|
});
|
|
171
257
|
|
|
258
|
+
// Commands return an error object (never throw) — don't queue a broken execution
|
|
259
|
+
if (execution.error) {
|
|
260
|
+
return res.status(execution.error).json({
|
|
261
|
+
error: execution.reason || 'Failed to create execution record',
|
|
262
|
+
code: execution.code,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
172
266
|
// Queue the execution
|
|
173
267
|
await QueuerUtil.send(
|
|
174
268
|
{
|
|
@@ -226,10 +320,14 @@ router.get('/scripts/:scriptName/executions', async (req, res) => {
|
|
|
226
320
|
const { status, limit = 50 } = req.query;
|
|
227
321
|
const commands = createAdminScriptCommands();
|
|
228
322
|
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
323
|
+
const parsedLimit = Number.parseInt(limit, 10);
|
|
324
|
+
const safeLimit = Number.isNaN(parsedLimit)
|
|
325
|
+
? 50
|
|
326
|
+
: Math.min(Math.max(parsedLimit, 1), 200);
|
|
327
|
+
|
|
328
|
+
const executions = await commands.findAdminProcessesByName(scriptName, {
|
|
329
|
+
limit: safeLimit,
|
|
330
|
+
...(status && { state: status }),
|
|
233
331
|
});
|
|
234
332
|
|
|
235
333
|
res.json({ executions });
|
|
@@ -262,6 +360,11 @@ router.get('/scripts/:scriptName/schedule', async (req, res) => {
|
|
|
262
360
|
code: error.code,
|
|
263
361
|
});
|
|
264
362
|
}
|
|
363
|
+
if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
|
|
364
|
+
return res
|
|
365
|
+
.status(503)
|
|
366
|
+
.json({ error: error.message, code: error.code });
|
|
367
|
+
}
|
|
265
368
|
console.error('Error getting schedule:', error);
|
|
266
369
|
res.status(500).json({ error: 'Failed to get schedule' });
|
|
267
370
|
}
|
|
@@ -289,7 +392,9 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
|
|
|
289
392
|
source: 'database',
|
|
290
393
|
...result.schedule,
|
|
291
394
|
},
|
|
292
|
-
...(result.schedulerWarning && {
|
|
395
|
+
...(result.schedulerWarning && {
|
|
396
|
+
schedulerWarning: result.schedulerWarning,
|
|
397
|
+
}),
|
|
293
398
|
});
|
|
294
399
|
} catch (error) {
|
|
295
400
|
if (error.code === 'SCRIPT_NOT_FOUND') {
|
|
@@ -304,6 +409,11 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
|
|
|
304
409
|
code: error.code,
|
|
305
410
|
});
|
|
306
411
|
}
|
|
412
|
+
if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
|
|
413
|
+
return res
|
|
414
|
+
.status(503)
|
|
415
|
+
.json({ error: error.message, code: error.code });
|
|
416
|
+
}
|
|
307
417
|
console.error('Error updating schedule:', error);
|
|
308
418
|
res.status(500).json({ error: 'Failed to update schedule' });
|
|
309
419
|
}
|
|
@@ -328,6 +438,11 @@ router.delete('/scripts/:scriptName/schedule', async (req, res) => {
|
|
|
328
438
|
code: error.code,
|
|
329
439
|
});
|
|
330
440
|
}
|
|
441
|
+
if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
|
|
442
|
+
return res
|
|
443
|
+
.status(503)
|
|
444
|
+
.json({ error: error.message, code: error.code });
|
|
445
|
+
}
|
|
331
446
|
console.error('Error deleting schedule:', error);
|
|
332
447
|
res.status(500).json({ error: 'Failed to delete schedule' });
|
|
333
448
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { getScriptFactory } = require('../application/script-factory');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Script Bootstrap
|
|
5
|
+
*
|
|
6
|
+
* Loads the host app's definition at Lambda runtime and registers its admin
|
|
7
|
+
* scripts (and the built-ins, when enabled) into the global ScriptFactory, so
|
|
8
|
+
* the router and SQS worker can resolve scripts by name. Also constructs the
|
|
9
|
+
* integrationFactory used by scripts that need hydrated integration instances.
|
|
10
|
+
*
|
|
11
|
+
* Runs once per process (memoized) and never throws — a missing/unloadable app
|
|
12
|
+
* definition is logged and leaves the factory empty rather than crashing the
|
|
13
|
+
* Lambda cold start.
|
|
14
|
+
*/
|
|
15
|
+
let bootstrapped = false;
|
|
16
|
+
let integrationFactory = null;
|
|
17
|
+
|
|
18
|
+
function registerScripts(factory, scriptClasses) {
|
|
19
|
+
for (const ScriptClass of scriptClasses || []) {
|
|
20
|
+
const name = ScriptClass?.Definition?.name;
|
|
21
|
+
// Guard against re-registration (register() throws on name collision)
|
|
22
|
+
if (name && !factory.has(name)) {
|
|
23
|
+
factory.register(ScriptClass);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build an integration factory backed by the framework's runtime hydration.
|
|
30
|
+
* Scripts call `context.instantiate(integrationId)` which delegates here.
|
|
31
|
+
* The require is lazy so the admin-scripts package has no load-time coupling
|
|
32
|
+
* to core's backend utilities (and unit tests never hit this path).
|
|
33
|
+
*/
|
|
34
|
+
function createIntegrationFactory() {
|
|
35
|
+
return {
|
|
36
|
+
async getInstanceFromIntegrationId({ integrationId }) {
|
|
37
|
+
const {
|
|
38
|
+
loadIntegrationForWebhook,
|
|
39
|
+
} = require('@friggframework/core/handlers/backend-utils');
|
|
40
|
+
return loadIntegrationForWebhook(integrationId);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @returns {{ integrationFactory: object }}
|
|
47
|
+
*/
|
|
48
|
+
function bootstrapAdminScripts() {
|
|
49
|
+
if (bootstrapped) {
|
|
50
|
+
return { integrationFactory };
|
|
51
|
+
}
|
|
52
|
+
bootstrapped = true;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const {
|
|
56
|
+
loadAppDefinition,
|
|
57
|
+
} = require('@friggframework/core/handlers/app-definition-loader');
|
|
58
|
+
const { adminScripts = [], admin = {} } = loadAppDefinition();
|
|
59
|
+
|
|
60
|
+
const factory = getScriptFactory();
|
|
61
|
+
registerScripts(factory, adminScripts);
|
|
62
|
+
|
|
63
|
+
if (admin.includeBuiltinScripts) {
|
|
64
|
+
const { builtinScripts } = require('../builtins');
|
|
65
|
+
registerScripts(factory, builtinScripts);
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(
|
|
69
|
+
'[admin-scripts] bootstrap: could not load app definition:',
|
|
70
|
+
error.message
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
integrationFactory = createIntegrationFactory();
|
|
75
|
+
return { integrationFactory };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Test-only: reset memoized bootstrap state. */
|
|
79
|
+
function _resetBootstrapForTests() {
|
|
80
|
+
bootstrapped = false;
|
|
81
|
+
integrationFactory = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
bootstrapAdminScripts,
|
|
86
|
+
_resetBootstrapForTests,
|
|
87
|
+
};
|
|
@@ -1,69 +1,136 @@
|
|
|
1
1
|
const { createScriptRunner } = require('../application/script-runner');
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
createAdminScriptCommands,
|
|
4
|
+
} = require('@friggframework/core/application/commands/admin-script-commands');
|
|
5
|
+
const { bootstrapAdminScripts } = require('./bootstrap');
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Processes script execution messages from AdminScriptQueue.
|
|
8
|
-
* Thin adapter: parses SQS messages and delegates to ScriptRunner.
|
|
9
|
-
* ScriptRunner handles execution tracking, error recording, and status updates.
|
|
8
|
+
* Run a single execution message through the ScriptRunner.
|
|
9
|
+
* @private
|
|
10
10
|
*/
|
|
11
|
-
async function
|
|
12
|
-
const
|
|
11
|
+
async function runMessage(message, integrationFactory) {
|
|
12
|
+
const { scriptName, executionId, trigger, params, parentExecutionId } =
|
|
13
|
+
message;
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (!scriptName) {
|
|
16
|
+
throw new Error('Invalid message: missing scriptName');
|
|
17
|
+
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
console.log(
|
|
20
|
+
`Processing script: ${scriptName}${
|
|
21
|
+
executionId ? `, executionId: ${executionId}` : ''
|
|
22
|
+
}`
|
|
23
|
+
);
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const runner = createScriptRunner({ integrationFactory });
|
|
26
|
+
const result = await runner.execute(scriptName, params, {
|
|
27
|
+
trigger: trigger || 'QUEUE',
|
|
28
|
+
mode: 'async',
|
|
29
|
+
// executionId is optional — when absent, ScriptRunner creates the record.
|
|
30
|
+
// Scheduled direct invokes and queueScript continuations have no id yet.
|
|
31
|
+
...(executionId && { executionId }),
|
|
32
|
+
...(parentExecutionId && { parentExecutionId }),
|
|
33
|
+
});
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
return {
|
|
36
|
+
scriptName,
|
|
37
|
+
status: result.status,
|
|
38
|
+
executionId: result.executionId,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Mark an admin process FAILED when the worker itself blows up (parse error,
|
|
44
|
+
* runner construction), so the record doesn't stay stuck in a non-terminal
|
|
45
|
+
* state. No-op when there is no execution id.
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
async function markFailed(executionId, error) {
|
|
49
|
+
if (!executionId) return;
|
|
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(
|
|
62
|
+
`Failed to update execution ${executionId} state:`,
|
|
63
|
+
updateError
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
35
67
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Admin Script Executor Lambda Handler
|
|
70
|
+
*
|
|
71
|
+
* Handles two invocation shapes:
|
|
72
|
+
* - SQS: `event.Records[]` — each record body is a JSON execution message
|
|
73
|
+
* (manual async executions and queueScript continuations).
|
|
74
|
+
* - EventBridge Scheduler direct invoke: the event itself is the message
|
|
75
|
+
* (`{ scriptName, trigger: 'SCHEDULED', params }`) with no `Records` wrapper.
|
|
76
|
+
*
|
|
77
|
+
* Thin adapter: parses the event and delegates to ScriptRunner, which owns
|
|
78
|
+
* execution tracking, error recording, and status updates.
|
|
79
|
+
*/
|
|
80
|
+
async function handler(event) {
|
|
81
|
+
const { integrationFactory } = bootstrapAdminScripts();
|
|
46
82
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
// EventBridge Scheduler invokes the Lambda directly (no Records wrapper)
|
|
84
|
+
if (!event.Records) {
|
|
85
|
+
try {
|
|
86
|
+
const result = await runMessage(event, integrationFactory);
|
|
87
|
+
console.log(
|
|
88
|
+
`Script completed: ${result.scriptName}, status: ${result.status}`
|
|
89
|
+
);
|
|
90
|
+
return {
|
|
91
|
+
statusCode: 200,
|
|
92
|
+
body: JSON.stringify({ processed: 1, results: [result] }),
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(
|
|
96
|
+
'Unexpected error processing scheduled invoke:',
|
|
97
|
+
error
|
|
98
|
+
);
|
|
99
|
+
await markFailed(event.executionId, error);
|
|
100
|
+
return {
|
|
101
|
+
statusCode: 200,
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
processed: 1,
|
|
104
|
+
results: [
|
|
105
|
+
{
|
|
106
|
+
scriptName: event.scriptName || 'unknown',
|
|
107
|
+
status: 'FAILED',
|
|
108
|
+
error: error.message,
|
|
58
109
|
},
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
],
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
64
115
|
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const record of event.Records) {
|
|
118
|
+
let message = {};
|
|
119
|
+
try {
|
|
120
|
+
message = JSON.parse(record.body);
|
|
121
|
+
const result = await runMessage(message, integrationFactory);
|
|
122
|
+
console.log(
|
|
123
|
+
`Script completed: ${result.scriptName}, status: ${result.status}`
|
|
124
|
+
);
|
|
125
|
+
results.push(result);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Only unexpected failures reach here (message parse errors, runner
|
|
128
|
+
// construction). Script execution errors are handled by ScriptRunner
|
|
129
|
+
// and returned as { status: 'FAILED' }.
|
|
130
|
+
console.error('Unexpected error processing record:', error);
|
|
131
|
+
await markFailed(message.executionId, error);
|
|
65
132
|
results.push({
|
|
66
|
-
scriptName: scriptName || 'unknown',
|
|
133
|
+
scriptName: message.scriptName || 'unknown',
|
|
67
134
|
status: 'FAILED',
|
|
68
135
|
error: error.message,
|
|
69
136
|
});
|