@friggframework/admin-scripts 2.0.0--canary.517.ff03f2c.0 → 2.0.0--canary.517.542a9ec.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 +3 -3
- package/package.json +7 -6
- package/src/application/__tests__/admin-script-base.test.js +2 -8
- package/src/application/admin-script-base.js +2 -4
- package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +5 -30
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +5 -38
- package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +3 -3
- package/src/application/use-cases/delete-schedule-use-case.js +8 -38
- package/src/application/use-cases/get-effective-schedule-use-case.js +12 -31
- package/src/application/use-cases/upsert-schedule-use-case.js +5 -9
- package/src/infrastructure/__tests__/admin-script-router.test.js +13 -16
- package/src/infrastructure/admin-script-router.js +20 -51
package/README.md
CHANGED
|
@@ -76,8 +76,8 @@ class AttioHealingScript extends AdminScriptBase {
|
|
|
76
76
|
requireIntegrationInstance: true, // needs this.context.instantiate()
|
|
77
77
|
},
|
|
78
78
|
|
|
79
|
-
//
|
|
80
|
-
|
|
79
|
+
// No schedule here — a script is a capability. An admin activates a
|
|
80
|
+
// recurring run at runtime via PUT /admin/scripts/:name/schedule.
|
|
81
81
|
|
|
82
82
|
display: { category: 'maintenance' },
|
|
83
83
|
};
|
|
@@ -203,7 +203,7 @@ curl "https://<your-app>/admin/scripts/attio-healing/executions?status=FAILED&li
|
|
|
203
203
|
|
|
204
204
|
## Scheduling
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
Scripts don't declare a schedule — a script is a capability. An admin activates a recurring run at runtime via `PUT .../schedule`, which persists the schedule and provisions it in EventBridge. The **effective** schedule is therefore either the runtime override (DB) or none — there is no code-defined default that fires on its own.
|
|
207
207
|
|
|
208
208
|
```bash
|
|
209
209
|
# Enable a daily 6am UTC run
|
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/admin-scripts",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.517.
|
|
4
|
+
"version": "2.0.0--canary.517.542a9ec.0",
|
|
5
5
|
"description": "Admin Script Runner for Frigg - Execute maintenance and operational scripts in hosted environments",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@aws-sdk/client-scheduler": "^3.588.0",
|
|
8
|
-
"@friggframework/core": "2.0.0--canary.517.
|
|
8
|
+
"@friggframework/core": "2.0.0--canary.517.542a9ec.0",
|
|
9
|
+
"@hapi/boom": "^10.0.1",
|
|
9
10
|
"express": "^4.18.2",
|
|
10
11
|
"serverless-http": "^3.2.0"
|
|
11
12
|
},
|
|
12
13
|
"devDependencies": {
|
|
13
|
-
"@friggframework/eslint-config": "2.0.0--canary.517.
|
|
14
|
-
"@friggframework/prettier-config": "2.0.0--canary.517.
|
|
15
|
-
"@friggframework/test": "2.0.0--canary.517.
|
|
14
|
+
"@friggframework/eslint-config": "2.0.0--canary.517.542a9ec.0",
|
|
15
|
+
"@friggframework/prettier-config": "2.0.0--canary.517.542a9ec.0",
|
|
16
|
+
"@friggframework/test": "2.0.0--canary.517.542a9ec.0",
|
|
16
17
|
"eslint": "^8.22.0",
|
|
17
18
|
"jest": "^29.7.0",
|
|
18
19
|
"prettier": "^2.7.1",
|
|
@@ -43,5 +44,5 @@
|
|
|
43
44
|
"maintenance",
|
|
44
45
|
"operations"
|
|
45
46
|
],
|
|
46
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "542a9ec92adca85b29dd727125bbeb5d7e688dbc"
|
|
47
48
|
}
|
|
@@ -18,16 +18,11 @@ describe('AdminScriptBase', () => {
|
|
|
18
18
|
name: 'test-script',
|
|
19
19
|
version: '1.0.0',
|
|
20
20
|
description: 'A test script',
|
|
21
|
-
source: '
|
|
21
|
+
source: 'USER_DEFINED',
|
|
22
22
|
inputSchema: { type: 'object' },
|
|
23
23
|
outputSchema: { type: 'object' },
|
|
24
|
-
schedule: {
|
|
25
|
-
enabled: true,
|
|
26
|
-
cronExpression: 'cron(0 12 * * ? *)',
|
|
27
|
-
},
|
|
28
24
|
config: {
|
|
29
25
|
timeout: 600000,
|
|
30
|
-
maxRetries: 3,
|
|
31
26
|
requireIntegrationInstance: true,
|
|
32
27
|
},
|
|
33
28
|
display: {
|
|
@@ -40,8 +35,7 @@ describe('AdminScriptBase', () => {
|
|
|
40
35
|
expect(TestScript.Definition.name).toBe('test-script');
|
|
41
36
|
expect(TestScript.Definition.version).toBe('1.0.0');
|
|
42
37
|
expect(TestScript.Definition.description).toBe('A test script');
|
|
43
|
-
expect(TestScript.Definition.source).toBe('
|
|
44
|
-
expect(TestScript.Definition.schedule.enabled).toBe(true);
|
|
38
|
+
expect(TestScript.Definition.source).toBe('USER_DEFINED');
|
|
45
39
|
expect(TestScript.Definition.config.timeout).toBe(600000);
|
|
46
40
|
});
|
|
47
41
|
|
|
@@ -8,10 +8,8 @@ class AdminScriptBase {
|
|
|
8
8
|
inputSchema: null,
|
|
9
9
|
outputSchema: null,
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
cronExpression: null,
|
|
14
|
-
},
|
|
11
|
+
// Scheduling is not declared here — a script is a capability. Admins
|
|
12
|
+
// activate a schedule at runtime via PUT /admin/scripts/:name/schedule.
|
|
15
13
|
|
|
16
14
|
config: {
|
|
17
15
|
timeout: 300000,
|
|
@@ -88,33 +88,7 @@ describe('DeleteScheduleUseCase', () => {
|
|
|
88
88
|
expect(result.schedulerWarning).toBe('Scheduler delete failed');
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
it('
|
|
92
|
-
const definitionSchedule = {
|
|
93
|
-
enabled: true,
|
|
94
|
-
cronExpression: '0 6 * * *',
|
|
95
|
-
timezone: 'America/Los_Angeles',
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
99
|
-
mockScriptFactory.get.mockReturnValue({
|
|
100
|
-
Definition: { schedule: definitionSchedule },
|
|
101
|
-
});
|
|
102
|
-
mockCommands.deleteSchedule.mockResolvedValue({
|
|
103
|
-
deletedCount: 1,
|
|
104
|
-
deleted: { scriptName: 'test-script' },
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const result = await useCase.execute('test-script');
|
|
108
|
-
|
|
109
|
-
expect(result.effectiveSchedule.source).toBe('definition');
|
|
110
|
-
expect(result.effectiveSchedule.enabled).toBe(true);
|
|
111
|
-
expect(result.effectiveSchedule.cronExpression).toBe('0 6 * * *');
|
|
112
|
-
expect(result.effectiveSchedule.timezone).toBe(
|
|
113
|
-
'America/Los_Angeles'
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should default timezone to UTC when not in definition', async () => {
|
|
91
|
+
it('always returns none after deletion, ignoring any Definition schedule', async () => {
|
|
118
92
|
mockScriptFactory.has.mockReturnValue(true);
|
|
119
93
|
mockScriptFactory.get.mockReturnValue({
|
|
120
94
|
Definition: {
|
|
@@ -128,10 +102,11 @@ describe('DeleteScheduleUseCase', () => {
|
|
|
128
102
|
|
|
129
103
|
const result = await useCase.execute('test-script');
|
|
130
104
|
|
|
131
|
-
expect(result.effectiveSchedule.
|
|
105
|
+
expect(result.effectiveSchedule.source).toBe('none');
|
|
106
|
+
expect(result.effectiveSchedule.enabled).toBe(false);
|
|
132
107
|
});
|
|
133
108
|
|
|
134
|
-
it('should return none as effective
|
|
109
|
+
it('should return none as effective after deletion', async () => {
|
|
135
110
|
mockScriptFactory.has.mockReturnValue(true);
|
|
136
111
|
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
137
112
|
mockCommands.deleteSchedule.mockResolvedValue({
|
|
@@ -170,7 +145,7 @@ describe('DeleteScheduleUseCase', () => {
|
|
|
170
145
|
try {
|
|
171
146
|
await useCase.execute('non-existent');
|
|
172
147
|
} catch (error) {
|
|
173
|
-
expect(error.
|
|
148
|
+
expect(error.output.statusCode).toBe(404);
|
|
174
149
|
}
|
|
175
150
|
});
|
|
176
151
|
});
|
|
@@ -42,41 +42,6 @@ describe('GetEffectiveScheduleUseCase', () => {
|
|
|
42
42
|
expect(result.schedule).toEqual(dbSchedule);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
it('should return definition schedule when no database override', async () => {
|
|
46
|
-
const definitionSchedule = {
|
|
47
|
-
enabled: true,
|
|
48
|
-
cronExpression: '0 12 * * *',
|
|
49
|
-
timezone: 'America/New_York',
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
53
|
-
mockScriptFactory.get.mockReturnValue({
|
|
54
|
-
Definition: { schedule: definitionSchedule },
|
|
55
|
-
});
|
|
56
|
-
mockCommands.getScheduleByScriptName.mockResolvedValue(null);
|
|
57
|
-
|
|
58
|
-
const result = await useCase.execute('test-script');
|
|
59
|
-
|
|
60
|
-
expect(result.source).toBe('definition');
|
|
61
|
-
expect(result.schedule.enabled).toBe(true);
|
|
62
|
-
expect(result.schedule.cronExpression).toBe('0 12 * * *');
|
|
63
|
-
expect(result.schedule.timezone).toBe('America/New_York');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should default timezone to UTC when not specified in definition', async () => {
|
|
67
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
68
|
-
mockScriptFactory.get.mockReturnValue({
|
|
69
|
-
Definition: {
|
|
70
|
-
schedule: { enabled: true, cronExpression: '0 12 * * *' },
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
mockCommands.getScheduleByScriptName.mockResolvedValue(null);
|
|
74
|
-
|
|
75
|
-
const result = await useCase.execute('test-script');
|
|
76
|
-
|
|
77
|
-
expect(result.schedule.timezone).toBe('UTC');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
45
|
it('should return none when no schedule configured', async () => {
|
|
81
46
|
mockScriptFactory.has.mockReturnValue(true);
|
|
82
47
|
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
@@ -89,10 +54,12 @@ describe('GetEffectiveScheduleUseCase', () => {
|
|
|
89
54
|
expect(result.schedule.scriptName).toBe('test-script');
|
|
90
55
|
});
|
|
91
56
|
|
|
92
|
-
it('
|
|
57
|
+
it('ignores any schedule declared in the Definition (DB override is the only source)', async () => {
|
|
93
58
|
mockScriptFactory.has.mockReturnValue(true);
|
|
94
59
|
mockScriptFactory.get.mockReturnValue({
|
|
95
|
-
Definition: {
|
|
60
|
+
Definition: {
|
|
61
|
+
schedule: { enabled: true, cronExpression: '0 12 * * *' },
|
|
62
|
+
},
|
|
96
63
|
});
|
|
97
64
|
mockCommands.getScheduleByScriptName.mockResolvedValue(null);
|
|
98
65
|
|
|
@@ -112,7 +79,7 @@ describe('GetEffectiveScheduleUseCase', () => {
|
|
|
112
79
|
try {
|
|
113
80
|
await useCase.execute('non-existent');
|
|
114
81
|
} catch (error) {
|
|
115
|
-
expect(error.
|
|
82
|
+
expect(error.output.statusCode).toBe(404);
|
|
116
83
|
}
|
|
117
84
|
});
|
|
118
85
|
});
|
|
@@ -153,7 +153,7 @@ describe('UpsertScheduleUseCase', () => {
|
|
|
153
153
|
try {
|
|
154
154
|
await useCase.execute('non-existent', { enabled: true });
|
|
155
155
|
} catch (error) {
|
|
156
|
-
expect(error.
|
|
156
|
+
expect(error.output.statusCode).toBe(404);
|
|
157
157
|
}
|
|
158
158
|
});
|
|
159
159
|
|
|
@@ -167,7 +167,7 @@ describe('UpsertScheduleUseCase', () => {
|
|
|
167
167
|
try {
|
|
168
168
|
await useCase.execute('test-script', { enabled: 'yes' });
|
|
169
169
|
} catch (error) {
|
|
170
|
-
expect(error.
|
|
170
|
+
expect(error.output.statusCode).toBe(400);
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
173
|
|
|
@@ -183,7 +183,7 @@ describe('UpsertScheduleUseCase', () => {
|
|
|
183
183
|
try {
|
|
184
184
|
await useCase.execute('test-script', { enabled: true });
|
|
185
185
|
} catch (error) {
|
|
186
|
-
expect(error.
|
|
186
|
+
expect(error.output.statusCode).toBe(400);
|
|
187
187
|
}
|
|
188
188
|
});
|
|
189
189
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Delete Schedule Use Case
|
|
3
5
|
*
|
|
4
6
|
* Application Layer - Hexagonal Architecture
|
|
5
7
|
*
|
|
6
8
|
* Deletes a schedule override and cleans up external scheduler resources.
|
|
7
|
-
*
|
|
9
|
+
* After deletion the script has no active schedule — schedules are never
|
|
10
|
+
* derived from the Definition; an admin re-activates one via PUT /schedule.
|
|
8
11
|
*/
|
|
9
12
|
class DeleteScheduleUseCase {
|
|
10
13
|
constructor({ commands, schedulerAdapter, scriptFactory }) {
|
|
@@ -30,9 +33,9 @@ class DeleteScheduleUseCase {
|
|
|
30
33
|
deleteResult.deleted?.externalScheduleId
|
|
31
34
|
);
|
|
32
35
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
// Nothing derives a schedule from the Definition, so once the DB
|
|
37
|
+
// override is removed the script has no active schedule.
|
|
38
|
+
const effectiveSchedule = { source: 'none', enabled: false };
|
|
36
39
|
|
|
37
40
|
return {
|
|
38
41
|
success: true,
|
|
@@ -51,41 +54,8 @@ class DeleteScheduleUseCase {
|
|
|
51
54
|
*/
|
|
52
55
|
_validateScriptExists(scriptName) {
|
|
53
56
|
if (!this.scriptFactory.has(scriptName)) {
|
|
54
|
-
|
|
55
|
-
error.code = 'SCRIPT_NOT_FOUND';
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get the definition schedule from a script class
|
|
62
|
-
* @private
|
|
63
|
-
*/
|
|
64
|
-
_getDefinitionSchedule(scriptName) {
|
|
65
|
-
const scriptClass = this.scriptFactory.get(scriptName);
|
|
66
|
-
return scriptClass.Definition?.schedule || null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Determine effective schedule after deletion
|
|
71
|
-
* @private
|
|
72
|
-
*/
|
|
73
|
-
_getEffectiveScheduleAfterDeletion(scriptName) {
|
|
74
|
-
const definitionSchedule = this._getDefinitionSchedule(scriptName);
|
|
75
|
-
|
|
76
|
-
if (definitionSchedule?.enabled) {
|
|
77
|
-
return {
|
|
78
|
-
source: 'definition',
|
|
79
|
-
enabled: definitionSchedule.enabled,
|
|
80
|
-
cronExpression: definitionSchedule.cronExpression,
|
|
81
|
-
timezone: definitionSchedule.timezone || 'UTC',
|
|
82
|
-
};
|
|
57
|
+
throw Boom.notFound(`Script "${scriptName}" not found`);
|
|
83
58
|
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
source: 'none',
|
|
87
|
-
enabled: false,
|
|
88
|
-
};
|
|
89
59
|
}
|
|
90
60
|
|
|
91
61
|
/**
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Get Effective Schedule Use Case
|
|
3
5
|
*
|
|
4
6
|
* Application Layer - Hexagonal Architecture
|
|
5
7
|
*
|
|
6
|
-
* Resolves the effective schedule for a script
|
|
7
|
-
* 1. Database override (runtime
|
|
8
|
-
* 2.
|
|
9
|
-
*
|
|
8
|
+
* Resolves the effective schedule for a script:
|
|
9
|
+
* 1. Database override (activated at runtime via PUT /schedule)
|
|
10
|
+
* 2. None (script is declared but not actively scheduled)
|
|
11
|
+
*
|
|
12
|
+
* Schedules are never derived from the script Definition — a script declares
|
|
13
|
+
* a capability; an admin activates a schedule explicitly via the API.
|
|
10
14
|
*/
|
|
11
15
|
class GetEffectiveScheduleUseCase {
|
|
12
16
|
constructor({ commands, scriptFactory }) {
|
|
@@ -17,7 +21,7 @@ class GetEffectiveScheduleUseCase {
|
|
|
17
21
|
/**
|
|
18
22
|
* Get effective schedule for a script
|
|
19
23
|
* @param {string} scriptName - Name of the script
|
|
20
|
-
* @returns {Promise<{source: 'database'|'
|
|
24
|
+
* @returns {Promise<{source: 'database'|'none', schedule: Object}>}
|
|
21
25
|
*/
|
|
22
26
|
async execute(scriptName) {
|
|
23
27
|
this._validateScriptExists(scriptName);
|
|
@@ -33,21 +37,8 @@ class GetEffectiveScheduleUseCase {
|
|
|
33
37
|
};
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
if (definitionSchedule?.enabled) {
|
|
39
|
-
return {
|
|
40
|
-
source: 'definition',
|
|
41
|
-
schedule: {
|
|
42
|
-
scriptName,
|
|
43
|
-
enabled: definitionSchedule.enabled,
|
|
44
|
-
cronExpression: definitionSchedule.cronExpression,
|
|
45
|
-
timezone: definitionSchedule.timezone || 'UTC',
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Priority 3: No schedule
|
|
40
|
+
// No database override: the script is declared but not actively
|
|
41
|
+
// scheduled. Admins activate scheduling explicitly via PUT /schedule.
|
|
51
42
|
return {
|
|
52
43
|
source: 'none',
|
|
53
44
|
schedule: {
|
|
@@ -62,19 +53,9 @@ class GetEffectiveScheduleUseCase {
|
|
|
62
53
|
*/
|
|
63
54
|
_validateScriptExists(scriptName) {
|
|
64
55
|
if (!this.scriptFactory.has(scriptName)) {
|
|
65
|
-
|
|
66
|
-
error.code = 'SCRIPT_NOT_FOUND';
|
|
67
|
-
throw error;
|
|
56
|
+
throw Boom.notFound(`Script "${scriptName}" not found`);
|
|
68
57
|
}
|
|
69
58
|
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* @private
|
|
73
|
-
*/
|
|
74
|
-
_getDefinitionSchedule(scriptName) {
|
|
75
|
-
const scriptClass = this.scriptFactory.get(scriptName);
|
|
76
|
-
return scriptClass.Definition?.schedule || null;
|
|
77
|
-
}
|
|
78
59
|
}
|
|
79
60
|
|
|
80
61
|
module.exports = { GetEffectiveScheduleUseCase };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Upsert Schedule Use Case
|
|
3
5
|
*
|
|
@@ -65,9 +67,7 @@ class UpsertScheduleUseCase {
|
|
|
65
67
|
*/
|
|
66
68
|
_validateScriptExists(scriptName) {
|
|
67
69
|
if (!this.scriptFactory.has(scriptName)) {
|
|
68
|
-
|
|
69
|
-
error.code = 'SCRIPT_NOT_FOUND';
|
|
70
|
-
throw error;
|
|
70
|
+
throw Boom.notFound(`Script "${scriptName}" not found`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -76,17 +76,13 @@ class UpsertScheduleUseCase {
|
|
|
76
76
|
*/
|
|
77
77
|
_validateInput(enabled, cronExpression) {
|
|
78
78
|
if (typeof enabled !== 'boolean') {
|
|
79
|
-
|
|
80
|
-
error.code = 'INVALID_INPUT';
|
|
81
|
-
throw error;
|
|
79
|
+
throw Boom.badRequest('enabled must be a boolean');
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
if (enabled && !cronExpression) {
|
|
85
|
-
|
|
83
|
+
throw Boom.badRequest(
|
|
86
84
|
'cronExpression is required when enabled is true'
|
|
87
85
|
);
|
|
88
|
-
error.code = 'INVALID_INPUT';
|
|
89
|
-
throw error;
|
|
90
86
|
}
|
|
91
87
|
}
|
|
92
88
|
|
|
@@ -107,7 +107,6 @@ describe('Admin Script Router', () => {
|
|
|
107
107
|
description: 'Test script',
|
|
108
108
|
category: 'test',
|
|
109
109
|
requireIntegrationInstance: false,
|
|
110
|
-
schedule: null,
|
|
111
110
|
});
|
|
112
111
|
});
|
|
113
112
|
|
|
@@ -352,12 +351,13 @@ describe('Admin Script Router', () => {
|
|
|
352
351
|
expect(response.body.timezone).toBe('America/New_York');
|
|
353
352
|
});
|
|
354
353
|
|
|
355
|
-
it('
|
|
354
|
+
it('ignores a schedule declared in the Definition (no DB override → none)', async () => {
|
|
356
355
|
mockCommands.getScheduleByScriptName = jest
|
|
357
356
|
.fn()
|
|
358
357
|
.mockResolvedValue(null);
|
|
359
358
|
|
|
360
|
-
//
|
|
359
|
+
// A script may still carry a schedule field, but it is never used —
|
|
360
|
+
// only a DB override (set via PUT) activates a schedule.
|
|
361
361
|
class ScheduledTestScript extends TestScript {
|
|
362
362
|
static Definition = {
|
|
363
363
|
...TestScript.Definition,
|
|
@@ -376,10 +376,8 @@ describe('Admin Script Router', () => {
|
|
|
376
376
|
);
|
|
377
377
|
|
|
378
378
|
expect(response.status).toBe(200);
|
|
379
|
-
expect(response.body.source).toBe('
|
|
380
|
-
expect(response.body.enabled).toBe(
|
|
381
|
-
expect(response.body.cronExpression).toBe('0 0 * * *');
|
|
382
|
-
expect(response.body.timezone).toBe('UTC');
|
|
379
|
+
expect(response.body.source).toBe('none');
|
|
380
|
+
expect(response.body.enabled).toBe(false);
|
|
383
381
|
});
|
|
384
382
|
|
|
385
383
|
it('should return none when no schedule configured', async () => {
|
|
@@ -404,7 +402,7 @@ describe('Admin Script Router', () => {
|
|
|
404
402
|
);
|
|
405
403
|
|
|
406
404
|
expect(response.status).toBe(404);
|
|
407
|
-
expect(response.body.
|
|
405
|
+
expect(response.body.error).toMatch(/not found/i);
|
|
408
406
|
});
|
|
409
407
|
});
|
|
410
408
|
|
|
@@ -481,7 +479,6 @@ describe('Admin Script Router', () => {
|
|
|
481
479
|
});
|
|
482
480
|
|
|
483
481
|
expect(response.status).toBe(400);
|
|
484
|
-
expect(response.body.code).toBe('INVALID_INPUT');
|
|
485
482
|
expect(response.body.error).toContain('enabled');
|
|
486
483
|
});
|
|
487
484
|
|
|
@@ -493,7 +490,6 @@ describe('Admin Script Router', () => {
|
|
|
493
490
|
});
|
|
494
491
|
|
|
495
492
|
expect(response.status).toBe(400);
|
|
496
|
-
expect(response.body.code).toBe('INVALID_INPUT');
|
|
497
493
|
expect(response.body.error).toContain('cronExpression');
|
|
498
494
|
});
|
|
499
495
|
|
|
@@ -508,7 +504,7 @@ describe('Admin Script Router', () => {
|
|
|
508
504
|
});
|
|
509
505
|
|
|
510
506
|
expect(response.status).toBe(404);
|
|
511
|
-
expect(response.body.
|
|
507
|
+
expect(response.body.error).toMatch(/not found/i);
|
|
512
508
|
});
|
|
513
509
|
|
|
514
510
|
it('should provision EventBridge schedule when enabled', async () => {
|
|
@@ -658,13 +654,14 @@ describe('Admin Script Router', () => {
|
|
|
658
654
|
);
|
|
659
655
|
});
|
|
660
656
|
|
|
661
|
-
it('
|
|
657
|
+
it('reports no active schedule after deleting the override', async () => {
|
|
662
658
|
mockCommands.deleteSchedule = jest.fn().mockResolvedValue({
|
|
663
659
|
acknowledged: true,
|
|
664
660
|
deletedCount: 1,
|
|
665
661
|
});
|
|
666
662
|
|
|
667
|
-
//
|
|
663
|
+
// Even if the script carries a schedule field, it is ignored —
|
|
664
|
+
// after removing the DB override there is no active schedule.
|
|
668
665
|
class ScheduledTestScript extends TestScript {
|
|
669
666
|
static Definition = {
|
|
670
667
|
...TestScript.Definition,
|
|
@@ -683,8 +680,8 @@ describe('Admin Script Router', () => {
|
|
|
683
680
|
);
|
|
684
681
|
|
|
685
682
|
expect(response.status).toBe(200);
|
|
686
|
-
expect(response.body.effectiveSchedule.source).toBe('
|
|
687
|
-
expect(response.body.effectiveSchedule.enabled).toBe(
|
|
683
|
+
expect(response.body.effectiveSchedule.source).toBe('none');
|
|
684
|
+
expect(response.body.effectiveSchedule.enabled).toBe(false);
|
|
688
685
|
});
|
|
689
686
|
|
|
690
687
|
it('should handle no schedule found', async () => {
|
|
@@ -712,7 +709,7 @@ describe('Admin Script Router', () => {
|
|
|
712
709
|
);
|
|
713
710
|
|
|
714
711
|
expect(response.status).toBe(404);
|
|
715
|
-
expect(response.body.
|
|
712
|
+
expect(response.body.error).toMatch(/not found/i);
|
|
716
713
|
});
|
|
717
714
|
|
|
718
715
|
it('should delete EventBridge schedule when external rule exists', async () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const serverless = require('serverless-http');
|
|
3
|
+
const Boom = require('@hapi/boom');
|
|
3
4
|
const { validateAdminApiKey } = require('./admin-auth-middleware');
|
|
4
5
|
const { getScriptFactory } = require('../application/script-factory');
|
|
5
6
|
const { createScriptRunner } = require('../application/script-runner');
|
|
@@ -26,13 +27,27 @@ const router = express.Router();
|
|
|
26
27
|
// Apply auth middleware to all admin routes
|
|
27
28
|
router.use(validateAdminApiKey);
|
|
28
29
|
|
|
29
|
-
// Register the host app's admin scripts
|
|
30
|
+
// Register the host app's admin scripts before handling requests.
|
|
30
31
|
// Memoized, so this only does work on the first request per process.
|
|
31
32
|
router.use((_req, _res, next) => {
|
|
32
33
|
bootstrapAdminScripts();
|
|
33
34
|
next();
|
|
34
35
|
});
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Translate a thrown error into an HTTP response. Boom errors (thrown by the
|
|
39
|
+
* schedule use cases) carry their own status code; anything else is an
|
|
40
|
+
* unexpected 500. Mirrors the framework's app-handler-helpers convention.
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
function sendError(res, error, fallbackMessage) {
|
|
44
|
+
if (error.isBoom) {
|
|
45
|
+
return res.status(error.output.statusCode).json({ error: error.message });
|
|
46
|
+
}
|
|
47
|
+
console.error(fallbackMessage, error);
|
|
48
|
+
return res.status(500).json({ error: fallbackMessage });
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
/**
|
|
37
52
|
* Build audit metadata for an execution from the request.
|
|
38
53
|
* @private
|
|
@@ -62,11 +77,9 @@ function createScheduleUseCases() {
|
|
|
62
77
|
process.env.SCHEDULER_PROVIDER ||
|
|
63
78
|
(process.env.AWS_LAMBDA_FUNCTION_NAME ? null : 'local');
|
|
64
79
|
if (!schedulerType) {
|
|
65
|
-
|
|
80
|
+
throw Boom.serverUnavailable(
|
|
66
81
|
'SCHEDULER_PROVIDER is not configured. Set it (e.g. "aws") via appDefinition.admin.enableScheduling.'
|
|
67
82
|
);
|
|
68
|
-
error.code = 'SCHEDULER_NOT_CONFIGURED';
|
|
69
|
-
throw error;
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
const schedulerAdapter = createSchedulerAdapter({
|
|
@@ -112,7 +125,6 @@ router.get('/scripts', async (req, res) => {
|
|
|
112
125
|
category: s.definition.display?.category || 'custom',
|
|
113
126
|
requireIntegrationInstance:
|
|
114
127
|
s.definition.config?.requireIntegrationInstance || false,
|
|
115
|
-
schedule: s.definition.schedule || null,
|
|
116
128
|
})),
|
|
117
129
|
});
|
|
118
130
|
} catch (error) {
|
|
@@ -148,7 +160,6 @@ router.get('/scripts/:scriptName', async (req, res) => {
|
|
|
148
160
|
outputSchema: definition.outputSchema,
|
|
149
161
|
config: definition.config,
|
|
150
162
|
display: definition.display,
|
|
151
|
-
schedule: definition.schedule,
|
|
152
163
|
});
|
|
153
164
|
} catch (error) {
|
|
154
165
|
console.error('Error getting script:', error);
|
|
@@ -354,19 +365,7 @@ router.get('/scripts/:scriptName/schedule', async (req, res) => {
|
|
|
354
365
|
...result.schedule,
|
|
355
366
|
});
|
|
356
367
|
} catch (error) {
|
|
357
|
-
|
|
358
|
-
return res.status(404).json({
|
|
359
|
-
error: error.message,
|
|
360
|
-
code: error.code,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
|
|
364
|
-
return res
|
|
365
|
-
.status(503)
|
|
366
|
-
.json({ error: error.message, code: error.code });
|
|
367
|
-
}
|
|
368
|
-
console.error('Error getting schedule:', error);
|
|
369
|
-
res.status(500).json({ error: 'Failed to get schedule' });
|
|
368
|
+
return sendError(res, error, 'Failed to get schedule');
|
|
370
369
|
}
|
|
371
370
|
});
|
|
372
371
|
|
|
@@ -397,25 +396,7 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
|
|
|
397
396
|
}),
|
|
398
397
|
});
|
|
399
398
|
} catch (error) {
|
|
400
|
-
|
|
401
|
-
return res.status(404).json({
|
|
402
|
-
error: error.message,
|
|
403
|
-
code: error.code,
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
if (error.code === 'INVALID_INPUT') {
|
|
407
|
-
return res.status(400).json({
|
|
408
|
-
error: error.message,
|
|
409
|
-
code: error.code,
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
|
|
413
|
-
return res
|
|
414
|
-
.status(503)
|
|
415
|
-
.json({ error: error.message, code: error.code });
|
|
416
|
-
}
|
|
417
|
-
console.error('Error updating schedule:', error);
|
|
418
|
-
res.status(500).json({ error: 'Failed to update schedule' });
|
|
399
|
+
return sendError(res, error, 'Failed to update schedule');
|
|
419
400
|
}
|
|
420
401
|
});
|
|
421
402
|
|
|
@@ -432,19 +413,7 @@ router.delete('/scripts/:scriptName/schedule', async (req, res) => {
|
|
|
432
413
|
|
|
433
414
|
res.json(result);
|
|
434
415
|
} catch (error) {
|
|
435
|
-
|
|
436
|
-
return res.status(404).json({
|
|
437
|
-
error: error.message,
|
|
438
|
-
code: error.code,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
if (error.code === 'SCHEDULER_NOT_CONFIGURED') {
|
|
442
|
-
return res
|
|
443
|
-
.status(503)
|
|
444
|
-
.json({ error: error.message, code: error.code });
|
|
445
|
-
}
|
|
446
|
-
console.error('Error deleting schedule:', error);
|
|
447
|
-
res.status(500).json({ error: 'Failed to delete schedule' });
|
|
416
|
+
return sendError(res, error, 'Failed to delete schedule');
|
|
448
417
|
}
|
|
449
418
|
});
|
|
450
419
|
|