@friggframework/admin-scripts 2.0.0--canary.517.300ded3.0 → 2.0.0--canary.522.cbd3d5a.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/index.js +2 -2
- package/package.json +8 -6
- package/src/application/__tests__/admin-frigg-commands.test.js +18 -18
- 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__/script-runner.test.js +14 -144
- package/src/application/admin-frigg-commands.js +7 -7
- package/src/application/admin-script-base.js +4 -2
- package/src/application/dry-run-http-interceptor.js +296 -0
- package/src/application/dry-run-repository-wrapper.js +261 -0
- package/src/application/script-runner.js +127 -121
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +95 -32
- package/src/infrastructure/__tests__/admin-script-router.test.js +25 -24
- package/src/infrastructure/admin-auth-middleware.js +43 -5
- package/src/infrastructure/admin-script-router.js +16 -14
- package/src/infrastructure/script-executor-handler.js +2 -2
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dry-Run HTTP Interceptor
|
|
3
|
+
*
|
|
4
|
+
* Creates a mock HTTP client that logs requests instead of executing them.
|
|
5
|
+
* Used to intercept API module calls during dry-run.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sanitize headers to remove authentication tokens
|
|
10
|
+
* @param {Object} headers - HTTP headers
|
|
11
|
+
* @returns {Object} Sanitized headers
|
|
12
|
+
*/
|
|
13
|
+
function sanitizeHeaders(headers) {
|
|
14
|
+
if (!headers || typeof headers !== 'object') {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const safe = { ...headers };
|
|
19
|
+
|
|
20
|
+
// Remove common auth headers
|
|
21
|
+
const sensitiveHeaders = [
|
|
22
|
+
'authorization',
|
|
23
|
+
'Authorization',
|
|
24
|
+
'x-api-key',
|
|
25
|
+
'X-API-Key',
|
|
26
|
+
'x-auth-token',
|
|
27
|
+
'X-Auth-Token',
|
|
28
|
+
'api-key',
|
|
29
|
+
'API-Key',
|
|
30
|
+
'apikey',
|
|
31
|
+
'ApiKey',
|
|
32
|
+
'token',
|
|
33
|
+
'Token',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const header of sensitiveHeaders) {
|
|
37
|
+
if (safe[header]) {
|
|
38
|
+
safe[header] = '[REDACTED]';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return safe;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detect service name from base URL
|
|
47
|
+
* @param {string} baseURL - Base URL of the API
|
|
48
|
+
* @returns {string} Service name
|
|
49
|
+
*/
|
|
50
|
+
function detectService(baseURL) {
|
|
51
|
+
if (!baseURL) return 'unknown';
|
|
52
|
+
|
|
53
|
+
const url = baseURL.toLowerCase();
|
|
54
|
+
|
|
55
|
+
// CRM Systems
|
|
56
|
+
if (url.includes('hubspot') || url.includes('hubapi')) return 'HubSpot';
|
|
57
|
+
if (url.includes('salesforce')) return 'Salesforce';
|
|
58
|
+
if (url.includes('pipedrive')) return 'Pipedrive';
|
|
59
|
+
if (url.includes('zoho')) return 'Zoho CRM';
|
|
60
|
+
if (url.includes('attio')) return 'Attio';
|
|
61
|
+
|
|
62
|
+
// Communication
|
|
63
|
+
if (url.includes('slack')) return 'Slack';
|
|
64
|
+
if (url.includes('discord')) return 'Discord';
|
|
65
|
+
if (url.includes('teams.microsoft')) return 'Microsoft Teams';
|
|
66
|
+
|
|
67
|
+
// Project Management
|
|
68
|
+
if (url.includes('asana')) return 'Asana';
|
|
69
|
+
if (url.includes('monday')) return 'Monday.com';
|
|
70
|
+
if (url.includes('trello')) return 'Trello';
|
|
71
|
+
if (url.includes('clickup')) return 'ClickUp';
|
|
72
|
+
|
|
73
|
+
// Storage
|
|
74
|
+
if (url.includes('googleapis.com/drive')) return 'Google Drive';
|
|
75
|
+
if (url.includes('dropbox')) return 'Dropbox';
|
|
76
|
+
if (url.includes('box.com')) return 'Box';
|
|
77
|
+
|
|
78
|
+
// Email & Marketing
|
|
79
|
+
if (url.includes('sendgrid')) return 'SendGrid';
|
|
80
|
+
if (url.includes('mailchimp')) return 'Mailchimp';
|
|
81
|
+
if (url.includes('gmail')) return 'Gmail';
|
|
82
|
+
|
|
83
|
+
// Accounting
|
|
84
|
+
if (url.includes('quickbooks')) return 'QuickBooks';
|
|
85
|
+
if (url.includes('xero')) return 'Xero';
|
|
86
|
+
|
|
87
|
+
// Other
|
|
88
|
+
if (url.includes('stripe')) return 'Stripe';
|
|
89
|
+
if (url.includes('shopify')) return 'Shopify';
|
|
90
|
+
if (url.includes('github')) return 'GitHub';
|
|
91
|
+
if (url.includes('gitlab')) return 'GitLab';
|
|
92
|
+
|
|
93
|
+
return 'unknown';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sanitize request data to remove sensitive information
|
|
98
|
+
* @param {*} data - Request data
|
|
99
|
+
* @returns {*} Sanitized data
|
|
100
|
+
*/
|
|
101
|
+
function sanitizeData(data) {
|
|
102
|
+
if (data === null || data === undefined) {
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof data !== 'object') {
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(data)) {
|
|
111
|
+
return data.map(sanitizeData);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sanitized = {};
|
|
115
|
+
for (const [key, value] of Object.entries(data)) {
|
|
116
|
+
const lowerKey = key.toLowerCase();
|
|
117
|
+
|
|
118
|
+
// Check if this is a leaf node that should be redacted
|
|
119
|
+
const isSensitiveField =
|
|
120
|
+
lowerKey === 'password' ||
|
|
121
|
+
lowerKey === 'token' ||
|
|
122
|
+
lowerKey === 'secret' ||
|
|
123
|
+
lowerKey === 'apikey' ||
|
|
124
|
+
lowerKey.endsWith('password') ||
|
|
125
|
+
lowerKey.endsWith('token') ||
|
|
126
|
+
lowerKey.endsWith('secret') ||
|
|
127
|
+
lowerKey.endsWith('key') && !lowerKey.endsWith('publickey');
|
|
128
|
+
|
|
129
|
+
// Only redact if it's a primitive value (not an object/array)
|
|
130
|
+
if (isSensitiveField && typeof value !== 'object') {
|
|
131
|
+
sanitized[key] = '[REDACTED]';
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Recursively sanitize nested objects
|
|
136
|
+
if (typeof value === 'object' && value !== null) {
|
|
137
|
+
sanitized[key] = sanitizeData(value);
|
|
138
|
+
} else {
|
|
139
|
+
sanitized[key] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return sanitized;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a dry-run HTTP client
|
|
148
|
+
*
|
|
149
|
+
* @param {Array} operationLog - Array to append logged HTTP requests
|
|
150
|
+
* @returns {Object} Mock HTTP client compatible with axios interface
|
|
151
|
+
*/
|
|
152
|
+
function createDryRunHttpClient(operationLog) {
|
|
153
|
+
/**
|
|
154
|
+
* Mock HTTP request handler
|
|
155
|
+
* @param {Object} config - Request configuration
|
|
156
|
+
* @returns {Promise<Object>} Mock response
|
|
157
|
+
*/
|
|
158
|
+
const mockRequest = async (config) => {
|
|
159
|
+
// Build full URL
|
|
160
|
+
let fullUrl = config.url;
|
|
161
|
+
if (config.baseURL && !config.url.startsWith('http')) {
|
|
162
|
+
fullUrl = `${config.baseURL}${config.url.startsWith('/') ? '' : '/'}${config.url}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Log the request that WOULD have been made
|
|
166
|
+
const logEntry = {
|
|
167
|
+
operation: 'HTTP_REQUEST',
|
|
168
|
+
method: (config.method || 'GET').toUpperCase(),
|
|
169
|
+
url: fullUrl,
|
|
170
|
+
baseURL: config.baseURL,
|
|
171
|
+
path: config.url,
|
|
172
|
+
service: detectService(config.baseURL || fullUrl),
|
|
173
|
+
headers: sanitizeHeaders(config.headers),
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Include request data for write operations
|
|
178
|
+
if (config.data && ['POST', 'PUT', 'PATCH'].includes(logEntry.method)) {
|
|
179
|
+
logEntry.data = sanitizeData(config.data);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Include query params
|
|
183
|
+
if (config.params) {
|
|
184
|
+
logEntry.params = sanitizeData(config.params);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
operationLog.push(logEntry);
|
|
188
|
+
|
|
189
|
+
// Return mock response
|
|
190
|
+
return {
|
|
191
|
+
status: 200,
|
|
192
|
+
statusText: 'OK (Dry-Run)',
|
|
193
|
+
data: {
|
|
194
|
+
_dryRun: true,
|
|
195
|
+
_message: 'This is a dry-run mock response',
|
|
196
|
+
_wouldHaveExecuted: `${logEntry.method} ${fullUrl}`,
|
|
197
|
+
_service: logEntry.service,
|
|
198
|
+
},
|
|
199
|
+
headers: {
|
|
200
|
+
'content-type': 'application/json',
|
|
201
|
+
'x-dry-run': 'true',
|
|
202
|
+
},
|
|
203
|
+
config,
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Return axios-compatible interface
|
|
208
|
+
return {
|
|
209
|
+
request: mockRequest,
|
|
210
|
+
get: (url, config = {}) => mockRequest({ ...config, method: 'GET', url }),
|
|
211
|
+
post: (url, data, config = {}) => mockRequest({ ...config, method: 'POST', url, data }),
|
|
212
|
+
put: (url, data, config = {}) => mockRequest({ ...config, method: 'PUT', url, data }),
|
|
213
|
+
patch: (url, data, config = {}) =>
|
|
214
|
+
mockRequest({ ...config, method: 'PATCH', url, data }),
|
|
215
|
+
delete: (url, config = {}) => mockRequest({ ...config, method: 'DELETE', url }),
|
|
216
|
+
head: (url, config = {}) => mockRequest({ ...config, method: 'HEAD', url }),
|
|
217
|
+
options: (url, config = {}) => mockRequest({ ...config, method: 'OPTIONS', url }),
|
|
218
|
+
|
|
219
|
+
// Axios-specific properties
|
|
220
|
+
defaults: {
|
|
221
|
+
headers: {
|
|
222
|
+
common: {},
|
|
223
|
+
get: {},
|
|
224
|
+
post: {},
|
|
225
|
+
put: {},
|
|
226
|
+
patch: {},
|
|
227
|
+
delete: {},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// Interceptors (no-op in dry-run)
|
|
232
|
+
interceptors: {
|
|
233
|
+
request: { use: () => {}, eject: () => {} },
|
|
234
|
+
response: { use: () => {}, eject: () => {} },
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Inject dry-run HTTP client into an integration instance
|
|
241
|
+
*
|
|
242
|
+
* @param {Object} integrationInstance - Integration instance from integrationFactory
|
|
243
|
+
* @param {Object} dryRunHttpClient - Dry-run HTTP client
|
|
244
|
+
*/
|
|
245
|
+
function injectDryRunHttpClient(integrationInstance, dryRunHttpClient) {
|
|
246
|
+
if (!integrationInstance) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Inject into primary API module
|
|
251
|
+
if (integrationInstance.primary?.api) {
|
|
252
|
+
injectIntoApiModule(integrationInstance.primary.api, dryRunHttpClient);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Inject into target API module
|
|
256
|
+
if (integrationInstance.target?.api) {
|
|
257
|
+
injectIntoApiModule(integrationInstance.target.api, dryRunHttpClient);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Inject dry-run HTTP client into an API module
|
|
263
|
+
* @param {Object} apiModule - API module instance
|
|
264
|
+
* @param {Object} dryRunHttpClient - Dry-run HTTP client
|
|
265
|
+
*/
|
|
266
|
+
function injectIntoApiModule(apiModule, dryRunHttpClient) {
|
|
267
|
+
// Common property names for HTTP clients in API modules
|
|
268
|
+
const httpClientProps = [
|
|
269
|
+
'_httpClient',
|
|
270
|
+
'httpClient',
|
|
271
|
+
'client',
|
|
272
|
+
'axios',
|
|
273
|
+
'request',
|
|
274
|
+
'api',
|
|
275
|
+
'http',
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
for (const prop of httpClientProps) {
|
|
279
|
+
if (apiModule[prop] && typeof apiModule[prop] === 'object') {
|
|
280
|
+
apiModule[prop] = dryRunHttpClient;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Also check if the API module itself has request methods
|
|
285
|
+
if (typeof apiModule.request === 'function') {
|
|
286
|
+
Object.assign(apiModule, dryRunHttpClient);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
createDryRunHttpClient,
|
|
292
|
+
injectDryRunHttpClient,
|
|
293
|
+
sanitizeHeaders,
|
|
294
|
+
sanitizeData,
|
|
295
|
+
detectService,
|
|
296
|
+
};
|
|
@@ -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
|
+
};
|