@friggframework/admin-scripts 2.0.0--canary.517.f04156f.0 → 2.0.0--canary.517.300ded3.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.
@@ -1,257 +0,0 @@
1
- const { createDryRunWrapper, wrapAdminFriggCommandsForDryRun, sanitizeArgs } = require('../dry-run-repository-wrapper');
2
-
3
- describe('Dry-Run Repository Wrapper', () => {
4
- describe('createDryRunWrapper', () => {
5
- let mockRepository;
6
- let operationLog;
7
-
8
- beforeEach(() => {
9
- operationLog = [];
10
- mockRepository = {
11
- // Read operations
12
- findById: jest.fn(async (id) => ({ id, name: 'Test Entity' })),
13
- findAll: jest.fn(async () => [{ id: '1' }, { id: '2' }]),
14
- getStatus: jest.fn(() => 'active'),
15
-
16
- // Write operations
17
- create: jest.fn(async (data) => ({ id: 'new-id', ...data })),
18
- update: jest.fn(async (id, data) => ({ id, ...data })),
19
- delete: jest.fn(async (id) => ({ deletedCount: 1 })),
20
- updateStatus: jest.fn(async (id, status) => ({ id, status })),
21
- };
22
- });
23
-
24
- test('should pass through read operations unchanged', async () => {
25
- const wrapped = createDryRunWrapper(mockRepository, operationLog, 'TestModel');
26
-
27
- // Call read operations
28
- const byId = await wrapped.findById('123');
29
- const all = await wrapped.findAll();
30
- const status = wrapped.getStatus();
31
-
32
- // Verify original methods were called
33
- expect(mockRepository.findById).toHaveBeenCalledWith('123');
34
- expect(mockRepository.findAll).toHaveBeenCalled();
35
- expect(mockRepository.getStatus).toHaveBeenCalled();
36
-
37
- // Verify results match
38
- expect(byId).toEqual({ id: '123', name: 'Test Entity' });
39
- expect(all).toHaveLength(2);
40
- expect(status).toBe('active');
41
-
42
- // No operations should be logged
43
- expect(operationLog).toHaveLength(0);
44
- });
45
-
46
- test('should intercept and log write operations', async () => {
47
- const wrapped = createDryRunWrapper(mockRepository, operationLog, 'TestModel');
48
-
49
- // Call write operations
50
- await wrapped.create({ name: 'New Entity' });
51
- await wrapped.update('123', { name: 'Updated' });
52
- await wrapped.delete('456');
53
-
54
- // Original write methods should NOT be called
55
- expect(mockRepository.create).not.toHaveBeenCalled();
56
- expect(mockRepository.update).not.toHaveBeenCalled();
57
- expect(mockRepository.delete).not.toHaveBeenCalled();
58
-
59
- // All operations should be logged
60
- expect(operationLog).toHaveLength(3);
61
-
62
- expect(operationLog[0]).toMatchObject({
63
- operation: 'CREATE',
64
- model: 'TestModel',
65
- method: 'create',
66
- });
67
-
68
- expect(operationLog[1]).toMatchObject({
69
- operation: 'UPDATE',
70
- model: 'TestModel',
71
- method: 'update',
72
- });
73
-
74
- expect(operationLog[2]).toMatchObject({
75
- operation: 'DELETE',
76
- model: 'TestModel',
77
- method: 'delete',
78
- });
79
- });
80
-
81
- test('should return mock data for create operations', async () => {
82
- const wrapped = createDryRunWrapper(mockRepository, operationLog, 'TestModel');
83
-
84
- const result = await wrapped.create({ name: 'Test', value: 42 });
85
-
86
- expect(result).toMatchObject({
87
- name: 'Test',
88
- value: 42,
89
- _dryRun: true,
90
- });
91
-
92
- expect(result.id).toMatch(/^dry-run-/);
93
- expect(result.createdAt).toBeDefined();
94
- });
95
-
96
- test('should return mock data for update operations', async () => {
97
- const wrapped = createDryRunWrapper(mockRepository, operationLog, 'TestModel');
98
-
99
- const result = await wrapped.update('123', { status: 'inactive' });
100
-
101
- expect(result).toMatchObject({
102
- id: '123',
103
- status: 'inactive',
104
- _dryRun: true,
105
- });
106
- });
107
-
108
- test('should return mock data for delete operations', async () => {
109
- const wrapped = createDryRunWrapper(mockRepository, operationLog, 'TestModel');
110
-
111
- const result = await wrapped.delete('123');
112
-
113
- expect(result).toEqual({
114
- deletedCount: 1,
115
- _dryRun: true,
116
- });
117
- });
118
-
119
- test('should try to return existing data for updates when possible', async () => {
120
- const wrapped = createDryRunWrapper(mockRepository, operationLog, 'TestModel');
121
-
122
- const result = await wrapped.updateStatus('123', 'inactive');
123
-
124
- // Should attempt to read existing data
125
- expect(mockRepository.findById).toHaveBeenCalledWith('123');
126
-
127
- // If found, should return existing merged with updates
128
- expect(result.id).toBe('123');
129
- });
130
- });
131
-
132
- describe('sanitizeArgs', () => {
133
- test('should redact sensitive fields in objects', () => {
134
- const args = [
135
- {
136
- id: '123',
137
- password: 'secret123',
138
- token: 'abc-def-ghi',
139
- apiKey: 'sk_live_123',
140
- name: 'Test User',
141
- },
142
- ];
143
-
144
- const sanitized = sanitizeArgs(args);
145
-
146
- expect(sanitized[0]).toEqual({
147
- id: '123',
148
- password: '[REDACTED]',
149
- token: '[REDACTED]',
150
- apiKey: '[REDACTED]',
151
- name: 'Test User',
152
- });
153
- });
154
-
155
- test('should handle nested objects', () => {
156
- const args = [
157
- {
158
- user: {
159
- name: 'Test',
160
- credentials: {
161
- password: 'secret',
162
- apiToken: 'token123',
163
- },
164
- },
165
- },
166
- ];
167
-
168
- const sanitized = sanitizeArgs(args);
169
-
170
- expect(sanitized[0].user.name).toBe('Test');
171
- expect(sanitized[0].user.credentials.password).toBe('[REDACTED]');
172
- expect(sanitized[0].user.credentials.apiToken).toBe('[REDACTED]');
173
- });
174
-
175
- test('should handle arrays', () => {
176
- const args = [
177
- [
178
- { id: '1', token: 'abc' },
179
- { id: '2', secret: 'xyz' },
180
- ],
181
- ];
182
-
183
- const sanitized = sanitizeArgs(args);
184
-
185
- expect(sanitized[0][0].token).toBe('[REDACTED]');
186
- expect(sanitized[0][1].secret).toBe('[REDACTED]');
187
- });
188
-
189
- test('should preserve primitives', () => {
190
- const args = ['string', 123, true, null, undefined];
191
- const sanitized = sanitizeArgs(args);
192
-
193
- expect(sanitized).toEqual(['string', 123, true, null, undefined]);
194
- });
195
- });
196
-
197
- describe('wrapAdminFriggCommandsForDryRun', () => {
198
- let mockCommands;
199
- let operationLog;
200
-
201
- beforeEach(() => {
202
- operationLog = [];
203
- mockCommands = {
204
- // Read operations
205
- findIntegrationById: jest.fn(async (id) => ({ id, status: 'active' })),
206
- listIntegrations: jest.fn(async () => []),
207
-
208
- // Write operations
209
- updateIntegrationConfig: jest.fn(async (id, config) => ({ id, config })),
210
- updateIntegrationStatus: jest.fn(async (id, status) => ({ id, status })),
211
- updateCredential: jest.fn(async (id, updates) => ({ id, ...updates })),
212
-
213
- // Other methods
214
- log: jest.fn(),
215
- };
216
- });
217
-
218
- test('should pass through read operations', async () => {
219
- const wrapped = wrapAdminFriggCommandsForDryRun(mockCommands, operationLog);
220
-
221
- const integration = await wrapped.findIntegrationById('123');
222
- const list = await wrapped.listIntegrations();
223
-
224
- expect(mockCommands.findIntegrationById).toHaveBeenCalledWith('123');
225
- expect(mockCommands.listIntegrations).toHaveBeenCalled();
226
-
227
- expect(integration.id).toBe('123');
228
- expect(operationLog).toHaveLength(0);
229
- });
230
-
231
- test('should intercept write operations', async () => {
232
- const wrapped = wrapAdminFriggCommandsForDryRun(mockCommands, operationLog);
233
-
234
- await wrapped.updateIntegrationConfig('123', { setting: 'value' });
235
- await wrapped.updateIntegrationStatus('456', 'inactive');
236
-
237
- expect(mockCommands.updateIntegrationConfig).not.toHaveBeenCalled();
238
- expect(mockCommands.updateIntegrationStatus).not.toHaveBeenCalled();
239
-
240
- expect(operationLog).toHaveLength(2);
241
- expect(operationLog[0].operation).toBe('UPDATEINTEGRATIONCONFIG');
242
- expect(operationLog[1].operation).toBe('UPDATEINTEGRATIONSTATUS');
243
- });
244
-
245
- test('should return existing data for known update methods', async () => {
246
- const wrapped = wrapAdminFriggCommandsForDryRun(mockCommands, operationLog);
247
-
248
- const result = await wrapped.updateIntegrationConfig('123', { new: 'config' });
249
-
250
- // Should have tried to fetch existing
251
- expect(mockCommands.findIntegrationById).toHaveBeenCalledWith('123');
252
-
253
- // Should return existing data
254
- expect(result.id).toBe('123');
255
- });
256
- });
257
- });
@@ -1,296 +0,0 @@
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
- };