@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.
- package/README.md +272 -0
- package/index.js +22 -19
- 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/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 +82 -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
- package/src/builtins/__tests__/integration-health-check.test.js +0 -607
- package/src/builtins/__tests__/oauth-token-refresh.test.js +0 -354
- package/src/builtins/index.js +0 -28
- package/src/builtins/integration-health-check.js +0 -278
- package/src/builtins/oauth-token-refresh.js +0 -220
|
@@ -15,12 +15,19 @@ jest.mock('../../application/script-runner');
|
|
|
15
15
|
jest.mock('@friggframework/core/application/commands/admin-script-commands');
|
|
16
16
|
jest.mock('@friggframework/core/queues');
|
|
17
17
|
jest.mock('../../adapters/scheduler-adapter-factory');
|
|
18
|
+
jest.mock('../bootstrap', () => ({
|
|
19
|
+
bootstrapAdminScripts: () => ({ integrationFactory: {} }),
|
|
20
|
+
}));
|
|
18
21
|
|
|
19
22
|
const { getScriptFactory } = require('../../application/script-factory');
|
|
20
23
|
const { createScriptRunner } = require('../../application/script-runner');
|
|
21
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
createAdminScriptCommands,
|
|
26
|
+
} = require('@friggframework/core/application/commands/admin-script-commands');
|
|
22
27
|
const { QueuerUtil } = require('@friggframework/core/queues');
|
|
23
|
-
const {
|
|
28
|
+
const {
|
|
29
|
+
createSchedulerAdapter,
|
|
30
|
+
} = require('../../adapters/scheduler-adapter-factory');
|
|
24
31
|
|
|
25
32
|
describe('Admin Script Router', () => {
|
|
26
33
|
let mockFactory;
|
|
@@ -56,7 +63,7 @@ describe('Admin Script Router', () => {
|
|
|
56
63
|
mockCommands = {
|
|
57
64
|
createAdminProcess: jest.fn(),
|
|
58
65
|
findAdminProcessById: jest.fn(),
|
|
59
|
-
|
|
66
|
+
findAdminProcessesByName: jest.fn(),
|
|
60
67
|
};
|
|
61
68
|
|
|
62
69
|
mockSchedulerAdapter = {
|
|
@@ -118,7 +125,9 @@ describe('Admin Script Router', () => {
|
|
|
118
125
|
|
|
119
126
|
describe('GET /admin/scripts/:scriptName', () => {
|
|
120
127
|
it('should return script details', async () => {
|
|
121
|
-
const response = await request(app).get(
|
|
128
|
+
const response = await request(app).get(
|
|
129
|
+
'/admin/scripts/test-script'
|
|
130
|
+
);
|
|
122
131
|
|
|
123
132
|
expect(response.status).toBe(200);
|
|
124
133
|
expect(response.body.name).toBe('test-script');
|
|
@@ -169,7 +178,8 @@ describe('Admin Script Router', () => {
|
|
|
169
178
|
});
|
|
170
179
|
|
|
171
180
|
it('should queue script for async execution', async () => {
|
|
172
|
-
process.env.ADMIN_SCRIPT_QUEUE_URL =
|
|
181
|
+
process.env.ADMIN_SCRIPT_QUEUE_URL =
|
|
182
|
+
'https://sqs.us-east-1.amazonaws.com/123/test-queue';
|
|
173
183
|
mockCommands.createAdminProcess.mockResolvedValue({
|
|
174
184
|
id: 'exec-456',
|
|
175
185
|
});
|
|
@@ -195,7 +205,8 @@ describe('Admin Script Router', () => {
|
|
|
195
205
|
});
|
|
196
206
|
|
|
197
207
|
it('should default to async mode', async () => {
|
|
198
|
-
process.env.ADMIN_SCRIPT_QUEUE_URL =
|
|
208
|
+
process.env.ADMIN_SCRIPT_QUEUE_URL =
|
|
209
|
+
'https://sqs.us-east-1.amazonaws.com/123/test-queue';
|
|
199
210
|
mockCommands.createAdminProcess.mockResolvedValue({
|
|
200
211
|
id: 'exec-789',
|
|
201
212
|
});
|
|
@@ -247,7 +258,9 @@ describe('Admin Script Router', () => {
|
|
|
247
258
|
status: 'COMPLETED',
|
|
248
259
|
});
|
|
249
260
|
|
|
250
|
-
const response = await request(app).get(
|
|
261
|
+
const response = await request(app).get(
|
|
262
|
+
'/admin/scripts/test-script/executions/exec-123'
|
|
263
|
+
);
|
|
251
264
|
|
|
252
265
|
expect(response.status).toBe(200);
|
|
253
266
|
expect(response.body.id).toBe('exec-123');
|
|
@@ -272,33 +285,39 @@ describe('Admin Script Router', () => {
|
|
|
272
285
|
|
|
273
286
|
describe('GET /admin/scripts/:scriptName/executions', () => {
|
|
274
287
|
it('should list executions for specific script', async () => {
|
|
275
|
-
mockCommands.
|
|
276
|
-
{ id: 'exec-1',
|
|
277
|
-
{ id: 'exec-2',
|
|
288
|
+
mockCommands.findAdminProcessesByName.mockResolvedValue([
|
|
289
|
+
{ id: 'exec-1', name: 'test-script', state: 'COMPLETED' },
|
|
290
|
+
{ id: 'exec-2', name: 'test-script', state: 'RUNNING' },
|
|
278
291
|
]);
|
|
279
292
|
|
|
280
|
-
const response = await request(app).get(
|
|
293
|
+
const response = await request(app).get(
|
|
294
|
+
'/admin/scripts/test-script/executions'
|
|
295
|
+
);
|
|
281
296
|
|
|
282
297
|
expect(response.status).toBe(200);
|
|
283
298
|
expect(response.body.executions).toHaveLength(2);
|
|
284
|
-
expect(mockCommands.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
299
|
+
expect(mockCommands.findAdminProcessesByName).toHaveBeenCalledWith(
|
|
300
|
+
'test-script',
|
|
301
|
+
{
|
|
302
|
+
limit: 50,
|
|
303
|
+
}
|
|
304
|
+
);
|
|
288
305
|
});
|
|
289
306
|
|
|
290
|
-
it('should accept query parameters', async () => {
|
|
291
|
-
mockCommands.
|
|
307
|
+
it('should accept query parameters (status maps to state, limit is bounded)', async () => {
|
|
308
|
+
mockCommands.findAdminProcessesByName.mockResolvedValue([]);
|
|
292
309
|
|
|
293
310
|
await request(app).get(
|
|
294
311
|
'/admin/scripts/test-script/executions?status=COMPLETED&limit=10'
|
|
295
312
|
);
|
|
296
313
|
|
|
297
|
-
expect(mockCommands.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
314
|
+
expect(mockCommands.findAdminProcessesByName).toHaveBeenCalledWith(
|
|
315
|
+
'test-script',
|
|
316
|
+
{
|
|
317
|
+
limit: 10,
|
|
318
|
+
state: 'COMPLETED',
|
|
319
|
+
}
|
|
320
|
+
);
|
|
302
321
|
});
|
|
303
322
|
});
|
|
304
323
|
|
|
@@ -311,15 +330,20 @@ describe('Admin Script Router', () => {
|
|
|
311
330
|
timezone: 'America/New_York',
|
|
312
331
|
lastTriggeredAt: new Date('2025-01-01T09:00:00Z'),
|
|
313
332
|
nextTriggerAt: new Date('2025-01-02T09:00:00Z'),
|
|
314
|
-
externalScheduleId:
|
|
333
|
+
externalScheduleId:
|
|
334
|
+
'arn:aws:events:us-east-1:123456789012:rule/test',
|
|
315
335
|
externalScheduleName: 'test-script-schedule',
|
|
316
336
|
createdAt: new Date('2025-01-01T00:00:00Z'),
|
|
317
337
|
updatedAt: new Date('2025-01-01T00:00:00Z'),
|
|
318
338
|
};
|
|
319
339
|
|
|
320
|
-
mockCommands.getScheduleByScriptName = jest
|
|
340
|
+
mockCommands.getScheduleByScriptName = jest
|
|
341
|
+
.fn()
|
|
342
|
+
.mockResolvedValue(dbSchedule);
|
|
321
343
|
|
|
322
|
-
const response = await request(app).get(
|
|
344
|
+
const response = await request(app).get(
|
|
345
|
+
'/admin/scripts/test-script/schedule'
|
|
346
|
+
);
|
|
323
347
|
|
|
324
348
|
expect(response.status).toBe(200);
|
|
325
349
|
expect(response.body.source).toBe('database');
|
|
@@ -329,7 +353,9 @@ describe('Admin Script Router', () => {
|
|
|
329
353
|
});
|
|
330
354
|
|
|
331
355
|
it('should return definition schedule when no database override', async () => {
|
|
332
|
-
mockCommands.getScheduleByScriptName = jest
|
|
356
|
+
mockCommands.getScheduleByScriptName = jest
|
|
357
|
+
.fn()
|
|
358
|
+
.mockResolvedValue(null);
|
|
333
359
|
|
|
334
360
|
// Update test script to include schedule
|
|
335
361
|
class ScheduledTestScript extends TestScript {
|
|
@@ -345,7 +371,9 @@ describe('Admin Script Router', () => {
|
|
|
345
371
|
|
|
346
372
|
mockFactory.get.mockReturnValue(ScheduledTestScript);
|
|
347
373
|
|
|
348
|
-
const response = await request(app).get(
|
|
374
|
+
const response = await request(app).get(
|
|
375
|
+
'/admin/scripts/test-script/schedule'
|
|
376
|
+
);
|
|
349
377
|
|
|
350
378
|
expect(response.status).toBe(200);
|
|
351
379
|
expect(response.body.source).toBe('definition');
|
|
@@ -355,9 +383,13 @@ describe('Admin Script Router', () => {
|
|
|
355
383
|
});
|
|
356
384
|
|
|
357
385
|
it('should return none when no schedule configured', async () => {
|
|
358
|
-
mockCommands.getScheduleByScriptName = jest
|
|
386
|
+
mockCommands.getScheduleByScriptName = jest
|
|
387
|
+
.fn()
|
|
388
|
+
.mockResolvedValue(null);
|
|
359
389
|
|
|
360
|
-
const response = await request(app).get(
|
|
390
|
+
const response = await request(app).get(
|
|
391
|
+
'/admin/scripts/test-script/schedule'
|
|
392
|
+
);
|
|
361
393
|
|
|
362
394
|
expect(response.status).toBe(200);
|
|
363
395
|
expect(response.body.source).toBe('none');
|
|
@@ -389,7 +421,9 @@ describe('Admin Script Router', () => {
|
|
|
389
421
|
updatedAt: new Date(),
|
|
390
422
|
};
|
|
391
423
|
|
|
392
|
-
mockCommands.upsertSchedule = jest
|
|
424
|
+
mockCommands.upsertSchedule = jest
|
|
425
|
+
.fn()
|
|
426
|
+
.mockResolvedValue(newSchedule);
|
|
393
427
|
|
|
394
428
|
const response = await request(app)
|
|
395
429
|
.put('/admin/scripts/test-script/schedule')
|
|
@@ -424,7 +458,9 @@ describe('Admin Script Router', () => {
|
|
|
424
458
|
updatedAt: new Date(),
|
|
425
459
|
};
|
|
426
460
|
|
|
427
|
-
mockCommands.upsertSchedule = jest
|
|
461
|
+
mockCommands.upsertSchedule = jest
|
|
462
|
+
.fn()
|
|
463
|
+
.mockResolvedValue(updatedSchedule);
|
|
428
464
|
|
|
429
465
|
const response = await request(app)
|
|
430
466
|
.put('/admin/scripts/test-script/schedule')
|
|
@@ -487,10 +523,15 @@ describe('Admin Script Router', () => {
|
|
|
487
523
|
updatedAt: new Date(),
|
|
488
524
|
};
|
|
489
525
|
|
|
490
|
-
mockCommands.upsertSchedule = jest
|
|
491
|
-
|
|
526
|
+
mockCommands.upsertSchedule = jest
|
|
527
|
+
.fn()
|
|
528
|
+
.mockResolvedValue(newSchedule);
|
|
529
|
+
mockCommands.updateScheduleExternalInfo = jest
|
|
530
|
+
.fn()
|
|
531
|
+
.mockResolvedValue(newSchedule);
|
|
492
532
|
mockSchedulerAdapter.createSchedule.mockResolvedValue({
|
|
493
|
-
scheduleArn:
|
|
533
|
+
scheduleArn:
|
|
534
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
494
535
|
scheduleName: 'frigg-script-test-script',
|
|
495
536
|
});
|
|
496
537
|
|
|
@@ -508,11 +549,16 @@ describe('Admin Script Router', () => {
|
|
|
508
549
|
cronExpression: '0 12 * * *',
|
|
509
550
|
timezone: 'America/Los_Angeles',
|
|
510
551
|
});
|
|
511
|
-
expect(
|
|
512
|
-
|
|
552
|
+
expect(
|
|
553
|
+
mockCommands.updateScheduleExternalInfo
|
|
554
|
+
).toHaveBeenCalledWith('test-script', {
|
|
555
|
+
externalScheduleId:
|
|
556
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
513
557
|
externalScheduleName: 'frigg-script-test-script',
|
|
514
558
|
});
|
|
515
|
-
expect(response.body.schedule.externalScheduleId).toBe(
|
|
559
|
+
expect(response.body.schedule.externalScheduleId).toBe(
|
|
560
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script'
|
|
561
|
+
);
|
|
516
562
|
});
|
|
517
563
|
|
|
518
564
|
it('should delete EventBridge schedule when disabling existing schedule', async () => {
|
|
@@ -521,14 +567,19 @@ describe('Admin Script Router', () => {
|
|
|
521
567
|
enabled: false,
|
|
522
568
|
cronExpression: null,
|
|
523
569
|
timezone: 'UTC',
|
|
524
|
-
externalScheduleId:
|
|
570
|
+
externalScheduleId:
|
|
571
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
525
572
|
externalScheduleName: 'frigg-script-test-script',
|
|
526
573
|
createdAt: new Date(),
|
|
527
574
|
updatedAt: new Date(),
|
|
528
575
|
};
|
|
529
576
|
|
|
530
|
-
mockCommands.upsertSchedule = jest
|
|
531
|
-
|
|
577
|
+
mockCommands.upsertSchedule = jest
|
|
578
|
+
.fn()
|
|
579
|
+
.mockResolvedValue(existingSchedule);
|
|
580
|
+
mockCommands.updateScheduleExternalInfo = jest
|
|
581
|
+
.fn()
|
|
582
|
+
.mockResolvedValue(existingSchedule);
|
|
532
583
|
mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
|
|
533
584
|
|
|
534
585
|
const response = await request(app)
|
|
@@ -538,8 +589,12 @@ describe('Admin Script Router', () => {
|
|
|
538
589
|
});
|
|
539
590
|
|
|
540
591
|
expect(response.status).toBe(200);
|
|
541
|
-
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
542
|
-
|
|
592
|
+
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
593
|
+
'test-script'
|
|
594
|
+
);
|
|
595
|
+
expect(
|
|
596
|
+
mockCommands.updateScheduleExternalInfo
|
|
597
|
+
).toHaveBeenCalledWith('test-script', {
|
|
543
598
|
externalScheduleId: null,
|
|
544
599
|
externalScheduleName: null,
|
|
545
600
|
});
|
|
@@ -555,8 +610,12 @@ describe('Admin Script Router', () => {
|
|
|
555
610
|
updatedAt: new Date(),
|
|
556
611
|
};
|
|
557
612
|
|
|
558
|
-
mockCommands.upsertSchedule = jest
|
|
559
|
-
|
|
613
|
+
mockCommands.upsertSchedule = jest
|
|
614
|
+
.fn()
|
|
615
|
+
.mockResolvedValue(newSchedule);
|
|
616
|
+
mockSchedulerAdapter.createSchedule.mockRejectedValue(
|
|
617
|
+
new Error('AWS Scheduler API error')
|
|
618
|
+
);
|
|
560
619
|
|
|
561
620
|
const response = await request(app)
|
|
562
621
|
.put('/admin/scripts/test-script/schedule')
|
|
@@ -568,7 +627,9 @@ describe('Admin Script Router', () => {
|
|
|
568
627
|
// Request should succeed despite scheduler error
|
|
569
628
|
expect(response.status).toBe(200);
|
|
570
629
|
expect(response.body.success).toBe(true);
|
|
571
|
-
expect(response.body.schedulerWarning).toBe(
|
|
630
|
+
expect(response.body.schedulerWarning).toBe(
|
|
631
|
+
'AWS Scheduler API error'
|
|
632
|
+
);
|
|
572
633
|
});
|
|
573
634
|
});
|
|
574
635
|
|
|
@@ -592,7 +653,9 @@ describe('Admin Script Router', () => {
|
|
|
592
653
|
expect(response.body.success).toBe(true);
|
|
593
654
|
expect(response.body.deletedCount).toBe(1);
|
|
594
655
|
expect(response.body.message).toContain('removed');
|
|
595
|
-
expect(mockCommands.deleteSchedule).toHaveBeenCalledWith(
|
|
656
|
+
expect(mockCommands.deleteSchedule).toHaveBeenCalledWith(
|
|
657
|
+
'test-script'
|
|
658
|
+
);
|
|
596
659
|
});
|
|
597
660
|
|
|
598
661
|
it('should return definition schedule after deleting override', async () => {
|
|
@@ -636,7 +699,9 @@ describe('Admin Script Router', () => {
|
|
|
636
699
|
|
|
637
700
|
expect(response.status).toBe(200);
|
|
638
701
|
expect(response.body.deletedCount).toBe(0);
|
|
639
|
-
expect(response.body.message).toContain(
|
|
702
|
+
expect(response.body.message).toContain(
|
|
703
|
+
'No schedule override found'
|
|
704
|
+
);
|
|
640
705
|
});
|
|
641
706
|
|
|
642
707
|
it('should return 404 for non-existent script', async () => {
|
|
@@ -658,7 +723,8 @@ describe('Admin Script Router', () => {
|
|
|
658
723
|
scriptName: 'test-script',
|
|
659
724
|
enabled: true,
|
|
660
725
|
cronExpression: '0 12 * * *',
|
|
661
|
-
externalScheduleId:
|
|
726
|
+
externalScheduleId:
|
|
727
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
662
728
|
externalScheduleName: 'frigg-script-test-script',
|
|
663
729
|
},
|
|
664
730
|
});
|
|
@@ -669,7 +735,9 @@ describe('Admin Script Router', () => {
|
|
|
669
735
|
);
|
|
670
736
|
|
|
671
737
|
expect(response.status).toBe(200);
|
|
672
|
-
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
738
|
+
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
739
|
+
'test-script'
|
|
740
|
+
);
|
|
673
741
|
});
|
|
674
742
|
|
|
675
743
|
it('should not call scheduler when no external rule exists', async () => {
|
|
@@ -700,10 +768,13 @@ describe('Admin Script Router', () => {
|
|
|
700
768
|
scriptName: 'test-script',
|
|
701
769
|
enabled: true,
|
|
702
770
|
cronExpression: '0 12 * * *',
|
|
703
|
-
externalScheduleId:
|
|
771
|
+
externalScheduleId:
|
|
772
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
704
773
|
},
|
|
705
774
|
});
|
|
706
|
-
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
|
|
775
|
+
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
|
|
776
|
+
new Error('Scheduler delete failed')
|
|
777
|
+
);
|
|
707
778
|
|
|
708
779
|
const response = await request(app).delete(
|
|
709
780
|
'/admin/scripts/test-script/schedule'
|
|
@@ -712,7 +783,9 @@ describe('Admin Script Router', () => {
|
|
|
712
783
|
// Request should succeed despite scheduler error
|
|
713
784
|
expect(response.status).toBe(200);
|
|
714
785
|
expect(response.body.success).toBe(true);
|
|
715
|
-
expect(response.body.schedulerWarning).toBe(
|
|
786
|
+
expect(response.body.schedulerWarning).toBe(
|
|
787
|
+
'Scheduler delete failed'
|
|
788
|
+
);
|
|
716
789
|
});
|
|
717
790
|
});
|
|
718
791
|
});
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Expects: x-frigg-admin-api-key header
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
validateAdminApiKey,
|
|
11
|
+
} = require('@friggframework/core/handlers/middleware/admin-auth');
|
|
10
12
|
|
|
11
13
|
module.exports = { validateAdminApiKey };
|
|
@@ -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
|
}
|