@friggframework/admin-scripts 2.0.0--canary.517.41839c5.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/LICENSE.md +9 -0
- package/index.js +66 -0
- package/package.json +53 -0
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +322 -0
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +325 -0
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +257 -0
- package/src/adapters/__tests__/scheduler-adapter.test.js +103 -0
- package/src/adapters/aws-scheduler-adapter.js +138 -0
- package/src/adapters/local-scheduler-adapter.js +103 -0
- package/src/adapters/scheduler-adapter-factory.js +69 -0
- package/src/adapters/scheduler-adapter.js +64 -0
- package/src/application/__tests__/admin-frigg-commands.test.js +643 -0
- package/src/application/__tests__/admin-script-base.test.js +273 -0
- package/src/application/__tests__/dry-run-http-interceptor.test.js +313 -0
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +257 -0
- package/src/application/__tests__/schedule-management-use-case.test.js +276 -0
- package/src/application/__tests__/script-factory.test.js +381 -0
- package/src/application/__tests__/script-runner.test.js +202 -0
- package/src/application/admin-frigg-commands.js +242 -0
- package/src/application/admin-script-base.js +138 -0
- package/src/application/dry-run-http-interceptor.js +296 -0
- package/src/application/dry-run-repository-wrapper.js +261 -0
- package/src/application/schedule-management-use-case.js +230 -0
- package/src/application/script-factory.js +161 -0
- package/src/application/script-runner.js +254 -0
- package/src/builtins/__tests__/integration-health-check.test.js +598 -0
- package/src/builtins/__tests__/oauth-token-refresh.test.js +344 -0
- package/src/builtins/index.js +28 -0
- package/src/builtins/integration-health-check.js +279 -0
- package/src/builtins/oauth-token-refresh.js +221 -0
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +148 -0
- package/src/infrastructure/__tests__/admin-script-router.test.js +701 -0
- package/src/infrastructure/admin-auth-middleware.js +49 -0
- package/src/infrastructure/admin-script-router.js +311 -0
- package/src/infrastructure/script-executor-handler.js +75 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin API Key Authentication Middleware
|
|
5
|
+
*
|
|
6
|
+
* Validates admin API keys for script endpoints.
|
|
7
|
+
* Expects: Authorization: Bearer <api-key>
|
|
8
|
+
*/
|
|
9
|
+
async function adminAuthMiddleware(req, res, next) {
|
|
10
|
+
try {
|
|
11
|
+
const authHeader = req.headers.authorization;
|
|
12
|
+
|
|
13
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
14
|
+
return res.status(401).json({
|
|
15
|
+
error: 'Missing or invalid Authorization header',
|
|
16
|
+
code: 'MISSING_AUTH'
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const apiKey = authHeader.substring(7); // Remove 'Bearer '
|
|
21
|
+
const commands = createAdminScriptCommands();
|
|
22
|
+
const result = await commands.validateAdminApiKey(apiKey);
|
|
23
|
+
|
|
24
|
+
if (result.error) {
|
|
25
|
+
return res.status(result.error).json({
|
|
26
|
+
error: result.reason,
|
|
27
|
+
code: result.code
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Attach validated key info to request for audit trail
|
|
32
|
+
req.adminApiKey = result.apiKey;
|
|
33
|
+
req.adminAudit = {
|
|
34
|
+
apiKeyName: result.apiKey.name,
|
|
35
|
+
apiKeyLast4: result.apiKey.keyLast4,
|
|
36
|
+
ipAddress: req.ip || req.connection?.remoteAddress || 'unknown'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
next();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Admin auth middleware error:', error);
|
|
42
|
+
res.status(500).json({
|
|
43
|
+
error: 'Authentication failed',
|
|
44
|
+
code: 'AUTH_ERROR'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { adminAuthMiddleware };
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const serverless = require('serverless-http');
|
|
3
|
+
const { adminAuthMiddleware } = require('./admin-auth-middleware');
|
|
4
|
+
const { getScriptFactory } = require('../application/script-factory');
|
|
5
|
+
const { createScriptRunner } = require('../application/script-runner');
|
|
6
|
+
const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
|
|
7
|
+
const { QueuerUtil } = require('@friggframework/core/queues');
|
|
8
|
+
const { createSchedulerAdapter } = require('../adapters/scheduler-adapter-factory');
|
|
9
|
+
const { ScheduleManagementUseCase } = require('../application/schedule-management-use-case');
|
|
10
|
+
|
|
11
|
+
const router = express.Router();
|
|
12
|
+
|
|
13
|
+
// Apply auth middleware to all admin routes
|
|
14
|
+
router.use(adminAuthMiddleware);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create ScheduleManagementUseCase instance
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
function createScheduleManagementUseCase() {
|
|
21
|
+
return new ScheduleManagementUseCase({
|
|
22
|
+
commands: createAdminScriptCommands(),
|
|
23
|
+
schedulerAdapter: createSchedulerAdapter(),
|
|
24
|
+
scriptFactory: getScriptFactory(),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* GET /admin/scripts
|
|
30
|
+
* List all registered scripts
|
|
31
|
+
*/
|
|
32
|
+
router.get('/scripts', async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const factory = getScriptFactory();
|
|
35
|
+
const scripts = factory.getAll();
|
|
36
|
+
|
|
37
|
+
res.json({
|
|
38
|
+
scripts: scripts.map((s) => ({
|
|
39
|
+
name: s.name,
|
|
40
|
+
version: s.definition.version,
|
|
41
|
+
description: s.definition.description,
|
|
42
|
+
category: s.definition.display?.category || 'custom',
|
|
43
|
+
requiresIntegrationFactory:
|
|
44
|
+
s.definition.config?.requiresIntegrationFactory || false,
|
|
45
|
+
schedule: s.definition.schedule || null,
|
|
46
|
+
})),
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Error listing scripts:', error);
|
|
50
|
+
res.status(500).json({ error: 'Failed to list scripts' });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* GET /admin/scripts/:scriptName
|
|
56
|
+
* Get script details
|
|
57
|
+
*/
|
|
58
|
+
router.get('/scripts/:scriptName', async (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const { scriptName } = req.params;
|
|
61
|
+
const factory = getScriptFactory();
|
|
62
|
+
|
|
63
|
+
if (!factory.has(scriptName)) {
|
|
64
|
+
return res.status(404).json({
|
|
65
|
+
error: `Script "${scriptName}" not found`,
|
|
66
|
+
code: 'SCRIPT_NOT_FOUND',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const scriptClass = factory.get(scriptName);
|
|
71
|
+
const definition = scriptClass.Definition;
|
|
72
|
+
|
|
73
|
+
res.json({
|
|
74
|
+
name: definition.name,
|
|
75
|
+
version: definition.version,
|
|
76
|
+
description: definition.description,
|
|
77
|
+
inputSchema: definition.inputSchema,
|
|
78
|
+
outputSchema: definition.outputSchema,
|
|
79
|
+
config: definition.config,
|
|
80
|
+
display: definition.display,
|
|
81
|
+
schedule: definition.schedule,
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error getting script:', error);
|
|
85
|
+
res.status(500).json({ error: 'Failed to get script details' });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* POST /admin/scripts/:scriptName/execute
|
|
91
|
+
* Execute a script (sync, async, or dry-run)
|
|
92
|
+
*/
|
|
93
|
+
router.post('/scripts/:scriptName/execute', async (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
const { scriptName } = req.params;
|
|
96
|
+
const { params = {}, mode = 'async', dryRun = false } = req.body;
|
|
97
|
+
const factory = getScriptFactory();
|
|
98
|
+
|
|
99
|
+
if (!factory.has(scriptName)) {
|
|
100
|
+
return res.status(404).json({
|
|
101
|
+
error: `Script "${scriptName}" not found`,
|
|
102
|
+
code: 'SCRIPT_NOT_FOUND',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Dry-run always executes synchronously
|
|
107
|
+
if (dryRun) {
|
|
108
|
+
const runner = createScriptRunner();
|
|
109
|
+
const result = await runner.execute(scriptName, params, {
|
|
110
|
+
trigger: 'MANUAL',
|
|
111
|
+
mode: 'sync',
|
|
112
|
+
dryRun: true,
|
|
113
|
+
audit: req.adminAudit,
|
|
114
|
+
});
|
|
115
|
+
return res.json(result);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (mode === 'sync') {
|
|
119
|
+
// Synchronous execution - wait for result
|
|
120
|
+
const runner = createScriptRunner();
|
|
121
|
+
const result = await runner.execute(scriptName, params, {
|
|
122
|
+
trigger: 'MANUAL',
|
|
123
|
+
mode: 'sync',
|
|
124
|
+
audit: req.adminAudit,
|
|
125
|
+
});
|
|
126
|
+
return res.json(result);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Async execution - queue and return immediately
|
|
130
|
+
const commands = createAdminScriptCommands();
|
|
131
|
+
const execution = await commands.createScriptExecution({
|
|
132
|
+
scriptName,
|
|
133
|
+
scriptVersion: factory.get(scriptName).Definition.version,
|
|
134
|
+
trigger: 'MANUAL',
|
|
135
|
+
mode: 'async',
|
|
136
|
+
input: params,
|
|
137
|
+
audit: req.adminAudit,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Queue the execution
|
|
141
|
+
await QueuerUtil.send(
|
|
142
|
+
{
|
|
143
|
+
scriptName,
|
|
144
|
+
executionId: execution.id,
|
|
145
|
+
trigger: 'MANUAL',
|
|
146
|
+
params,
|
|
147
|
+
},
|
|
148
|
+
process.env.ADMIN_SCRIPT_QUEUE_URL
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
res.status(202).json({
|
|
152
|
+
executionId: execution.id,
|
|
153
|
+
status: 'PENDING',
|
|
154
|
+
scriptName,
|
|
155
|
+
message: 'Script queued for execution',
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('Error executing script:', error);
|
|
159
|
+
res.status(500).json({ error: 'Failed to execute script' });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* GET /admin/executions/:executionId
|
|
165
|
+
* Get execution status
|
|
166
|
+
*/
|
|
167
|
+
router.get('/executions/:executionId', async (req, res) => {
|
|
168
|
+
try {
|
|
169
|
+
const { executionId } = req.params;
|
|
170
|
+
const commands = createAdminScriptCommands();
|
|
171
|
+
const execution = await commands.findScriptExecutionById(executionId);
|
|
172
|
+
|
|
173
|
+
if (execution.error) {
|
|
174
|
+
return res.status(execution.error).json({
|
|
175
|
+
error: execution.reason,
|
|
176
|
+
code: execution.code,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
res.json(execution);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('Error getting execution:', error);
|
|
183
|
+
res.status(500).json({ error: 'Failed to get execution' });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* GET /admin/executions
|
|
189
|
+
* List recent executions
|
|
190
|
+
*/
|
|
191
|
+
router.get('/executions', async (req, res) => {
|
|
192
|
+
try {
|
|
193
|
+
const { scriptName, status, limit = 50 } = req.query;
|
|
194
|
+
const commands = createAdminScriptCommands();
|
|
195
|
+
|
|
196
|
+
const executions = await commands.findRecentExecutions({
|
|
197
|
+
scriptName,
|
|
198
|
+
status,
|
|
199
|
+
limit: Number.parseInt(limit, 10),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
res.json({ executions });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('Error listing executions:', error);
|
|
205
|
+
res.status(500).json({ error: 'Failed to list executions' });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* GET /admin/scripts/:scriptName/schedule
|
|
211
|
+
* Get effective schedule (DB override > Definition default > none)
|
|
212
|
+
*/
|
|
213
|
+
router.get('/scripts/:scriptName/schedule', async (req, res) => {
|
|
214
|
+
try {
|
|
215
|
+
const { scriptName } = req.params;
|
|
216
|
+
const useCase = createScheduleManagementUseCase();
|
|
217
|
+
|
|
218
|
+
const result = await useCase.getEffectiveSchedule(scriptName);
|
|
219
|
+
|
|
220
|
+
res.json({
|
|
221
|
+
source: result.source,
|
|
222
|
+
scriptName,
|
|
223
|
+
...result.schedule,
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error.code === 'SCRIPT_NOT_FOUND') {
|
|
227
|
+
return res.status(404).json({
|
|
228
|
+
error: error.message,
|
|
229
|
+
code: error.code,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
console.error('Error getting schedule:', error);
|
|
233
|
+
res.status(500).json({ error: 'Failed to get schedule' });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* PUT /admin/scripts/:scriptName/schedule
|
|
239
|
+
* Create or update schedule override
|
|
240
|
+
*/
|
|
241
|
+
router.put('/scripts/:scriptName/schedule', async (req, res) => {
|
|
242
|
+
try {
|
|
243
|
+
const { scriptName } = req.params;
|
|
244
|
+
const { enabled, cronExpression, timezone } = req.body;
|
|
245
|
+
const useCase = createScheduleManagementUseCase();
|
|
246
|
+
|
|
247
|
+
const result = await useCase.upsertSchedule(scriptName, {
|
|
248
|
+
enabled,
|
|
249
|
+
cronExpression,
|
|
250
|
+
timezone,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
res.json({
|
|
254
|
+
success: result.success,
|
|
255
|
+
schedule: {
|
|
256
|
+
source: 'database',
|
|
257
|
+
...result.schedule,
|
|
258
|
+
},
|
|
259
|
+
...(result.schedulerWarning && { schedulerWarning: result.schedulerWarning }),
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error.code === 'SCRIPT_NOT_FOUND') {
|
|
263
|
+
return res.status(404).json({
|
|
264
|
+
error: error.message,
|
|
265
|
+
code: error.code,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (error.code === 'INVALID_INPUT') {
|
|
269
|
+
return res.status(400).json({
|
|
270
|
+
error: error.message,
|
|
271
|
+
code: error.code,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
console.error('Error updating schedule:', error);
|
|
275
|
+
res.status(500).json({ error: 'Failed to update schedule' });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* DELETE /admin/scripts/:scriptName/schedule
|
|
281
|
+
* Remove schedule override (revert to Definition default)
|
|
282
|
+
*/
|
|
283
|
+
router.delete('/scripts/:scriptName/schedule', async (req, res) => {
|
|
284
|
+
try {
|
|
285
|
+
const { scriptName } = req.params;
|
|
286
|
+
const useCase = createScheduleManagementUseCase();
|
|
287
|
+
|
|
288
|
+
const result = await useCase.deleteSchedule(scriptName);
|
|
289
|
+
|
|
290
|
+
res.json(result);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error.code === 'SCRIPT_NOT_FOUND') {
|
|
293
|
+
return res.status(404).json({
|
|
294
|
+
error: error.message,
|
|
295
|
+
code: error.code,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
console.error('Error deleting schedule:', error);
|
|
299
|
+
res.status(500).json({ error: 'Failed to delete schedule' });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Create Express app
|
|
304
|
+
const app = express();
|
|
305
|
+
app.use(express.json());
|
|
306
|
+
app.use('/admin', router);
|
|
307
|
+
|
|
308
|
+
// Export for Lambda
|
|
309
|
+
const handler = serverless(app);
|
|
310
|
+
|
|
311
|
+
module.exports = { router, app, handler };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const { createScriptRunner } = require('../application/script-runner');
|
|
2
|
+
const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQS Queue Worker Lambda Handler
|
|
6
|
+
*
|
|
7
|
+
* Processes script execution messages from AdminScriptQueue
|
|
8
|
+
*/
|
|
9
|
+
async function handler(event) {
|
|
10
|
+
const results = [];
|
|
11
|
+
|
|
12
|
+
for (const record of event.Records) {
|
|
13
|
+
const message = JSON.parse(record.body);
|
|
14
|
+
const { scriptName, executionId, trigger, params } = message;
|
|
15
|
+
|
|
16
|
+
console.log(`Processing script: ${scriptName}, executionId: ${executionId}`);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const runner = createScriptRunner();
|
|
20
|
+
const commands = createAdminScriptCommands();
|
|
21
|
+
|
|
22
|
+
// If executionId provided (async from API), update existing record
|
|
23
|
+
if (executionId) {
|
|
24
|
+
await commands.updateScriptExecutionStatus(executionId, 'RUNNING');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await runner.execute(scriptName, params, {
|
|
28
|
+
trigger: trigger || 'QUEUE',
|
|
29
|
+
mode: 'async',
|
|
30
|
+
executionId, // Reuse existing if provided
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log(
|
|
34
|
+
`Script completed: ${scriptName}, status: ${result.status}`
|
|
35
|
+
);
|
|
36
|
+
results.push({
|
|
37
|
+
scriptName,
|
|
38
|
+
status: result.status,
|
|
39
|
+
executionId: result.executionId,
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`Script failed: ${scriptName}`, error);
|
|
43
|
+
|
|
44
|
+
// Try to update execution status if we have an ID
|
|
45
|
+
if (executionId) {
|
|
46
|
+
const commands = createAdminScriptCommands();
|
|
47
|
+
await commands
|
|
48
|
+
.completeScriptExecution(executionId, {
|
|
49
|
+
status: 'FAILED',
|
|
50
|
+
error: {
|
|
51
|
+
name: error.name,
|
|
52
|
+
message: error.message,
|
|
53
|
+
stack: error.stack,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
.catch((e) =>
|
|
57
|
+
console.error('Failed to update execution:', e)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
results.push({
|
|
62
|
+
scriptName,
|
|
63
|
+
status: 'FAILED',
|
|
64
|
+
error: error.message,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
statusCode: 200,
|
|
71
|
+
body: JSON.stringify({ processed: results.length, results }),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { handler };
|