@friggframework/admin-scripts 2.0.0--canary.522.cbd3d5a.0 → 2.0.0--canary.517.21b69ac.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.
Files changed (31) hide show
  1. package/PR_517_REVIEW_TRACKER.md +56 -0
  2. package/index.js +12 -3
  3. package/package.json +6 -9
  4. package/src/application/__tests__/admin-frigg-commands.test.js +19 -19
  5. package/src/application/__tests__/admin-script-base.test.js +100 -84
  6. package/src/application/__tests__/script-runner.test.js +146 -16
  7. package/src/application/admin-frigg-commands.js +20 -32
  8. package/src/application/admin-script-base.js +20 -99
  9. package/src/application/script-runner.js +131 -135
  10. package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +168 -0
  11. package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +114 -0
  12. package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +201 -0
  13. package/src/application/use-cases/delete-schedule-use-case.js +108 -0
  14. package/src/application/use-cases/get-effective-schedule-use-case.js +78 -0
  15. package/src/application/use-cases/index.js +18 -0
  16. package/src/application/use-cases/upsert-schedule-use-case.js +127 -0
  17. package/src/builtins/__tests__/integration-health-check.test.js +67 -60
  18. package/src/builtins/__tests__/oauth-token-refresh.test.js +45 -37
  19. package/src/builtins/integration-health-check.js +23 -24
  20. package/src/builtins/oauth-token-refresh.js +19 -20
  21. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
  22. package/src/infrastructure/__tests__/admin-script-router.test.js +46 -47
  23. package/src/infrastructure/admin-auth-middleware.js +5 -43
  24. package/src/infrastructure/admin-script-router.js +38 -32
  25. package/src/infrastructure/script-executor-handler.js +2 -2
  26. package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
  27. package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
  28. package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
  29. package/src/application/dry-run-http-interceptor.js +0 -296
  30. package/src/application/dry-run-repository-wrapper.js +0 -261
  31. package/src/application/schedule-management-use-case.js +0 -230
@@ -1,261 +0,0 @@
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
- };
@@ -1,230 +0,0 @@
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 };