@friggframework/admin-scripts 2.0.0--canary.522.cbd3d5a.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/index.js +2 -2
- package/package.json +6 -9
- package/src/application/__tests__/admin-frigg-commands.test.js +19 -19
- package/src/application/__tests__/admin-script-base.test.js +2 -2
- package/src/application/__tests__/script-runner.test.js +146 -16
- package/src/application/admin-frigg-commands.js +8 -8
- package/src/application/admin-script-base.js +3 -5
- package/src/application/script-runner.js +125 -129
- 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-auth-middleware.test.js +32 -95
- package/src/infrastructure/__tests__/admin-script-router.test.js +46 -47
- package/src/infrastructure/admin-auth-middleware.js +5 -43
- package/src/infrastructure/admin-script-router.js +38 -32
- package/src/infrastructure/script-executor-handler.js +2 -2
- 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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
const { UpsertScheduleUseCase } = require('../upsert-schedule-use-case');
|
|
2
|
+
|
|
3
|
+
describe('UpsertScheduleUseCase', () => {
|
|
4
|
+
let useCase;
|
|
5
|
+
let mockCommands;
|
|
6
|
+
let mockSchedulerAdapter;
|
|
7
|
+
let mockScriptFactory;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockCommands = {
|
|
11
|
+
upsertSchedule: jest.fn(),
|
|
12
|
+
updateScheduleExternalInfo: jest.fn(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
mockSchedulerAdapter = {
|
|
16
|
+
createSchedule: jest.fn(),
|
|
17
|
+
deleteSchedule: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
mockScriptFactory = {
|
|
21
|
+
has: jest.fn(),
|
|
22
|
+
get: jest.fn(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
useCase = new UpsertScheduleUseCase({
|
|
26
|
+
commands: mockCommands,
|
|
27
|
+
schedulerAdapter: mockSchedulerAdapter,
|
|
28
|
+
scriptFactory: mockScriptFactory,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('execute', () => {
|
|
33
|
+
it('should create schedule and provision external scheduler when enabled', async () => {
|
|
34
|
+
const savedSchedule = {
|
|
35
|
+
scriptName: 'test-script',
|
|
36
|
+
enabled: true,
|
|
37
|
+
cronExpression: '0 12 * * *',
|
|
38
|
+
timezone: 'UTC',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
42
|
+
mockCommands.upsertSchedule.mockResolvedValue(savedSchedule);
|
|
43
|
+
mockSchedulerAdapter.createSchedule.mockResolvedValue({
|
|
44
|
+
scheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
45
|
+
scheduleName: 'frigg-script-test-script',
|
|
46
|
+
});
|
|
47
|
+
mockCommands.updateScheduleExternalInfo.mockResolvedValue({
|
|
48
|
+
...savedSchedule,
|
|
49
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = await useCase.execute('test-script', {
|
|
53
|
+
enabled: true,
|
|
54
|
+
cronExpression: '0 12 * * *',
|
|
55
|
+
timezone: 'UTC',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result.success).toBe(true);
|
|
59
|
+
expect(result.schedule.scriptName).toBe('test-script');
|
|
60
|
+
expect(mockSchedulerAdapter.createSchedule).toHaveBeenCalledWith({
|
|
61
|
+
scriptName: 'test-script',
|
|
62
|
+
cronExpression: '0 12 * * *',
|
|
63
|
+
timezone: 'UTC',
|
|
64
|
+
});
|
|
65
|
+
expect(mockCommands.updateScheduleExternalInfo).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should default timezone to UTC', async () => {
|
|
69
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
70
|
+
mockCommands.upsertSchedule.mockResolvedValue({
|
|
71
|
+
scriptName: 'test-script',
|
|
72
|
+
enabled: true,
|
|
73
|
+
cronExpression: '0 12 * * *',
|
|
74
|
+
timezone: 'UTC',
|
|
75
|
+
});
|
|
76
|
+
mockSchedulerAdapter.createSchedule.mockResolvedValue({
|
|
77
|
+
scheduleArn: 'arn:test',
|
|
78
|
+
scheduleName: 'test',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await useCase.execute('test-script', {
|
|
82
|
+
enabled: true,
|
|
83
|
+
cronExpression: '0 12 * * *',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(mockSchedulerAdapter.createSchedule).toHaveBeenCalledWith({
|
|
87
|
+
scriptName: 'test-script',
|
|
88
|
+
cronExpression: '0 12 * * *',
|
|
89
|
+
timezone: 'UTC',
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should delete external scheduler when disabling', async () => {
|
|
94
|
+
const existingSchedule = {
|
|
95
|
+
scriptName: 'test-script',
|
|
96
|
+
enabled: false,
|
|
97
|
+
cronExpression: null,
|
|
98
|
+
timezone: 'UTC',
|
|
99
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
103
|
+
mockCommands.upsertSchedule.mockResolvedValue(existingSchedule);
|
|
104
|
+
mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
|
|
105
|
+
mockCommands.updateScheduleExternalInfo.mockResolvedValue({
|
|
106
|
+
...existingSchedule,
|
|
107
|
+
externalScheduleId: null,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await useCase.execute('test-script', {
|
|
111
|
+
enabled: false,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result.success).toBe(true);
|
|
115
|
+
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle scheduler errors gracefully with warning', async () => {
|
|
119
|
+
const savedSchedule = {
|
|
120
|
+
scriptName: 'test-script',
|
|
121
|
+
enabled: true,
|
|
122
|
+
cronExpression: '0 12 * * *',
|
|
123
|
+
timezone: 'UTC',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
127
|
+
mockCommands.upsertSchedule.mockResolvedValue(savedSchedule);
|
|
128
|
+
mockSchedulerAdapter.createSchedule.mockRejectedValue(
|
|
129
|
+
new Error('Scheduler API error')
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const result = await useCase.execute('test-script', {
|
|
133
|
+
enabled: true,
|
|
134
|
+
cronExpression: '0 12 * * *',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Should succeed with warning, not fail
|
|
138
|
+
expect(result.success).toBe(true);
|
|
139
|
+
expect(result.schedulerWarning).toBe('Scheduler API error');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should throw SCRIPT_NOT_FOUND error when script does not exist', async () => {
|
|
143
|
+
mockScriptFactory.has.mockReturnValue(false);
|
|
144
|
+
|
|
145
|
+
await expect(useCase.execute('non-existent', { enabled: true }))
|
|
146
|
+
.rejects.toThrow('Script "non-existent" not found');
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await useCase.execute('non-existent', { enabled: true });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
expect(error.code).toBe('SCRIPT_NOT_FOUND');
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should throw INVALID_INPUT error when enabled is not a boolean', async () => {
|
|
156
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
157
|
+
|
|
158
|
+
await expect(useCase.execute('test-script', { enabled: 'yes' }))
|
|
159
|
+
.rejects.toThrow('enabled must be a boolean');
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await useCase.execute('test-script', { enabled: 'yes' });
|
|
163
|
+
} catch (error) {
|
|
164
|
+
expect(error.code).toBe('INVALID_INPUT');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should throw INVALID_INPUT error when enabled without cronExpression', async () => {
|
|
169
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
170
|
+
|
|
171
|
+
await expect(useCase.execute('test-script', { enabled: true }))
|
|
172
|
+
.rejects.toThrow('cronExpression is required when enabled is true');
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await useCase.execute('test-script', { enabled: true });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
expect(error.code).toBe('INVALID_INPUT');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should not require cronExpression when disabled', async () => {
|
|
182
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
183
|
+
mockCommands.upsertSchedule.mockResolvedValue({
|
|
184
|
+
scriptName: 'test-script',
|
|
185
|
+
enabled: false,
|
|
186
|
+
cronExpression: null,
|
|
187
|
+
timezone: 'UTC',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const result = await useCase.execute('test-script', { enabled: false });
|
|
191
|
+
|
|
192
|
+
expect(result.success).toBe(true);
|
|
193
|
+
expect(mockCommands.upsertSchedule).toHaveBeenCalledWith({
|
|
194
|
+
scriptName: 'test-script',
|
|
195
|
+
enabled: false,
|
|
196
|
+
cronExpression: null,
|
|
197
|
+
timezone: 'UTC',
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -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: {
|