@friggframework/admin-scripts 2.0.0--canary.517.41839c5.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/LICENSE.md +9 -0
- package/index.js +66 -0
- package/package.json +53 -0
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +322 -0
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +325 -0
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +257 -0
- package/src/adapters/__tests__/scheduler-adapter.test.js +103 -0
- package/src/adapters/aws-scheduler-adapter.js +138 -0
- package/src/adapters/local-scheduler-adapter.js +103 -0
- package/src/adapters/scheduler-adapter-factory.js +69 -0
- package/src/adapters/scheduler-adapter.js +64 -0
- package/src/application/__tests__/admin-frigg-commands.test.js +643 -0
- package/src/application/__tests__/admin-script-base.test.js +273 -0
- package/src/application/__tests__/dry-run-http-interceptor.test.js +313 -0
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +257 -0
- package/src/application/__tests__/schedule-management-use-case.test.js +276 -0
- package/src/application/__tests__/script-factory.test.js +381 -0
- package/src/application/__tests__/script-runner.test.js +202 -0
- package/src/application/admin-frigg-commands.js +242 -0
- package/src/application/admin-script-base.js +138 -0
- package/src/application/dry-run-http-interceptor.js +296 -0
- package/src/application/dry-run-repository-wrapper.js +261 -0
- package/src/application/schedule-management-use-case.js +230 -0
- package/src/application/script-factory.js +161 -0
- package/src/application/script-runner.js +254 -0
- package/src/builtins/__tests__/integration-health-check.test.js +598 -0
- package/src/builtins/__tests__/oauth-token-refresh.test.js +344 -0
- package/src/builtins/index.js +28 -0
- package/src/builtins/integration-health-check.js +279 -0
- package/src/builtins/oauth-token-refresh.js +221 -0
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +148 -0
- package/src/infrastructure/__tests__/admin-script-router.test.js +701 -0
- package/src/infrastructure/admin-auth-middleware.js +49 -0
- package/src/infrastructure/admin-script-router.js +311 -0
- package/src/infrastructure/script-executor-handler.js +75 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dry-Run Repository Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps any repository to intercept write operations.
|
|
5
|
+
* - READ operations pass through unchanged
|
|
6
|
+
* - WRITE operations are logged but not executed
|
|
7
|
+
*
|
|
8
|
+
* Uses Proxy pattern for dynamic method interception
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a dry-run wrapper for any repository
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} repository - The real repository to wrap
|
|
15
|
+
* @param {Array} operationLog - Array to append logged operations
|
|
16
|
+
* @param {string} modelName - Name of the model (for logging)
|
|
17
|
+
* @returns {Proxy} Wrapped repository that logs write operations
|
|
18
|
+
*/
|
|
19
|
+
function createDryRunWrapper(repository, operationLog, modelName) {
|
|
20
|
+
return new Proxy(repository, {
|
|
21
|
+
get(target, prop) {
|
|
22
|
+
const value = target[prop];
|
|
23
|
+
|
|
24
|
+
// Return non-function properties as-is
|
|
25
|
+
if (typeof value !== 'function') {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Identify write operations by name pattern
|
|
30
|
+
const writePatterns = /^(create|update|delete|upsert|append|remove|insert|save)/i;
|
|
31
|
+
const isWrite = writePatterns.test(prop);
|
|
32
|
+
|
|
33
|
+
// Pass through read operations
|
|
34
|
+
if (!isWrite) {
|
|
35
|
+
return value.bind(target);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Wrap write operation
|
|
39
|
+
return async (...args) => {
|
|
40
|
+
// Log the operation that WOULD have been performed
|
|
41
|
+
operationLog.push({
|
|
42
|
+
operation: prop.toUpperCase(),
|
|
43
|
+
model: modelName,
|
|
44
|
+
method: prop,
|
|
45
|
+
args: sanitizeArgs(args),
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
wouldExecute: `${modelName}.${prop}()`,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// For write operations, try to return existing data or mock data
|
|
51
|
+
// This helps scripts continue executing without errors
|
|
52
|
+
|
|
53
|
+
// For updates, try to return existing data
|
|
54
|
+
if (prop.includes('update') || prop.includes('upsert')) {
|
|
55
|
+
// Try to extract ID from first argument
|
|
56
|
+
const possibleId = args[0];
|
|
57
|
+
let existing = null;
|
|
58
|
+
|
|
59
|
+
if (possibleId && typeof possibleId === 'string') {
|
|
60
|
+
// Try to find existing record
|
|
61
|
+
const findMethod = getFindMethod(target, prop);
|
|
62
|
+
if (findMethod) {
|
|
63
|
+
try {
|
|
64
|
+
existing = await findMethod.call(target, possibleId);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
// Ignore errors, continue to mock
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Return merged data
|
|
72
|
+
if (existing) {
|
|
73
|
+
// Merge update data with existing
|
|
74
|
+
return { ...existing, ...args[1], _dryRun: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// No existing data, return mock
|
|
78
|
+
if (args[1]) {
|
|
79
|
+
return { id: possibleId, ...args[1], _dryRun: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { id: possibleId, _dryRun: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For creates, return mock object with the data
|
|
86
|
+
if (prop.includes('create') || prop.includes('insert')) {
|
|
87
|
+
const data = args[0] || {};
|
|
88
|
+
return {
|
|
89
|
+
id: `dry-run-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
90
|
+
...data,
|
|
91
|
+
_dryRun: true,
|
|
92
|
+
createdAt: new Date().toISOString(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// For deletes, return success indication
|
|
97
|
+
if (prop.includes('delete') || prop.includes('remove')) {
|
|
98
|
+
return { deletedCount: 1, _dryRun: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Default: return mock success
|
|
102
|
+
return { success: true, _dryRun: true };
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Try to find a corresponding find method for an update operation
|
|
110
|
+
* @param {Object} target - Repository target
|
|
111
|
+
* @param {string} updateMethod - Update method name
|
|
112
|
+
* @returns {Function|null} Find method or null
|
|
113
|
+
*/
|
|
114
|
+
function getFindMethod(target, updateMethod) {
|
|
115
|
+
// Common patterns: updateIntegration -> findIntegrationById
|
|
116
|
+
const patterns = [
|
|
117
|
+
() => {
|
|
118
|
+
const match = updateMethod.match(/update(\w+)/i);
|
|
119
|
+
return match ? `find${match[1]}ById` : null;
|
|
120
|
+
},
|
|
121
|
+
() => {
|
|
122
|
+
const match = updateMethod.match(/update(\w+)/i);
|
|
123
|
+
return match ? `get${match[1]}ById` : null;
|
|
124
|
+
},
|
|
125
|
+
() => 'findById',
|
|
126
|
+
() => 'getById',
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
for (const pattern of patterns) {
|
|
130
|
+
const methodName = pattern();
|
|
131
|
+
if (methodName && typeof target[methodName] === 'function') {
|
|
132
|
+
return target[methodName];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Sanitize arguments for logging (remove sensitive data)
|
|
141
|
+
* @param {Array} args - Function arguments
|
|
142
|
+
* @returns {Array} Sanitized arguments
|
|
143
|
+
*/
|
|
144
|
+
function sanitizeArgs(args) {
|
|
145
|
+
return args.map((arg) => {
|
|
146
|
+
if (arg === null || arg === undefined) {
|
|
147
|
+
return arg;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof arg !== 'object') {
|
|
151
|
+
return arg;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (Array.isArray(arg)) {
|
|
155
|
+
return arg.map((item) => sanitizeArgs([item])[0]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Sanitize object - remove sensitive fields
|
|
159
|
+
const sanitized = {};
|
|
160
|
+
for (const [key, value] of Object.entries(arg)) {
|
|
161
|
+
const lowerKey = key.toLowerCase();
|
|
162
|
+
|
|
163
|
+
// Skip sensitive fields
|
|
164
|
+
if (
|
|
165
|
+
lowerKey.includes('password') ||
|
|
166
|
+
lowerKey.includes('token') ||
|
|
167
|
+
lowerKey.includes('secret') ||
|
|
168
|
+
lowerKey.includes('key') ||
|
|
169
|
+
lowerKey.includes('auth')
|
|
170
|
+
) {
|
|
171
|
+
sanitized[key] = '[REDACTED]';
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Recursively sanitize nested objects
|
|
176
|
+
if (typeof value === 'object' && value !== null) {
|
|
177
|
+
sanitized[key] = sanitizeArgs([value])[0];
|
|
178
|
+
} else {
|
|
179
|
+
sanitized[key] = value;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return sanitized;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Wrap AdminFriggCommands for dry-run mode
|
|
189
|
+
*
|
|
190
|
+
* @param {Object} realCommands - Real AdminFriggCommands instance
|
|
191
|
+
* @param {Array} operationLog - Array to append logged operations
|
|
192
|
+
* @returns {Object} Wrapped commands with dry-run repository wrappers
|
|
193
|
+
*/
|
|
194
|
+
function wrapAdminFriggCommandsForDryRun(realCommands, operationLog) {
|
|
195
|
+
return new Proxy(realCommands, {
|
|
196
|
+
get(target, prop) {
|
|
197
|
+
const value = target[prop];
|
|
198
|
+
|
|
199
|
+
// Pass through non-functions
|
|
200
|
+
if (typeof value !== 'function') {
|
|
201
|
+
// For lazy-loaded repositories, wrap them
|
|
202
|
+
if (prop.endsWith('Repository') && value && typeof value === 'object') {
|
|
203
|
+
const modelName = prop.replace('Repository', '');
|
|
204
|
+
return createDryRunWrapper(
|
|
205
|
+
value,
|
|
206
|
+
operationLog,
|
|
207
|
+
modelName.charAt(0).toUpperCase() + modelName.slice(1)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Identify write operations on the commands themselves
|
|
214
|
+
const writePatterns = /^(update|create|delete|append)/i;
|
|
215
|
+
const isWrite = writePatterns.test(prop);
|
|
216
|
+
|
|
217
|
+
if (!isWrite) {
|
|
218
|
+
// Read operations pass through
|
|
219
|
+
return value.bind(target);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Wrap write operations
|
|
223
|
+
return async (...args) => {
|
|
224
|
+
operationLog.push({
|
|
225
|
+
operation: prop.toUpperCase(),
|
|
226
|
+
source: 'AdminFriggCommands',
|
|
227
|
+
method: prop,
|
|
228
|
+
args: sanitizeArgs(args),
|
|
229
|
+
timestamp: new Date().toISOString(),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// For specific known methods, try to return sensible mocks
|
|
233
|
+
if (prop === 'updateIntegrationConfig') {
|
|
234
|
+
const [integrationId] = args;
|
|
235
|
+
const existing = await target.findIntegrationById(integrationId);
|
|
236
|
+
return existing;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (prop === 'updateIntegrationStatus') {
|
|
240
|
+
const [integrationId] = args;
|
|
241
|
+
const existing = await target.findIntegrationById(integrationId);
|
|
242
|
+
return existing;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (prop === 'updateCredential') {
|
|
246
|
+
const [credentialId, updates] = args;
|
|
247
|
+
return { id: credentialId, ...updates, _dryRun: true };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Default mock
|
|
251
|
+
return { success: true, _dryRun: true };
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
createDryRunWrapper,
|
|
259
|
+
wrapAdminFriggCommandsForDryRun,
|
|
260
|
+
sanitizeArgs,
|
|
261
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedule Management Use Case
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Orchestrates schedule management operations:
|
|
7
|
+
* - Get effective schedule (DB override > Definition > none)
|
|
8
|
+
* - Upsert schedule with EventBridge provisioning
|
|
9
|
+
* - Delete schedule with EventBridge cleanup
|
|
10
|
+
*
|
|
11
|
+
* This use case encapsulates the business logic that was previously
|
|
12
|
+
* embedded in the router, reducing cognitive complexity and improving testability.
|
|
13
|
+
*/
|
|
14
|
+
class ScheduleManagementUseCase {
|
|
15
|
+
constructor({ commands, schedulerAdapter, scriptFactory }) {
|
|
16
|
+
this.commands = commands;
|
|
17
|
+
this.schedulerAdapter = schedulerAdapter;
|
|
18
|
+
this.scriptFactory = scriptFactory;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate that a script exists
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
_validateScriptExists(scriptName) {
|
|
26
|
+
if (!this.scriptFactory.has(scriptName)) {
|
|
27
|
+
const error = new Error(`Script "${scriptName}" not found`);
|
|
28
|
+
error.code = 'SCRIPT_NOT_FOUND';
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the definition schedule from a script class
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
_getDefinitionSchedule(scriptName) {
|
|
38
|
+
const scriptClass = this.scriptFactory.get(scriptName);
|
|
39
|
+
return scriptClass.Definition?.schedule || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get effective schedule (DB override > Definition default > none)
|
|
44
|
+
*/
|
|
45
|
+
async getEffectiveSchedule(scriptName) {
|
|
46
|
+
this._validateScriptExists(scriptName);
|
|
47
|
+
|
|
48
|
+
// Check database override first
|
|
49
|
+
const dbSchedule = await this.commands.getScheduleByScriptName(scriptName);
|
|
50
|
+
if (dbSchedule) {
|
|
51
|
+
return {
|
|
52
|
+
source: 'database',
|
|
53
|
+
schedule: dbSchedule,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check definition default
|
|
58
|
+
const definitionSchedule = this._getDefinitionSchedule(scriptName);
|
|
59
|
+
if (definitionSchedule?.enabled) {
|
|
60
|
+
return {
|
|
61
|
+
source: 'definition',
|
|
62
|
+
schedule: {
|
|
63
|
+
scriptName,
|
|
64
|
+
enabled: definitionSchedule.enabled,
|
|
65
|
+
cronExpression: definitionSchedule.cronExpression,
|
|
66
|
+
timezone: definitionSchedule.timezone || 'UTC',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// No schedule configured
|
|
72
|
+
return {
|
|
73
|
+
source: 'none',
|
|
74
|
+
schedule: {
|
|
75
|
+
scriptName,
|
|
76
|
+
enabled: false,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create or update schedule with EventBridge provisioning
|
|
83
|
+
*/
|
|
84
|
+
async upsertSchedule(scriptName, { enabled, cronExpression, timezone }) {
|
|
85
|
+
this._validateScriptExists(scriptName);
|
|
86
|
+
this._validateScheduleInput(enabled, cronExpression);
|
|
87
|
+
|
|
88
|
+
// Save to database
|
|
89
|
+
const schedule = await this.commands.upsertSchedule({
|
|
90
|
+
scriptName,
|
|
91
|
+
enabled,
|
|
92
|
+
cronExpression: cronExpression || null,
|
|
93
|
+
timezone: timezone || 'UTC',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Provision/deprovision EventBridge
|
|
97
|
+
const schedulerResult = await this._syncEventBridgeSchedule(
|
|
98
|
+
scriptName,
|
|
99
|
+
enabled,
|
|
100
|
+
cronExpression,
|
|
101
|
+
timezone,
|
|
102
|
+
schedule.awsScheduleArn
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
schedule: {
|
|
108
|
+
...schedule,
|
|
109
|
+
awsScheduleArn: schedulerResult.awsScheduleArn || schedule.awsScheduleArn,
|
|
110
|
+
awsScheduleName: schedulerResult.awsScheduleName || schedule.awsScheduleName,
|
|
111
|
+
},
|
|
112
|
+
...(schedulerResult.warning && { schedulerWarning: schedulerResult.warning }),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validate schedule input
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
_validateScheduleInput(enabled, cronExpression) {
|
|
121
|
+
if (typeof enabled !== 'boolean') {
|
|
122
|
+
const error = new Error('enabled must be a boolean');
|
|
123
|
+
error.code = 'INVALID_INPUT';
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (enabled && !cronExpression) {
|
|
128
|
+
const error = new Error('cronExpression is required when enabled is true');
|
|
129
|
+
error.code = 'INVALID_INPUT';
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Sync EventBridge schedule based on enabled state
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
async _syncEventBridgeSchedule(scriptName, enabled, cronExpression, timezone, existingArn) {
|
|
139
|
+
const result = { awsScheduleArn: null, awsScheduleName: null, warning: null };
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
if (enabled && cronExpression) {
|
|
143
|
+
// Create/update EventBridge schedule
|
|
144
|
+
const awsInfo = await this.schedulerAdapter.createSchedule({
|
|
145
|
+
scriptName,
|
|
146
|
+
cronExpression,
|
|
147
|
+
timezone: timezone || 'UTC',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (awsInfo?.scheduleArn) {
|
|
151
|
+
await this.commands.updateScheduleAwsInfo(scriptName, {
|
|
152
|
+
awsScheduleArn: awsInfo.scheduleArn,
|
|
153
|
+
awsScheduleName: awsInfo.scheduleName,
|
|
154
|
+
});
|
|
155
|
+
result.awsScheduleArn = awsInfo.scheduleArn;
|
|
156
|
+
result.awsScheduleName = awsInfo.scheduleName;
|
|
157
|
+
}
|
|
158
|
+
} else if (!enabled && existingArn) {
|
|
159
|
+
// Delete EventBridge schedule
|
|
160
|
+
await this.schedulerAdapter.deleteSchedule(scriptName);
|
|
161
|
+
await this.commands.updateScheduleAwsInfo(scriptName, {
|
|
162
|
+
awsScheduleArn: null,
|
|
163
|
+
awsScheduleName: null,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Non-fatal: DB schedule is saved, AWS can be retried
|
|
168
|
+
result.warning = error.message;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Delete schedule override and cleanup EventBridge
|
|
176
|
+
*/
|
|
177
|
+
async deleteSchedule(scriptName) {
|
|
178
|
+
this._validateScriptExists(scriptName);
|
|
179
|
+
|
|
180
|
+
// Delete from database
|
|
181
|
+
const deleteResult = await this.commands.deleteSchedule(scriptName);
|
|
182
|
+
|
|
183
|
+
// Cleanup EventBridge if needed
|
|
184
|
+
const schedulerWarning = await this._cleanupEventBridgeSchedule(
|
|
185
|
+
scriptName,
|
|
186
|
+
deleteResult.deleted?.awsScheduleArn
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Get effective schedule after deletion
|
|
190
|
+
const definitionSchedule = this._getDefinitionSchedule(scriptName);
|
|
191
|
+
const effectiveSchedule = definitionSchedule?.enabled
|
|
192
|
+
? {
|
|
193
|
+
source: 'definition',
|
|
194
|
+
enabled: definitionSchedule.enabled,
|
|
195
|
+
cronExpression: definitionSchedule.cronExpression,
|
|
196
|
+
timezone: definitionSchedule.timezone || 'UTC',
|
|
197
|
+
}
|
|
198
|
+
: { source: 'none', enabled: false };
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
deletedCount: deleteResult.deletedCount,
|
|
203
|
+
message: deleteResult.deletedCount > 0
|
|
204
|
+
? 'Schedule override removed'
|
|
205
|
+
: 'No schedule override found',
|
|
206
|
+
effectiveSchedule,
|
|
207
|
+
...(schedulerWarning && { schedulerWarning }),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Cleanup EventBridge schedule if it exists
|
|
213
|
+
* @private
|
|
214
|
+
*/
|
|
215
|
+
async _cleanupEventBridgeSchedule(scriptName, awsScheduleArn) {
|
|
216
|
+
if (!awsScheduleArn) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await this.schedulerAdapter.deleteSchedule(scriptName);
|
|
222
|
+
return null;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
// Non-fatal: DB is cleaned up, AWS can be retried
|
|
225
|
+
return error.message;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = { ScheduleManagementUseCase };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script Factory
|
|
3
|
+
*
|
|
4
|
+
* Registry and factory for admin scripts.
|
|
5
|
+
* Manages script registration, validation, and instantiation.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```javascript
|
|
9
|
+
* const factory = new ScriptFactory();
|
|
10
|
+
* factory.register(MyScript);
|
|
11
|
+
* const script = factory.createInstance('my-script', { executionId: '123' });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
class ScriptFactory {
|
|
15
|
+
constructor(scripts = []) {
|
|
16
|
+
this.registry = new Map();
|
|
17
|
+
|
|
18
|
+
// Register initial scripts
|
|
19
|
+
scripts.forEach((ScriptClass) => this.register(ScriptClass));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a script class
|
|
24
|
+
* @param {Function} ScriptClass - Script class extending AdminScriptBase
|
|
25
|
+
* @throws {Error} If script invalid or name collision
|
|
26
|
+
*/
|
|
27
|
+
register(ScriptClass) {
|
|
28
|
+
if (!ScriptClass || !ScriptClass.Definition) {
|
|
29
|
+
throw new Error('Script class must have a static Definition property');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const definition = ScriptClass.Definition;
|
|
33
|
+
const name = definition.name;
|
|
34
|
+
|
|
35
|
+
if (!name) {
|
|
36
|
+
throw new Error('Script Definition must have a name');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (this.registry.has(name)) {
|
|
40
|
+
throw new Error(`Script "${name}" is already registered`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.registry.set(name, ScriptClass);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Register multiple scripts at once
|
|
48
|
+
* @param {Array} scriptClasses - Array of script classes
|
|
49
|
+
*/
|
|
50
|
+
registerAll(scriptClasses) {
|
|
51
|
+
scriptClasses.forEach((ScriptClass) => this.register(ScriptClass));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if script is registered
|
|
56
|
+
* @param {string} name - Script name
|
|
57
|
+
* @returns {boolean} True if registered
|
|
58
|
+
*/
|
|
59
|
+
has(name) {
|
|
60
|
+
return this.registry.has(name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get script class by name
|
|
65
|
+
* @param {string} name - Script name
|
|
66
|
+
* @returns {Function} Script class
|
|
67
|
+
* @throws {Error} If script not found
|
|
68
|
+
*/
|
|
69
|
+
get(name) {
|
|
70
|
+
const ScriptClass = this.registry.get(name);
|
|
71
|
+
if (!ScriptClass) {
|
|
72
|
+
throw new Error(`Script "${name}" not found`);
|
|
73
|
+
}
|
|
74
|
+
return ScriptClass;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get array of all registered script names
|
|
79
|
+
* @returns {Array<string>} Array of script names
|
|
80
|
+
*/
|
|
81
|
+
getNames() {
|
|
82
|
+
return Array.from(this.registry.keys());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all registered scripts
|
|
87
|
+
* @returns {Array} Array of { name, definition, class }
|
|
88
|
+
*/
|
|
89
|
+
getAll() {
|
|
90
|
+
const scripts = [];
|
|
91
|
+
for (const [name, ScriptClass] of this.registry.entries()) {
|
|
92
|
+
scripts.push({
|
|
93
|
+
name,
|
|
94
|
+
definition: ScriptClass.Definition,
|
|
95
|
+
class: ScriptClass,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return scripts;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create script instance
|
|
103
|
+
* @param {string} name - Script name
|
|
104
|
+
* @param {Object} params - Constructor parameters
|
|
105
|
+
* @returns {Object} Script instance
|
|
106
|
+
* @throws {Error} If script not found
|
|
107
|
+
*/
|
|
108
|
+
createInstance(name, params = {}) {
|
|
109
|
+
const ScriptClass = this.get(name);
|
|
110
|
+
return new ScriptClass(params);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Remove script from registry
|
|
115
|
+
* @param {string} name - Script name
|
|
116
|
+
* @returns {boolean} True if removed
|
|
117
|
+
*/
|
|
118
|
+
unregister(name) {
|
|
119
|
+
return this.registry.delete(name);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Clear all registered scripts
|
|
124
|
+
*/
|
|
125
|
+
clear() {
|
|
126
|
+
this.registry.clear();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get count of registered scripts
|
|
131
|
+
* @returns {number} Count
|
|
132
|
+
*/
|
|
133
|
+
get size() {
|
|
134
|
+
return this.registry.size;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Singleton instance for global use
|
|
139
|
+
let globalFactory = null;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get global script factory instance
|
|
143
|
+
* @returns {ScriptFactory} Global factory
|
|
144
|
+
*/
|
|
145
|
+
function getScriptFactory() {
|
|
146
|
+
if (!globalFactory) {
|
|
147
|
+
globalFactory = new ScriptFactory();
|
|
148
|
+
}
|
|
149
|
+
return globalFactory;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create a new script factory instance
|
|
154
|
+
* @param {Array} scripts - Initial scripts to register
|
|
155
|
+
* @returns {ScriptFactory} New factory
|
|
156
|
+
*/
|
|
157
|
+
function createScriptFactory(scripts = []) {
|
|
158
|
+
return new ScriptFactory(scripts);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { ScriptFactory, getScriptFactory, createScriptFactory };
|