@friggframework/admin-scripts 2.0.0--canary.545.b4ca16d.0 → 2.0.0--canary.517.ff03f2c.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +272 -0
- package/index.js +22 -19
- package/package.json +6 -6
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +106 -45
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +24 -10
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +30 -9
- package/src/adapters/__tests__/scheduler-adapter.test.js +6 -2
- package/src/adapters/aws-scheduler-adapter.js +55 -28
- package/src/adapters/local-scheduler-adapter.js +2 -2
- package/src/adapters/scheduler-adapter-factory.js +3 -1
- package/src/adapters/scheduler-adapter.js +9 -3
- package/src/application/__tests__/admin-frigg-commands.test.js +73 -30
- package/src/application/__tests__/admin-script-base.test.js +23 -6
- package/src/application/__tests__/script-factory.test.js +30 -6
- package/src/application/__tests__/script-runner.test.js +113 -24
- package/src/application/__tests__/validate-script-input.test.js +54 -15
- package/src/application/admin-frigg-commands.js +21 -9
- package/src/application/admin-script-base.js +3 -2
- package/src/application/script-factory.js +3 -1
- package/src/application/script-runner.js +90 -48
- package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +16 -7
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +9 -4
- package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +21 -10
- package/src/application/use-cases/delete-schedule-use-case.js +6 -4
- package/src/application/use-cases/get-effective-schedule-use-case.js +3 -1
- package/src/application/use-cases/index.js +3 -1
- package/src/application/use-cases/upsert-schedule-use-case.js +30 -11
- package/src/application/validate-script-input.js +10 -3
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +9 -3
- package/src/infrastructure/__tests__/admin-script-router.test.js +125 -52
- package/src/infrastructure/admin-auth-middleware.js +3 -1
- package/src/infrastructure/admin-script-router.js +129 -14
- package/src/infrastructure/bootstrap.js +82 -0
- package/src/infrastructure/script-executor-handler.js +119 -52
- package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
- package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
- package/src/application/dry-run-http-interceptor.js +0 -296
- package/src/application/dry-run-repository-wrapper.js +0 -261
- package/src/application/schedule-management-use-case.js +0 -230
- package/src/builtins/__tests__/integration-health-check.test.js +0 -607
- package/src/builtins/__tests__/oauth-token-refresh.test.js +0 -354
- package/src/builtins/index.js +0 -28
- package/src/builtins/integration-health-check.js +0 -278
- package/src/builtins/oauth-token-refresh.js +0 -220
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const { getScriptFactory } = require('./script-factory');
|
|
2
2
|
const { createAdminScriptContext } = require('./admin-frigg-commands');
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
createAdminScriptCommands,
|
|
5
|
+
} = require('@friggframework/core/application/commands/admin-script-commands');
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Script Runner
|
|
@@ -31,10 +33,17 @@ class ScriptRunner {
|
|
|
31
33
|
* Pass this when resuming a queued execution to continue using the same execution record.
|
|
32
34
|
*/
|
|
33
35
|
async execute(scriptName, params = {}, options = {}) {
|
|
34
|
-
const {
|
|
36
|
+
const {
|
|
37
|
+
trigger,
|
|
38
|
+
audit = {},
|
|
39
|
+
executionId: existingExecutionId,
|
|
40
|
+
parentExecutionId,
|
|
41
|
+
} = options;
|
|
35
42
|
|
|
36
43
|
if (!trigger) {
|
|
37
|
-
throw new Error(
|
|
44
|
+
throw new Error(
|
|
45
|
+
'options.trigger is required (MANUAL | SCHEDULED | QUEUE)'
|
|
46
|
+
);
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
// Get script class
|
|
@@ -42,7 +51,10 @@ class ScriptRunner {
|
|
|
42
51
|
const definition = scriptClass.Definition;
|
|
43
52
|
|
|
44
53
|
// Validate integrationFactory requirement
|
|
45
|
-
if (
|
|
54
|
+
if (
|
|
55
|
+
definition.config?.requireIntegrationInstance &&
|
|
56
|
+
!this.integrationFactory
|
|
57
|
+
) {
|
|
46
58
|
throw new Error(
|
|
47
59
|
`Script "${scriptName}" requires integrationFactory but none was provided`
|
|
48
60
|
);
|
|
@@ -59,21 +71,30 @@ class ScriptRunner {
|
|
|
59
71
|
mode: options.mode || 'async',
|
|
60
72
|
input: params,
|
|
61
73
|
audit,
|
|
74
|
+
parentExecutionId,
|
|
62
75
|
});
|
|
76
|
+
// Commands return an error object (never throw) — fail loudly rather
|
|
77
|
+
// than tracking an `undefined` execution id.
|
|
78
|
+
if (execution.error) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
execution.reason || 'Failed to create admin process record'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
63
83
|
executionId = execution.id;
|
|
64
84
|
}
|
|
65
85
|
|
|
66
86
|
const startTime = new Date();
|
|
67
87
|
|
|
88
|
+
// Created up front so collected logs can be persisted on both paths.
|
|
89
|
+
const context = createAdminScriptContext({
|
|
90
|
+
executionId,
|
|
91
|
+
integrationFactory: this.integrationFactory,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let output;
|
|
68
95
|
try {
|
|
69
96
|
await this.commands.updateAdminProcessState(executionId, 'RUNNING');
|
|
70
97
|
|
|
71
|
-
// Create context for the script (facade over repositories, queue, logging)
|
|
72
|
-
const context = createAdminScriptContext({
|
|
73
|
-
executionId,
|
|
74
|
-
integrationFactory: this.integrationFactory,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
98
|
// Create script instance with context injected via constructor
|
|
78
99
|
const script = this.scriptFactory.createInstance(scriptName, {
|
|
79
100
|
context,
|
|
@@ -82,46 +103,33 @@ class ScriptRunner {
|
|
|
82
103
|
});
|
|
83
104
|
|
|
84
105
|
// Execute the script
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Calculate metrics
|
|
88
|
-
const endTime = new Date();
|
|
89
|
-
const durationMs = endTime - startTime;
|
|
90
|
-
|
|
91
|
-
await this.commands.completeAdminProcess(executionId, {
|
|
92
|
-
state: 'COMPLETED',
|
|
93
|
-
output,
|
|
94
|
-
metrics: {
|
|
95
|
-
startTime: startTime.toISOString(),
|
|
96
|
-
endTime: endTime.toISOString(),
|
|
97
|
-
durationMs,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
executionId,
|
|
103
|
-
status: 'COMPLETED',
|
|
104
|
-
scriptName,
|
|
105
|
-
output,
|
|
106
|
-
metrics: { durationMs },
|
|
107
|
-
};
|
|
106
|
+
output = await script.execute(params);
|
|
108
107
|
} catch (error) {
|
|
109
|
-
const
|
|
110
|
-
const durationMs = endTime - startTime;
|
|
108
|
+
const durationMs = new Date() - startTime;
|
|
111
109
|
|
|
112
|
-
await this.commands.completeAdminProcess(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
110
|
+
const completion = await this.commands.completeAdminProcess(
|
|
111
|
+
executionId,
|
|
112
|
+
{
|
|
113
|
+
state: 'FAILED',
|
|
114
|
+
error: {
|
|
115
|
+
name: error.name,
|
|
116
|
+
message: error.message,
|
|
117
|
+
stack: error.stack,
|
|
118
|
+
},
|
|
119
|
+
metrics: {
|
|
120
|
+
startTime: startTime.toISOString(),
|
|
121
|
+
endTime: new Date().toISOString(),
|
|
122
|
+
durationMs,
|
|
123
|
+
},
|
|
124
|
+
logs: context.getLogs(),
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
if (completion?.error) {
|
|
128
|
+
console.error(
|
|
129
|
+
`Failed to persist FAILED state for execution ${executionId}:`,
|
|
130
|
+
completion.reason
|
|
131
|
+
);
|
|
132
|
+
}
|
|
125
133
|
|
|
126
134
|
return {
|
|
127
135
|
executionId,
|
|
@@ -134,6 +142,40 @@ class ScriptRunner {
|
|
|
134
142
|
metrics: { durationMs },
|
|
135
143
|
};
|
|
136
144
|
}
|
|
145
|
+
|
|
146
|
+
// Script succeeded. Persist completion OUTSIDE the try above so a
|
|
147
|
+
// persistence failure here is never misreported as a script failure.
|
|
148
|
+
const durationMs = new Date() - startTime;
|
|
149
|
+
const result = {
|
|
150
|
+
executionId,
|
|
151
|
+
status: 'COMPLETED',
|
|
152
|
+
scriptName,
|
|
153
|
+
output,
|
|
154
|
+
metrics: { durationMs },
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const completion = await this.commands.completeAdminProcess(
|
|
158
|
+
executionId,
|
|
159
|
+
{
|
|
160
|
+
state: 'COMPLETED',
|
|
161
|
+
output,
|
|
162
|
+
metrics: {
|
|
163
|
+
startTime: startTime.toISOString(),
|
|
164
|
+
endTime: new Date().toISOString(),
|
|
165
|
+
durationMs,
|
|
166
|
+
},
|
|
167
|
+
logs: context.getLogs(),
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
if (completion?.error) {
|
|
171
|
+
console.error(
|
|
172
|
+
`Script "${scriptName}" ran successfully but persisting COMPLETED state failed for execution ${executionId}:`,
|
|
173
|
+
completion.reason
|
|
174
|
+
);
|
|
175
|
+
result.stateUpdateFailed = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result;
|
|
137
179
|
}
|
|
138
180
|
}
|
|
139
181
|
|
|
@@ -31,7 +31,8 @@ describe('DeleteScheduleUseCase', () => {
|
|
|
31
31
|
it('should delete schedule and cleanup external scheduler', async () => {
|
|
32
32
|
const deletedSchedule = {
|
|
33
33
|
scriptName: 'test-script',
|
|
34
|
-
externalScheduleId:
|
|
34
|
+
externalScheduleId:
|
|
35
|
+
'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
mockScriptFactory.has.mockReturnValue(true);
|
|
@@ -47,7 +48,9 @@ describe('DeleteScheduleUseCase', () => {
|
|
|
47
48
|
expect(result.success).toBe(true);
|
|
48
49
|
expect(result.deletedCount).toBe(1);
|
|
49
50
|
expect(result.message).toBe('Schedule override removed');
|
|
50
|
-
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
51
|
+
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
52
|
+
'test-script'
|
|
53
|
+
);
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
it('should not call scheduler when no external rule exists', async () => {
|
|
@@ -71,7 +74,8 @@ describe('DeleteScheduleUseCase', () => {
|
|
|
71
74
|
deletedCount: 1,
|
|
72
75
|
deleted: {
|
|
73
76
|
scriptName: 'test-script',
|
|
74
|
-
externalScheduleId:
|
|
77
|
+
externalScheduleId:
|
|
78
|
+
'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
75
79
|
},
|
|
76
80
|
});
|
|
77
81
|
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
|
|
@@ -105,13 +109,17 @@ describe('DeleteScheduleUseCase', () => {
|
|
|
105
109
|
expect(result.effectiveSchedule.source).toBe('definition');
|
|
106
110
|
expect(result.effectiveSchedule.enabled).toBe(true);
|
|
107
111
|
expect(result.effectiveSchedule.cronExpression).toBe('0 6 * * *');
|
|
108
|
-
expect(result.effectiveSchedule.timezone).toBe(
|
|
112
|
+
expect(result.effectiveSchedule.timezone).toBe(
|
|
113
|
+
'America/Los_Angeles'
|
|
114
|
+
);
|
|
109
115
|
});
|
|
110
116
|
|
|
111
117
|
it('should default timezone to UTC when not in definition', async () => {
|
|
112
118
|
mockScriptFactory.has.mockReturnValue(true);
|
|
113
119
|
mockScriptFactory.get.mockReturnValue({
|
|
114
|
-
Definition: {
|
|
120
|
+
Definition: {
|
|
121
|
+
schedule: { enabled: true, cronExpression: '0 6 * * *' },
|
|
122
|
+
},
|
|
115
123
|
});
|
|
116
124
|
mockCommands.deleteSchedule.mockResolvedValue({
|
|
117
125
|
deletedCount: 1,
|
|
@@ -155,8 +163,9 @@ describe('DeleteScheduleUseCase', () => {
|
|
|
155
163
|
it('should throw SCRIPT_NOT_FOUND error when script does not exist', async () => {
|
|
156
164
|
mockScriptFactory.has.mockReturnValue(false);
|
|
157
165
|
|
|
158
|
-
await expect(useCase.execute('non-existent'))
|
|
159
|
-
|
|
166
|
+
await expect(useCase.execute('non-existent')).rejects.toThrow(
|
|
167
|
+
'Script "non-existent" not found'
|
|
168
|
+
);
|
|
160
169
|
|
|
161
170
|
try {
|
|
162
171
|
await useCase.execute('non-existent');
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
GetEffectiveScheduleUseCase,
|
|
3
|
+
} = require('../get-effective-schedule-use-case');
|
|
2
4
|
|
|
3
5
|
describe('GetEffectiveScheduleUseCase', () => {
|
|
4
6
|
let useCase;
|
|
@@ -64,7 +66,9 @@ describe('GetEffectiveScheduleUseCase', () => {
|
|
|
64
66
|
it('should default timezone to UTC when not specified in definition', async () => {
|
|
65
67
|
mockScriptFactory.has.mockReturnValue(true);
|
|
66
68
|
mockScriptFactory.get.mockReturnValue({
|
|
67
|
-
Definition: {
|
|
69
|
+
Definition: {
|
|
70
|
+
schedule: { enabled: true, cronExpression: '0 12 * * *' },
|
|
71
|
+
},
|
|
68
72
|
});
|
|
69
73
|
mockCommands.getScheduleByScriptName.mockResolvedValue(null);
|
|
70
74
|
|
|
@@ -101,8 +105,9 @@ describe('GetEffectiveScheduleUseCase', () => {
|
|
|
101
105
|
it('should throw SCRIPT_NOT_FOUND error when script does not exist', async () => {
|
|
102
106
|
mockScriptFactory.has.mockReturnValue(false);
|
|
103
107
|
|
|
104
|
-
await expect(useCase.execute('non-existent'))
|
|
105
|
-
|
|
108
|
+
await expect(useCase.execute('non-existent')).rejects.toThrow(
|
|
109
|
+
'Script "non-existent" not found'
|
|
110
|
+
);
|
|
106
111
|
|
|
107
112
|
try {
|
|
108
113
|
await useCase.execute('non-existent');
|
|
@@ -46,7 +46,8 @@ describe('UpsertScheduleUseCase', () => {
|
|
|
46
46
|
});
|
|
47
47
|
mockCommands.updateScheduleExternalInfo.mockResolvedValue({
|
|
48
48
|
...savedSchedule,
|
|
49
|
-
externalScheduleId:
|
|
49
|
+
externalScheduleId:
|
|
50
|
+
'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
50
51
|
});
|
|
51
52
|
|
|
52
53
|
const result = await useCase.execute('test-script', {
|
|
@@ -96,7 +97,8 @@ describe('UpsertScheduleUseCase', () => {
|
|
|
96
97
|
enabled: false,
|
|
97
98
|
cronExpression: null,
|
|
98
99
|
timezone: 'UTC',
|
|
99
|
-
externalScheduleId:
|
|
100
|
+
externalScheduleId:
|
|
101
|
+
'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
100
102
|
};
|
|
101
103
|
|
|
102
104
|
mockScriptFactory.has.mockReturnValue(true);
|
|
@@ -112,7 +114,9 @@ describe('UpsertScheduleUseCase', () => {
|
|
|
112
114
|
});
|
|
113
115
|
|
|
114
116
|
expect(result.success).toBe(true);
|
|
115
|
-
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
117
|
+
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
118
|
+
'test-script'
|
|
119
|
+
);
|
|
116
120
|
});
|
|
117
121
|
|
|
118
122
|
it('should handle scheduler errors gracefully with warning', async () => {
|
|
@@ -142,8 +146,9 @@ describe('UpsertScheduleUseCase', () => {
|
|
|
142
146
|
it('should throw SCRIPT_NOT_FOUND error when script does not exist', async () => {
|
|
143
147
|
mockScriptFactory.has.mockReturnValue(false);
|
|
144
148
|
|
|
145
|
-
await expect(
|
|
146
|
-
.
|
|
149
|
+
await expect(
|
|
150
|
+
useCase.execute('non-existent', { enabled: true })
|
|
151
|
+
).rejects.toThrow('Script "non-existent" not found');
|
|
147
152
|
|
|
148
153
|
try {
|
|
149
154
|
await useCase.execute('non-existent', { enabled: true });
|
|
@@ -155,8 +160,9 @@ describe('UpsertScheduleUseCase', () => {
|
|
|
155
160
|
it('should throw INVALID_INPUT error when enabled is not a boolean', async () => {
|
|
156
161
|
mockScriptFactory.has.mockReturnValue(true);
|
|
157
162
|
|
|
158
|
-
await expect(
|
|
159
|
-
.
|
|
163
|
+
await expect(
|
|
164
|
+
useCase.execute('test-script', { enabled: 'yes' })
|
|
165
|
+
).rejects.toThrow('enabled must be a boolean');
|
|
160
166
|
|
|
161
167
|
try {
|
|
162
168
|
await useCase.execute('test-script', { enabled: 'yes' });
|
|
@@ -168,8 +174,11 @@ describe('UpsertScheduleUseCase', () => {
|
|
|
168
174
|
it('should throw INVALID_INPUT error when enabled without cronExpression', async () => {
|
|
169
175
|
mockScriptFactory.has.mockReturnValue(true);
|
|
170
176
|
|
|
171
|
-
await expect(
|
|
172
|
-
.
|
|
177
|
+
await expect(
|
|
178
|
+
useCase.execute('test-script', { enabled: true })
|
|
179
|
+
).rejects.toThrow(
|
|
180
|
+
'cronExpression is required when enabled is true'
|
|
181
|
+
);
|
|
173
182
|
|
|
174
183
|
try {
|
|
175
184
|
await useCase.execute('test-script', { enabled: true });
|
|
@@ -187,7 +196,9 @@ describe('UpsertScheduleUseCase', () => {
|
|
|
187
196
|
timezone: 'UTC',
|
|
188
197
|
});
|
|
189
198
|
|
|
190
|
-
const result = await useCase.execute('test-script', {
|
|
199
|
+
const result = await useCase.execute('test-script', {
|
|
200
|
+
enabled: false,
|
|
201
|
+
});
|
|
191
202
|
|
|
192
203
|
expect(result.success).toBe(true);
|
|
193
204
|
expect(mockCommands.upsertSchedule).toHaveBeenCalledWith({
|
|
@@ -31,14 +31,16 @@ class DeleteScheduleUseCase {
|
|
|
31
31
|
);
|
|
32
32
|
|
|
33
33
|
// Determine effective schedule after deletion
|
|
34
|
-
const effectiveSchedule =
|
|
34
|
+
const effectiveSchedule =
|
|
35
|
+
this._getEffectiveScheduleAfterDeletion(scriptName);
|
|
35
36
|
|
|
36
37
|
return {
|
|
37
38
|
success: true,
|
|
38
39
|
deletedCount: deleteResult.deletedCount,
|
|
39
|
-
message:
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
message:
|
|
41
|
+
deleteResult.deletedCount > 0
|
|
42
|
+
? 'Schedule override removed'
|
|
43
|
+
: 'No schedule override found',
|
|
42
44
|
effectiveSchedule,
|
|
43
45
|
...(schedulerWarning && { schedulerWarning }),
|
|
44
46
|
};
|
|
@@ -23,7 +23,9 @@ class GetEffectiveScheduleUseCase {
|
|
|
23
23
|
this._validateScriptExists(scriptName);
|
|
24
24
|
|
|
25
25
|
// Priority 1: Database override
|
|
26
|
-
const dbSchedule = await this.commands.getScheduleByScriptName(
|
|
26
|
+
const dbSchedule = await this.commands.getScheduleByScriptName(
|
|
27
|
+
scriptName
|
|
28
|
+
);
|
|
27
29
|
if (dbSchedule) {
|
|
28
30
|
return {
|
|
29
31
|
source: 'database',
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* - DeleteScheduleUseCase: Delete schedule with scheduler cleanup
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
GetEffectiveScheduleUseCase,
|
|
12
|
+
} = require('./get-effective-schedule-use-case');
|
|
11
13
|
const { UpsertScheduleUseCase } = require('./upsert-schedule-use-case');
|
|
12
14
|
const { DeleteScheduleUseCase } = require('./delete-schedule-use-case');
|
|
13
15
|
|
|
@@ -47,10 +47,16 @@ class UpsertScheduleUseCase {
|
|
|
47
47
|
success: true,
|
|
48
48
|
schedule: {
|
|
49
49
|
...schedule,
|
|
50
|
-
externalScheduleId:
|
|
51
|
-
|
|
50
|
+
externalScheduleId:
|
|
51
|
+
schedulerResult.externalScheduleId ||
|
|
52
|
+
schedule.externalScheduleId,
|
|
53
|
+
externalScheduleName:
|
|
54
|
+
schedulerResult.externalScheduleName ||
|
|
55
|
+
schedule.externalScheduleName,
|
|
52
56
|
},
|
|
53
|
-
...(schedulerResult.warning && {
|
|
57
|
+
...(schedulerResult.warning && {
|
|
58
|
+
schedulerWarning: schedulerResult.warning,
|
|
59
|
+
}),
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
|
|
@@ -76,7 +82,9 @@ class UpsertScheduleUseCase {
|
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
if (enabled && !cronExpression) {
|
|
79
|
-
const error = new Error(
|
|
85
|
+
const error = new Error(
|
|
86
|
+
'cronExpression is required when enabled is true'
|
|
87
|
+
);
|
|
80
88
|
error.code = 'INVALID_INPUT';
|
|
81
89
|
throw error;
|
|
82
90
|
}
|
|
@@ -87,17 +95,28 @@ class UpsertScheduleUseCase {
|
|
|
87
95
|
* Abstracts AWS EventBridge or other scheduler providers
|
|
88
96
|
* @private
|
|
89
97
|
*/
|
|
90
|
-
async _syncExternalScheduler(
|
|
91
|
-
|
|
98
|
+
async _syncExternalScheduler(
|
|
99
|
+
scriptName,
|
|
100
|
+
enabled,
|
|
101
|
+
cronExpression,
|
|
102
|
+
timezone,
|
|
103
|
+
existingId
|
|
104
|
+
) {
|
|
105
|
+
const result = {
|
|
106
|
+
externalScheduleId: null,
|
|
107
|
+
externalScheduleName: null,
|
|
108
|
+
warning: null,
|
|
109
|
+
};
|
|
92
110
|
|
|
93
111
|
try {
|
|
94
112
|
if (enabled && cronExpression) {
|
|
95
113
|
// Create/update external schedule
|
|
96
|
-
const schedulerInfo =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
const schedulerInfo =
|
|
115
|
+
await this.schedulerAdapter.createSchedule({
|
|
116
|
+
scriptName,
|
|
117
|
+
cronExpression,
|
|
118
|
+
timezone: timezone || 'UTC',
|
|
119
|
+
});
|
|
101
120
|
|
|
102
121
|
if (schedulerInfo?.scheduleArn) {
|
|
103
122
|
await this.commands.updateScheduleExternalInfo(scriptName, {
|
|
@@ -27,7 +27,8 @@ function validateScriptInput(scriptFactory, scriptName, params = {}) {
|
|
|
27
27
|
name: definition.name,
|
|
28
28
|
version: definition.version,
|
|
29
29
|
description: definition.description,
|
|
30
|
-
requireIntegrationInstance:
|
|
30
|
+
requireIntegrationInstance:
|
|
31
|
+
definition.config?.requireIntegrationInstance || false,
|
|
31
32
|
},
|
|
32
33
|
input: params,
|
|
33
34
|
inputSchema: definition.inputSchema || null,
|
|
@@ -91,7 +92,10 @@ function validateType(key, value, schema) {
|
|
|
91
92
|
const expectedType = schema.type;
|
|
92
93
|
if (!expectedType) return null;
|
|
93
94
|
|
|
94
|
-
if (
|
|
95
|
+
if (
|
|
96
|
+
expectedType === 'integer' &&
|
|
97
|
+
(typeof value !== 'number' || !Number.isInteger(value))
|
|
98
|
+
) {
|
|
95
99
|
return `Parameter "${key}" must be an integer`;
|
|
96
100
|
}
|
|
97
101
|
if (expectedType === 'number' && typeof value !== 'number') {
|
|
@@ -106,7 +110,10 @@ function validateType(key, value, schema) {
|
|
|
106
110
|
if (expectedType === 'array' && !Array.isArray(value)) {
|
|
107
111
|
return `Parameter "${key}" must be an array`;
|
|
108
112
|
}
|
|
109
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
expectedType === 'object' &&
|
|
115
|
+
(typeof value !== 'object' || Array.isArray(value))
|
|
116
|
+
) {
|
|
110
117
|
return `Parameter "${key}" must be an object`;
|
|
111
118
|
}
|
|
112
119
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
1
2
|
const { validateAdminApiKey } = require('../admin-auth-middleware');
|
|
2
3
|
|
|
4
|
+
// Generated at runtime so no credential-like literal is committed
|
|
5
|
+
const TEST_ADMIN_KEY = crypto.randomBytes(16).toString('hex');
|
|
6
|
+
|
|
3
7
|
describe('validateAdminApiKey', () => {
|
|
4
8
|
let mockReq;
|
|
5
9
|
let mockRes;
|
|
@@ -8,7 +12,7 @@ describe('validateAdminApiKey', () => {
|
|
|
8
12
|
|
|
9
13
|
beforeEach(() => {
|
|
10
14
|
originalEnv = process.env.ADMIN_API_KEY;
|
|
11
|
-
process.env.ADMIN_API_KEY =
|
|
15
|
+
process.env.ADMIN_API_KEY = TEST_ADMIN_KEY;
|
|
12
16
|
|
|
13
17
|
mockReq = {
|
|
14
18
|
headers: {},
|
|
@@ -61,7 +65,9 @@ describe('validateAdminApiKey', () => {
|
|
|
61
65
|
|
|
62
66
|
describe('API key validation', () => {
|
|
63
67
|
it('should reject request with invalid API key', () => {
|
|
64
|
-
mockReq.headers[
|
|
68
|
+
mockReq.headers[
|
|
69
|
+
'x-frigg-admin-api-key'
|
|
70
|
+
] = `${TEST_ADMIN_KEY}-wrong`;
|
|
65
71
|
|
|
66
72
|
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
67
73
|
|
|
@@ -74,7 +80,7 @@ describe('validateAdminApiKey', () => {
|
|
|
74
80
|
});
|
|
75
81
|
|
|
76
82
|
it('should accept request with valid API key', () => {
|
|
77
|
-
mockReq.headers['x-frigg-admin-api-key'] =
|
|
83
|
+
mockReq.headers['x-frigg-admin-api-key'] = TEST_ADMIN_KEY;
|
|
78
84
|
|
|
79
85
|
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
80
86
|
|