@friggframework/admin-scripts 2.0.0--canary.517.300ded3.0 → 2.0.0--canary.517.35ee143.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/package.json +6 -7
- package/src/application/__tests__/admin-frigg-commands.test.js +1 -1
- package/src/application/__tests__/admin-script-base.test.js +2 -2
- package/src/application/__tests__/script-runner.test.js +2 -2
- package/src/application/admin-frigg-commands.js +1 -1
- package/src/application/admin-script-base.js +1 -1
- package/src/application/script-runner.js +5 -3
- package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +168 -0
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +114 -0
- package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +201 -0
- package/src/application/use-cases/delete-schedule-use-case.js +108 -0
- package/src/application/use-cases/get-effective-schedule-use-case.js +78 -0
- package/src/application/use-cases/index.js +18 -0
- package/src/application/use-cases/upsert-schedule-use-case.js +127 -0
- package/src/builtins/__tests__/integration-health-check.test.js +1 -1
- package/src/builtins/__tests__/oauth-token-refresh.test.js +1 -1
- package/src/builtins/integration-health-check.js +1 -1
- package/src/builtins/oauth-token-refresh.js +1 -1
- package/src/infrastructure/__tests__/admin-script-router.test.js +22 -22
- package/src/infrastructure/admin-script-router.js +24 -16
- package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
- package/src/application/schedule-management-use-case.js +0 -230
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delete Schedule Use Case
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Deletes a schedule override and cleans up external scheduler resources.
|
|
7
|
+
* Returns the effective schedule after deletion (may fall back to definition).
|
|
8
|
+
*/
|
|
9
|
+
class DeleteScheduleUseCase {
|
|
10
|
+
constructor({ commands, schedulerAdapter, scriptFactory }) {
|
|
11
|
+
this.commands = commands;
|
|
12
|
+
this.schedulerAdapter = schedulerAdapter;
|
|
13
|
+
this.scriptFactory = scriptFactory;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Delete a schedule override
|
|
18
|
+
* @param {string} scriptName - Name of the script
|
|
19
|
+
* @returns {Promise<{success: boolean, deletedCount: number, message: string, effectiveSchedule: Object, schedulerWarning?: string}>}
|
|
20
|
+
*/
|
|
21
|
+
async execute(scriptName) {
|
|
22
|
+
this._validateScriptExists(scriptName);
|
|
23
|
+
|
|
24
|
+
// Delete from database
|
|
25
|
+
const deleteResult = await this.commands.deleteSchedule(scriptName);
|
|
26
|
+
|
|
27
|
+
// Cleanup external scheduler if needed
|
|
28
|
+
const schedulerWarning = await this._cleanupExternalScheduler(
|
|
29
|
+
scriptName,
|
|
30
|
+
deleteResult.deleted?.externalScheduleId
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Determine effective schedule after deletion
|
|
34
|
+
const effectiveSchedule = this._getEffectiveScheduleAfterDeletion(scriptName);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
deletedCount: deleteResult.deletedCount,
|
|
39
|
+
message: deleteResult.deletedCount > 0
|
|
40
|
+
? 'Schedule override removed'
|
|
41
|
+
: 'No schedule override found',
|
|
42
|
+
effectiveSchedule,
|
|
43
|
+
...(schedulerWarning && { schedulerWarning }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
_validateScriptExists(scriptName) {
|
|
51
|
+
if (!this.scriptFactory.has(scriptName)) {
|
|
52
|
+
const error = new Error(`Script "${scriptName}" not found`);
|
|
53
|
+
error.code = 'SCRIPT_NOT_FOUND';
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the definition schedule from a script class
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
_getDefinitionSchedule(scriptName) {
|
|
63
|
+
const scriptClass = this.scriptFactory.get(scriptName);
|
|
64
|
+
return scriptClass.Definition?.schedule || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Determine effective schedule after deletion
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
_getEffectiveScheduleAfterDeletion(scriptName) {
|
|
72
|
+
const definitionSchedule = this._getDefinitionSchedule(scriptName);
|
|
73
|
+
|
|
74
|
+
if (definitionSchedule?.enabled) {
|
|
75
|
+
return {
|
|
76
|
+
source: 'definition',
|
|
77
|
+
enabled: definitionSchedule.enabled,
|
|
78
|
+
cronExpression: definitionSchedule.cronExpression,
|
|
79
|
+
timezone: definitionSchedule.timezone || 'UTC',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
source: 'none',
|
|
85
|
+
enabled: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Cleanup external scheduler resources
|
|
91
|
+
* @private
|
|
92
|
+
*/
|
|
93
|
+
async _cleanupExternalScheduler(scriptName, externalScheduleId) {
|
|
94
|
+
if (!externalScheduleId) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await this.schedulerAdapter.deleteSchedule(scriptName);
|
|
100
|
+
return null;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Non-fatal: DB is cleaned up, external scheduler can be retried
|
|
103
|
+
return error.message;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { DeleteScheduleUseCase };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get Effective Schedule Use Case
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Resolves the effective schedule for a script following priority:
|
|
7
|
+
* 1. Database override (runtime configuration)
|
|
8
|
+
* 2. Definition default (code-defined schedule)
|
|
9
|
+
* 3. None (manual execution only)
|
|
10
|
+
*/
|
|
11
|
+
class GetEffectiveScheduleUseCase {
|
|
12
|
+
constructor({ commands, scriptFactory }) {
|
|
13
|
+
this.commands = commands;
|
|
14
|
+
this.scriptFactory = scriptFactory;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get effective schedule for a script
|
|
19
|
+
* @param {string} scriptName - Name of the script
|
|
20
|
+
* @returns {Promise<{source: 'database'|'definition'|'none', schedule: Object}>}
|
|
21
|
+
*/
|
|
22
|
+
async execute(scriptName) {
|
|
23
|
+
this._validateScriptExists(scriptName);
|
|
24
|
+
|
|
25
|
+
// Priority 1: Database override
|
|
26
|
+
const dbSchedule = await this.commands.getScheduleByScriptName(scriptName);
|
|
27
|
+
if (dbSchedule) {
|
|
28
|
+
return {
|
|
29
|
+
source: 'database',
|
|
30
|
+
schedule: dbSchedule,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Priority 2: Definition default
|
|
35
|
+
const definitionSchedule = this._getDefinitionSchedule(scriptName);
|
|
36
|
+
if (definitionSchedule?.enabled) {
|
|
37
|
+
return {
|
|
38
|
+
source: 'definition',
|
|
39
|
+
schedule: {
|
|
40
|
+
scriptName,
|
|
41
|
+
enabled: definitionSchedule.enabled,
|
|
42
|
+
cronExpression: definitionSchedule.cronExpression,
|
|
43
|
+
timezone: definitionSchedule.timezone || 'UTC',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Priority 3: No schedule
|
|
49
|
+
return {
|
|
50
|
+
source: 'none',
|
|
51
|
+
schedule: {
|
|
52
|
+
scriptName,
|
|
53
|
+
enabled: false,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
_validateScriptExists(scriptName) {
|
|
62
|
+
if (!this.scriptFactory.has(scriptName)) {
|
|
63
|
+
const error = new Error(`Script "${scriptName}" not found`);
|
|
64
|
+
error.code = 'SCRIPT_NOT_FOUND';
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
_getDefinitionSchedule(scriptName) {
|
|
73
|
+
const scriptClass = this.scriptFactory.get(scriptName);
|
|
74
|
+
return scriptClass.Definition?.schedule || null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { GetEffectiveScheduleUseCase };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedule Management Use Cases
|
|
3
|
+
*
|
|
4
|
+
* Separated by Single Responsibility Principle:
|
|
5
|
+
* - GetEffectiveScheduleUseCase: Read schedule with priority resolution
|
|
6
|
+
* - UpsertScheduleUseCase: Create/update schedule with scheduler sync
|
|
7
|
+
* - DeleteScheduleUseCase: Delete schedule with scheduler cleanup
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { GetEffectiveScheduleUseCase } = require('./get-effective-schedule-use-case');
|
|
11
|
+
const { UpsertScheduleUseCase } = require('./upsert-schedule-use-case');
|
|
12
|
+
const { DeleteScheduleUseCase } = require('./delete-schedule-use-case');
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
GetEffectiveScheduleUseCase,
|
|
16
|
+
UpsertScheduleUseCase,
|
|
17
|
+
DeleteScheduleUseCase,
|
|
18
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upsert Schedule Use Case
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Creates or updates a schedule override with external scheduler provisioning.
|
|
7
|
+
* Abstracts scheduler provider (AWS EventBridge, etc.) behind schedulerAdapter.
|
|
8
|
+
*/
|
|
9
|
+
class UpsertScheduleUseCase {
|
|
10
|
+
constructor({ commands, schedulerAdapter, scriptFactory }) {
|
|
11
|
+
this.commands = commands;
|
|
12
|
+
this.schedulerAdapter = schedulerAdapter;
|
|
13
|
+
this.scriptFactory = scriptFactory;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create or update a schedule
|
|
18
|
+
* @param {string} scriptName - Name of the script
|
|
19
|
+
* @param {Object} input - Schedule configuration
|
|
20
|
+
* @param {boolean} input.enabled - Whether schedule is enabled
|
|
21
|
+
* @param {string} [input.cronExpression] - Cron expression (required if enabled)
|
|
22
|
+
* @param {string} [input.timezone] - Timezone (defaults to UTC)
|
|
23
|
+
* @returns {Promise<{success: boolean, schedule: Object, schedulerWarning?: string}>}
|
|
24
|
+
*/
|
|
25
|
+
async execute(scriptName, { enabled, cronExpression, timezone }) {
|
|
26
|
+
this._validateScriptExists(scriptName);
|
|
27
|
+
this._validateInput(enabled, cronExpression);
|
|
28
|
+
|
|
29
|
+
// Save to database
|
|
30
|
+
const schedule = await this.commands.upsertSchedule({
|
|
31
|
+
scriptName,
|
|
32
|
+
enabled,
|
|
33
|
+
cronExpression: cronExpression || null,
|
|
34
|
+
timezone: timezone || 'UTC',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Sync with external scheduler (AWS EventBridge, etc.)
|
|
38
|
+
const schedulerResult = await this._syncExternalScheduler(
|
|
39
|
+
scriptName,
|
|
40
|
+
enabled,
|
|
41
|
+
cronExpression,
|
|
42
|
+
timezone,
|
|
43
|
+
schedule.externalScheduleId
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
schedule: {
|
|
49
|
+
...schedule,
|
|
50
|
+
externalScheduleId: schedulerResult.externalScheduleId || schedule.externalScheduleId,
|
|
51
|
+
externalScheduleName: schedulerResult.externalScheduleName || schedule.externalScheduleName,
|
|
52
|
+
},
|
|
53
|
+
...(schedulerResult.warning && { schedulerWarning: schedulerResult.warning }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @private
|
|
59
|
+
*/
|
|
60
|
+
_validateScriptExists(scriptName) {
|
|
61
|
+
if (!this.scriptFactory.has(scriptName)) {
|
|
62
|
+
const error = new Error(`Script "${scriptName}" not found`);
|
|
63
|
+
error.code = 'SCRIPT_NOT_FOUND';
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
_validateInput(enabled, cronExpression) {
|
|
72
|
+
if (typeof enabled !== 'boolean') {
|
|
73
|
+
const error = new Error('enabled must be a boolean');
|
|
74
|
+
error.code = 'INVALID_INPUT';
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (enabled && !cronExpression) {
|
|
79
|
+
const error = new Error('cronExpression is required when enabled is true');
|
|
80
|
+
error.code = 'INVALID_INPUT';
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sync with external scheduler service
|
|
87
|
+
* Abstracts AWS EventBridge or other scheduler providers
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
async _syncExternalScheduler(scriptName, enabled, cronExpression, timezone, existingId) {
|
|
91
|
+
const result = { externalScheduleId: null, externalScheduleName: null, warning: null };
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
if (enabled && cronExpression) {
|
|
95
|
+
// Create/update external schedule
|
|
96
|
+
const schedulerInfo = await this.schedulerAdapter.createSchedule({
|
|
97
|
+
scriptName,
|
|
98
|
+
cronExpression,
|
|
99
|
+
timezone: timezone || 'UTC',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (schedulerInfo?.scheduleArn) {
|
|
103
|
+
await this.commands.updateScheduleExternalInfo(scriptName, {
|
|
104
|
+
externalScheduleId: schedulerInfo.scheduleArn,
|
|
105
|
+
externalScheduleName: schedulerInfo.scheduleName,
|
|
106
|
+
});
|
|
107
|
+
result.externalScheduleId = schedulerInfo.scheduleArn;
|
|
108
|
+
result.externalScheduleName = schedulerInfo.scheduleName;
|
|
109
|
+
}
|
|
110
|
+
} else if (!enabled && existingId) {
|
|
111
|
+
// Delete external schedule
|
|
112
|
+
await this.schedulerAdapter.deleteSchedule(scriptName);
|
|
113
|
+
await this.commands.updateScheduleExternalInfo(scriptName, {
|
|
114
|
+
externalScheduleId: null,
|
|
115
|
+
externalScheduleName: null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// Non-fatal: DB schedule is saved, external scheduler can be retried
|
|
120
|
+
result.warning = error.message;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { UpsertScheduleUseCase };
|
|
@@ -6,7 +6,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
6
6
|
expect(IntegrationHealthCheckScript.Definition.name).toBe('integration-health-check');
|
|
7
7
|
expect(IntegrationHealthCheckScript.Definition.version).toBe('1.0.0');
|
|
8
8
|
expect(IntegrationHealthCheckScript.Definition.source).toBe('BUILTIN');
|
|
9
|
-
expect(IntegrationHealthCheckScript.Definition.config.
|
|
9
|
+
expect(IntegrationHealthCheckScript.Definition.config.requireIntegrationInstance).toBe(true);
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
it('should have valid input schema', () => {
|
|
@@ -6,7 +6,7 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
6
6
|
expect(OAuthTokenRefreshScript.Definition.name).toBe('oauth-token-refresh');
|
|
7
7
|
expect(OAuthTokenRefreshScript.Definition.version).toBe('1.0.0');
|
|
8
8
|
expect(OAuthTokenRefreshScript.Definition.source).toBe('BUILTIN');
|
|
9
|
-
expect(OAuthTokenRefreshScript.Definition.config.
|
|
9
|
+
expect(OAuthTokenRefreshScript.Definition.config.requireIntegrationInstance).toBe(true);
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
it('should have valid input schema', () => {
|
|
@@ -47,7 +47,7 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
47
47
|
config: {
|
|
48
48
|
timeout: 600000, // 10 minutes
|
|
49
49
|
maxRetries: 1,
|
|
50
|
-
|
|
50
|
+
requireIntegrationInstance: true, // Needs to call external APIs
|
|
51
51
|
},
|
|
52
52
|
|
|
53
53
|
display: {
|
|
@@ -99,7 +99,7 @@ describe('Admin Script Router', () => {
|
|
|
99
99
|
version: '1.0.0',
|
|
100
100
|
description: 'Test script',
|
|
101
101
|
category: 'test',
|
|
102
|
-
|
|
102
|
+
requireIntegrationInstance: false,
|
|
103
103
|
schedule: null,
|
|
104
104
|
});
|
|
105
105
|
});
|
|
@@ -293,8 +293,8 @@ describe('Admin Script Router', () => {
|
|
|
293
293
|
timezone: 'America/New_York',
|
|
294
294
|
lastTriggeredAt: new Date('2025-01-01T09:00:00Z'),
|
|
295
295
|
nextTriggerAt: new Date('2025-01-02T09:00:00Z'),
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
externalScheduleId: 'arn:aws:events:us-east-1:123456789012:rule/test',
|
|
297
|
+
externalScheduleName: 'test-script-schedule',
|
|
298
298
|
createdAt: new Date('2025-01-01T00:00:00Z'),
|
|
299
299
|
updatedAt: new Date('2025-01-01T00:00:00Z'),
|
|
300
300
|
};
|
|
@@ -470,7 +470,7 @@ describe('Admin Script Router', () => {
|
|
|
470
470
|
};
|
|
471
471
|
|
|
472
472
|
mockCommands.upsertSchedule = jest.fn().mockResolvedValue(newSchedule);
|
|
473
|
-
mockCommands.
|
|
473
|
+
mockCommands.updateScheduleExternalInfo = jest.fn().mockResolvedValue(newSchedule);
|
|
474
474
|
mockSchedulerAdapter.createSchedule.mockResolvedValue({
|
|
475
475
|
scheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
476
476
|
scheduleName: 'frigg-script-test-script',
|
|
@@ -490,11 +490,11 @@ describe('Admin Script Router', () => {
|
|
|
490
490
|
cronExpression: '0 12 * * *',
|
|
491
491
|
timezone: 'America/Los_Angeles',
|
|
492
492
|
});
|
|
493
|
-
expect(mockCommands.
|
|
494
|
-
|
|
495
|
-
|
|
493
|
+
expect(mockCommands.updateScheduleExternalInfo).toHaveBeenCalledWith('test-script', {
|
|
494
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
495
|
+
externalScheduleName: 'frigg-script-test-script',
|
|
496
496
|
});
|
|
497
|
-
expect(response.body.schedule.
|
|
497
|
+
expect(response.body.schedule.externalScheduleId).toBe('arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script');
|
|
498
498
|
});
|
|
499
499
|
|
|
500
500
|
it('should delete EventBridge schedule when disabling existing schedule', async () => {
|
|
@@ -503,14 +503,14 @@ describe('Admin Script Router', () => {
|
|
|
503
503
|
enabled: false,
|
|
504
504
|
cronExpression: null,
|
|
505
505
|
timezone: 'UTC',
|
|
506
|
-
|
|
507
|
-
|
|
506
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
507
|
+
externalScheduleName: 'frigg-script-test-script',
|
|
508
508
|
createdAt: new Date(),
|
|
509
509
|
updatedAt: new Date(),
|
|
510
510
|
};
|
|
511
511
|
|
|
512
512
|
mockCommands.upsertSchedule = jest.fn().mockResolvedValue(existingSchedule);
|
|
513
|
-
mockCommands.
|
|
513
|
+
mockCommands.updateScheduleExternalInfo = jest.fn().mockResolvedValue(existingSchedule);
|
|
514
514
|
mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
|
|
515
515
|
|
|
516
516
|
const response = await request(app)
|
|
@@ -521,9 +521,9 @@ describe('Admin Script Router', () => {
|
|
|
521
521
|
|
|
522
522
|
expect(response.status).toBe(200);
|
|
523
523
|
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
|
|
524
|
-
expect(mockCommands.
|
|
525
|
-
|
|
526
|
-
|
|
524
|
+
expect(mockCommands.updateScheduleExternalInfo).toHaveBeenCalledWith('test-script', {
|
|
525
|
+
externalScheduleId: null,
|
|
526
|
+
externalScheduleName: null,
|
|
527
527
|
});
|
|
528
528
|
});
|
|
529
529
|
|
|
@@ -632,7 +632,7 @@ describe('Admin Script Router', () => {
|
|
|
632
632
|
expect(response.body.code).toBe('SCRIPT_NOT_FOUND');
|
|
633
633
|
});
|
|
634
634
|
|
|
635
|
-
it('should delete EventBridge schedule when
|
|
635
|
+
it('should delete EventBridge schedule when external rule exists', async () => {
|
|
636
636
|
mockCommands.deleteSchedule = jest.fn().mockResolvedValue({
|
|
637
637
|
acknowledged: true,
|
|
638
638
|
deletedCount: 1,
|
|
@@ -640,8 +640,8 @@ describe('Admin Script Router', () => {
|
|
|
640
640
|
scriptName: 'test-script',
|
|
641
641
|
enabled: true,
|
|
642
642
|
cronExpression: '0 12 * * *',
|
|
643
|
-
|
|
644
|
-
|
|
643
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
644
|
+
externalScheduleName: 'frigg-script-test-script',
|
|
645
645
|
},
|
|
646
646
|
});
|
|
647
647
|
mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
|
|
@@ -654,7 +654,7 @@ describe('Admin Script Router', () => {
|
|
|
654
654
|
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
|
|
655
655
|
});
|
|
656
656
|
|
|
657
|
-
it('should not call scheduler when no
|
|
657
|
+
it('should not call scheduler when no external rule exists', async () => {
|
|
658
658
|
mockCommands.deleteSchedule = jest.fn().mockResolvedValue({
|
|
659
659
|
acknowledged: true,
|
|
660
660
|
deletedCount: 1,
|
|
@@ -662,7 +662,7 @@ describe('Admin Script Router', () => {
|
|
|
662
662
|
scriptName: 'test-script',
|
|
663
663
|
enabled: true,
|
|
664
664
|
cronExpression: '0 12 * * *',
|
|
665
|
-
// No
|
|
665
|
+
// No externalScheduleId
|
|
666
666
|
},
|
|
667
667
|
});
|
|
668
668
|
|
|
@@ -682,10 +682,10 @@ describe('Admin Script Router', () => {
|
|
|
682
682
|
scriptName: 'test-script',
|
|
683
683
|
enabled: true,
|
|
684
684
|
cronExpression: '0 12 * * *',
|
|
685
|
-
|
|
685
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
686
686
|
},
|
|
687
687
|
});
|
|
688
|
-
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(new Error('
|
|
688
|
+
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(new Error('Scheduler delete failed'));
|
|
689
689
|
|
|
690
690
|
const response = await request(app).delete(
|
|
691
691
|
'/admin/scripts/test-script/schedule'
|
|
@@ -694,7 +694,7 @@ describe('Admin Script Router', () => {
|
|
|
694
694
|
// Request should succeed despite scheduler error
|
|
695
695
|
expect(response.status).toBe(200);
|
|
696
696
|
expect(response.body.success).toBe(true);
|
|
697
|
-
expect(response.body.schedulerWarning).toBe('
|
|
697
|
+
expect(response.body.schedulerWarning).toBe('Scheduler delete failed');
|
|
698
698
|
});
|
|
699
699
|
});
|
|
700
700
|
});
|
|
@@ -6,7 +6,11 @@ const { createScriptRunner } = require('../application/script-runner');
|
|
|
6
6
|
const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
|
|
7
7
|
const { QueuerUtil } = require('@friggframework/core/queues');
|
|
8
8
|
const { createSchedulerAdapter } = require('../adapters/scheduler-adapter-factory');
|
|
9
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
GetEffectiveScheduleUseCase,
|
|
11
|
+
UpsertScheduleUseCase,
|
|
12
|
+
DeleteScheduleUseCase,
|
|
13
|
+
} = require('../application/use-cases');
|
|
10
14
|
|
|
11
15
|
const router = express.Router();
|
|
12
16
|
|
|
@@ -14,15 +18,19 @@ const router = express.Router();
|
|
|
14
18
|
router.use(validateAdminApiKey);
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
|
-
* Create
|
|
21
|
+
* Create schedule use case instances
|
|
18
22
|
* @private
|
|
19
23
|
*/
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
function createScheduleUseCases() {
|
|
25
|
+
const commands = createAdminScriptCommands();
|
|
26
|
+
const schedulerAdapter = createSchedulerAdapter();
|
|
27
|
+
const scriptFactory = getScriptFactory();
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
getEffectiveSchedule: new GetEffectiveScheduleUseCase({ commands, scriptFactory }),
|
|
31
|
+
upsertSchedule: new UpsertScheduleUseCase({ commands, schedulerAdapter, scriptFactory }),
|
|
32
|
+
deleteSchedule: new DeleteScheduleUseCase({ commands, schedulerAdapter, scriptFactory }),
|
|
33
|
+
};
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/**
|
|
@@ -40,8 +48,8 @@ router.get('/scripts', async (req, res) => {
|
|
|
40
48
|
version: s.definition.version,
|
|
41
49
|
description: s.definition.description,
|
|
42
50
|
category: s.definition.display?.category || 'custom',
|
|
43
|
-
|
|
44
|
-
s.definition.config?.
|
|
51
|
+
requireIntegrationInstance:
|
|
52
|
+
s.definition.config?.requireIntegrationInstance || false,
|
|
45
53
|
schedule: s.definition.schedule || null,
|
|
46
54
|
})),
|
|
47
55
|
});
|
|
@@ -211,9 +219,9 @@ router.get('/scripts/:scriptName/executions', async (req, res) => {
|
|
|
211
219
|
router.get('/scripts/:scriptName/schedule', async (req, res) => {
|
|
212
220
|
try {
|
|
213
221
|
const { scriptName } = req.params;
|
|
214
|
-
const
|
|
222
|
+
const { getEffectiveSchedule } = createScheduleUseCases();
|
|
215
223
|
|
|
216
|
-
const result = await
|
|
224
|
+
const result = await getEffectiveSchedule.execute(scriptName);
|
|
217
225
|
|
|
218
226
|
res.json({
|
|
219
227
|
source: result.source,
|
|
@@ -240,9 +248,9 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
|
|
|
240
248
|
try {
|
|
241
249
|
const { scriptName } = req.params;
|
|
242
250
|
const { enabled, cronExpression, timezone } = req.body;
|
|
243
|
-
const
|
|
251
|
+
const { upsertSchedule } = createScheduleUseCases();
|
|
244
252
|
|
|
245
|
-
const result = await
|
|
253
|
+
const result = await upsertSchedule.execute(scriptName, {
|
|
246
254
|
enabled,
|
|
247
255
|
cronExpression,
|
|
248
256
|
timezone,
|
|
@@ -281,9 +289,9 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
|
|
|
281
289
|
router.delete('/scripts/:scriptName/schedule', async (req, res) => {
|
|
282
290
|
try {
|
|
283
291
|
const { scriptName } = req.params;
|
|
284
|
-
const
|
|
292
|
+
const { deleteSchedule } = createScheduleUseCases();
|
|
285
293
|
|
|
286
|
-
const result = await
|
|
294
|
+
const result = await deleteSchedule.execute(scriptName);
|
|
287
295
|
|
|
288
296
|
res.json(result);
|
|
289
297
|
} catch (error) {
|