@friggframework/admin-scripts 2.0.0--canary.517.c197eb5.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 +6 -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 +4 -29
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +4 -37
- package/src/application/use-cases/delete-schedule-use-case.js +5 -35
- package/src/application/use-cases/get-effective-schedule-use-case.js +9 -28
- package/src/infrastructure/__tests__/admin-script-router.test.js +10 -11
- package/src/infrastructure/admin-script-router.js +0 -2
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,19 +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
9
|
"@hapi/boom": "^10.0.1",
|
|
10
10
|
"express": "^4.18.2",
|
|
11
11
|
"serverless-http": "^3.2.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@friggframework/eslint-config": "2.0.0--canary.517.
|
|
15
|
-
"@friggframework/prettier-config": "2.0.0--canary.517.
|
|
16
|
-
"@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",
|
|
17
17
|
"eslint": "^8.22.0",
|
|
18
18
|
"jest": "^29.7.0",
|
|
19
19
|
"prettier": "^2.7.1",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"maintenance",
|
|
45
45
|
"operations"
|
|
46
46
|
],
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "542a9ec92adca85b29dd727125bbeb5d7e688dbc"
|
|
48
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({
|
|
@@ -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
|
|
|
@@ -6,7 +6,8 @@ const Boom = require('@hapi/boom');
|
|
|
6
6
|
* Application Layer - Hexagonal Architecture
|
|
7
7
|
*
|
|
8
8
|
* Deletes a schedule override and cleans up external scheduler resources.
|
|
9
|
-
*
|
|
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.
|
|
10
11
|
*/
|
|
11
12
|
class DeleteScheduleUseCase {
|
|
12
13
|
constructor({ commands, schedulerAdapter, scriptFactory }) {
|
|
@@ -32,9 +33,9 @@ class DeleteScheduleUseCase {
|
|
|
32
33
|
deleteResult.deleted?.externalScheduleId
|
|
33
34
|
);
|
|
34
35
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
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 };
|
|
38
39
|
|
|
39
40
|
return {
|
|
40
41
|
success: true,
|
|
@@ -57,37 +58,6 @@ class DeleteScheduleUseCase {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
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
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
source: 'none',
|
|
87
|
-
enabled: false,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
61
|
/**
|
|
92
62
|
* Cleanup external scheduler resources
|
|
93
63
|
* @private
|
|
@@ -5,10 +5,12 @@ const Boom = require('@hapi/boom');
|
|
|
5
5
|
*
|
|
6
6
|
* Application Layer - Hexagonal Architecture
|
|
7
7
|
*
|
|
8
|
-
* Resolves the effective schedule for a script
|
|
9
|
-
* 1. Database override (runtime
|
|
10
|
-
* 2.
|
|
11
|
-
*
|
|
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.
|
|
12
14
|
*/
|
|
13
15
|
class GetEffectiveScheduleUseCase {
|
|
14
16
|
constructor({ commands, scriptFactory }) {
|
|
@@ -19,7 +21,7 @@ class GetEffectiveScheduleUseCase {
|
|
|
19
21
|
/**
|
|
20
22
|
* Get effective schedule for a script
|
|
21
23
|
* @param {string} scriptName - Name of the script
|
|
22
|
-
* @returns {Promise<{source: 'database'|'
|
|
24
|
+
* @returns {Promise<{source: 'database'|'none', schedule: Object}>}
|
|
23
25
|
*/
|
|
24
26
|
async execute(scriptName) {
|
|
25
27
|
this._validateScriptExists(scriptName);
|
|
@@ -35,21 +37,8 @@ class GetEffectiveScheduleUseCase {
|
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
if (definitionSchedule?.enabled) {
|
|
41
|
-
return {
|
|
42
|
-
source: 'definition',
|
|
43
|
-
schedule: {
|
|
44
|
-
scriptName,
|
|
45
|
-
enabled: definitionSchedule.enabled,
|
|
46
|
-
cronExpression: definitionSchedule.cronExpression,
|
|
47
|
-
timezone: definitionSchedule.timezone || 'UTC',
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Priority 3: No schedule
|
|
40
|
+
// No database override: the script is declared but not actively
|
|
41
|
+
// scheduled. Admins activate scheduling explicitly via PUT /schedule.
|
|
53
42
|
return {
|
|
54
43
|
source: 'none',
|
|
55
44
|
schedule: {
|
|
@@ -67,14 +56,6 @@ class GetEffectiveScheduleUseCase {
|
|
|
67
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 };
|
|
@@ -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 () => {
|
|
@@ -656,13 +654,14 @@ describe('Admin Script Router', () => {
|
|
|
656
654
|
);
|
|
657
655
|
});
|
|
658
656
|
|
|
659
|
-
it('
|
|
657
|
+
it('reports no active schedule after deleting the override', async () => {
|
|
660
658
|
mockCommands.deleteSchedule = jest.fn().mockResolvedValue({
|
|
661
659
|
acknowledged: true,
|
|
662
660
|
deletedCount: 1,
|
|
663
661
|
});
|
|
664
662
|
|
|
665
|
-
//
|
|
663
|
+
// Even if the script carries a schedule field, it is ignored —
|
|
664
|
+
// after removing the DB override there is no active schedule.
|
|
666
665
|
class ScheduledTestScript extends TestScript {
|
|
667
666
|
static Definition = {
|
|
668
667
|
...TestScript.Definition,
|
|
@@ -681,8 +680,8 @@ describe('Admin Script Router', () => {
|
|
|
681
680
|
);
|
|
682
681
|
|
|
683
682
|
expect(response.status).toBe(200);
|
|
684
|
-
expect(response.body.effectiveSchedule.source).toBe('
|
|
685
|
-
expect(response.body.effectiveSchedule.enabled).toBe(
|
|
683
|
+
expect(response.body.effectiveSchedule.source).toBe('none');
|
|
684
|
+
expect(response.body.effectiveSchedule.enabled).toBe(false);
|
|
686
685
|
});
|
|
687
686
|
|
|
688
687
|
it('should handle no schedule found', async () => {
|
|
@@ -125,7 +125,6 @@ router.get('/scripts', async (req, res) => {
|
|
|
125
125
|
category: s.definition.display?.category || 'custom',
|
|
126
126
|
requireIntegrationInstance:
|
|
127
127
|
s.definition.config?.requireIntegrationInstance || false,
|
|
128
|
-
schedule: s.definition.schedule || null,
|
|
129
128
|
})),
|
|
130
129
|
});
|
|
131
130
|
} catch (error) {
|
|
@@ -161,7 +160,6 @@ router.get('/scripts/:scriptName', async (req, res) => {
|
|
|
161
160
|
outputSchema: definition.outputSchema,
|
|
162
161
|
config: definition.config,
|
|
163
162
|
display: definition.display,
|
|
164
|
-
schedule: definition.schedule,
|
|
165
163
|
});
|
|
166
164
|
} catch (error) {
|
|
167
165
|
console.error('Error getting script:', error);
|