@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.
Files changed (35) hide show
  1. package/LICENSE.md +9 -0
  2. package/index.js +66 -0
  3. package/package.json +53 -0
  4. package/src/adapters/__tests__/aws-scheduler-adapter.test.js +322 -0
  5. package/src/adapters/__tests__/local-scheduler-adapter.test.js +325 -0
  6. package/src/adapters/__tests__/scheduler-adapter-factory.test.js +257 -0
  7. package/src/adapters/__tests__/scheduler-adapter.test.js +103 -0
  8. package/src/adapters/aws-scheduler-adapter.js +138 -0
  9. package/src/adapters/local-scheduler-adapter.js +103 -0
  10. package/src/adapters/scheduler-adapter-factory.js +69 -0
  11. package/src/adapters/scheduler-adapter.js +64 -0
  12. package/src/application/__tests__/admin-frigg-commands.test.js +643 -0
  13. package/src/application/__tests__/admin-script-base.test.js +273 -0
  14. package/src/application/__tests__/dry-run-http-interceptor.test.js +313 -0
  15. package/src/application/__tests__/dry-run-repository-wrapper.test.js +257 -0
  16. package/src/application/__tests__/schedule-management-use-case.test.js +276 -0
  17. package/src/application/__tests__/script-factory.test.js +381 -0
  18. package/src/application/__tests__/script-runner.test.js +202 -0
  19. package/src/application/admin-frigg-commands.js +242 -0
  20. package/src/application/admin-script-base.js +138 -0
  21. package/src/application/dry-run-http-interceptor.js +296 -0
  22. package/src/application/dry-run-repository-wrapper.js +261 -0
  23. package/src/application/schedule-management-use-case.js +230 -0
  24. package/src/application/script-factory.js +161 -0
  25. package/src/application/script-runner.js +254 -0
  26. package/src/builtins/__tests__/integration-health-check.test.js +598 -0
  27. package/src/builtins/__tests__/oauth-token-refresh.test.js +344 -0
  28. package/src/builtins/index.js +28 -0
  29. package/src/builtins/integration-health-check.js +279 -0
  30. package/src/builtins/oauth-token-refresh.js +221 -0
  31. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +148 -0
  32. package/src/infrastructure/__tests__/admin-script-router.test.js +701 -0
  33. package/src/infrastructure/admin-auth-middleware.js +49 -0
  34. package/src/infrastructure/admin-script-router.js +311 -0
  35. package/src/infrastructure/script-executor-handler.js +75 -0
@@ -0,0 +1,273 @@
1
+ const { AdminScriptBase } = require('../admin-script-base');
2
+
3
+ describe('AdminScriptBase', () => {
4
+ describe('Static Definition pattern', () => {
5
+ it('should have a default Definition', () => {
6
+ expect(AdminScriptBase.Definition).toBeDefined();
7
+ expect(AdminScriptBase.Definition.name).toBe('Script Name');
8
+ expect(AdminScriptBase.Definition.version).toBe('0.0.0');
9
+ expect(AdminScriptBase.Definition.description).toBe(
10
+ 'What this script does'
11
+ );
12
+ expect(AdminScriptBase.Definition.source).toBe('USER_DEFINED');
13
+ });
14
+
15
+ it('should allow child classes to override Definition', () => {
16
+ class TestScript extends AdminScriptBase {
17
+ static Definition = {
18
+ name: 'test-script',
19
+ version: '1.0.0',
20
+ description: 'A test script',
21
+ source: 'BUILTIN',
22
+ inputSchema: { type: 'object' },
23
+ outputSchema: { type: 'object' },
24
+ schedule: {
25
+ enabled: true,
26
+ cronExpression: 'cron(0 12 * * ? *)',
27
+ },
28
+ config: {
29
+ timeout: 600000,
30
+ maxRetries: 3,
31
+ requiresIntegrationFactory: true,
32
+ },
33
+ display: {
34
+ label: 'Test Script',
35
+ description: 'For testing',
36
+ category: 'testing',
37
+ },
38
+ };
39
+ }
40
+
41
+ expect(TestScript.Definition.name).toBe('test-script');
42
+ expect(TestScript.Definition.version).toBe('1.0.0');
43
+ expect(TestScript.Definition.description).toBe('A test script');
44
+ expect(TestScript.Definition.source).toBe('BUILTIN');
45
+ expect(TestScript.Definition.schedule.enabled).toBe(true);
46
+ expect(TestScript.Definition.config.timeout).toBe(600000);
47
+ });
48
+ });
49
+
50
+ describe('Static methods', () => {
51
+ it('getName() should return the script name', () => {
52
+ class TestScript extends AdminScriptBase {
53
+ static Definition = {
54
+ name: 'my-script',
55
+ version: '1.0.0',
56
+ description: 'test',
57
+ };
58
+ }
59
+
60
+ expect(TestScript.getName()).toBe('my-script');
61
+ });
62
+
63
+ it('getCurrentVersion() should return the version', () => {
64
+ class TestScript extends AdminScriptBase {
65
+ static Definition = {
66
+ name: 'my-script',
67
+ version: '2.3.1',
68
+ description: 'test',
69
+ };
70
+ }
71
+
72
+ expect(TestScript.getCurrentVersion()).toBe('2.3.1');
73
+ });
74
+
75
+ it('getDefinition() should return the full Definition', () => {
76
+ class TestScript extends AdminScriptBase {
77
+ static Definition = {
78
+ name: 'my-script',
79
+ version: '1.0.0',
80
+ description: 'test',
81
+ source: 'USER_DEFINED',
82
+ };
83
+ }
84
+
85
+ const definition = TestScript.getDefinition();
86
+ expect(definition).toEqual({
87
+ name: 'my-script',
88
+ version: '1.0.0',
89
+ description: 'test',
90
+ source: 'USER_DEFINED',
91
+ });
92
+ });
93
+ });
94
+
95
+ describe('Constructor', () => {
96
+ it('should initialize with default values', () => {
97
+ const script = new AdminScriptBase();
98
+
99
+ expect(script.executionId).toBeNull();
100
+ expect(script.logs).toEqual([]);
101
+ expect(script._startTime).toBeNull();
102
+ expect(script.integrationFactory).toBeNull();
103
+ });
104
+
105
+ it('should accept executionId parameter', () => {
106
+ const script = new AdminScriptBase({ executionId: 'exec_123' });
107
+
108
+ expect(script.executionId).toBe('exec_123');
109
+ });
110
+
111
+ it('should accept integrationFactory parameter', () => {
112
+ const mockFactory = { mock: true };
113
+ const script = new AdminScriptBase({
114
+ integrationFactory: mockFactory,
115
+ });
116
+
117
+ expect(script.integrationFactory).toBe(mockFactory);
118
+ });
119
+
120
+ it('should accept both executionId and integrationFactory', () => {
121
+ const mockFactory = { mock: true };
122
+ const script = new AdminScriptBase({
123
+ executionId: 'exec_456',
124
+ integrationFactory: mockFactory,
125
+ });
126
+
127
+ expect(script.executionId).toBe('exec_456');
128
+ expect(script.integrationFactory).toBe(mockFactory);
129
+ });
130
+ });
131
+
132
+ describe('execute()', () => {
133
+ it('should throw error when not implemented by subclass', async () => {
134
+ const script = new AdminScriptBase();
135
+
136
+ await expect(script.execute({}, {})).rejects.toThrow(
137
+ 'AdminScriptBase.execute() must be implemented by subclass'
138
+ );
139
+ });
140
+
141
+ it('should allow child classes to implement execute()', async () => {
142
+ class TestScript extends AdminScriptBase {
143
+ static Definition = {
144
+ name: 'test',
145
+ version: '1.0.0',
146
+ description: 'test',
147
+ };
148
+
149
+ async execute(frigg, params) {
150
+ return { result: 'success', params };
151
+ }
152
+ }
153
+
154
+ const script = new TestScript();
155
+ const frigg = {};
156
+ const params = { foo: 'bar' };
157
+
158
+ const result = await script.execute(frigg, params);
159
+
160
+ expect(result.result).toBe('success');
161
+ expect(result.params).toEqual({ foo: 'bar' });
162
+ });
163
+ });
164
+
165
+ describe('Logging methods', () => {
166
+ it('log() should create log entry with timestamp', () => {
167
+ const script = new AdminScriptBase();
168
+ const beforeTime = new Date().toISOString();
169
+
170
+ const entry = script.log('info', 'Test message', { key: 'value' });
171
+
172
+ const afterTime = new Date().toISOString();
173
+
174
+ expect(entry.level).toBe('info');
175
+ expect(entry.message).toBe('Test message');
176
+ expect(entry.data).toEqual({ key: 'value' });
177
+ expect(entry.timestamp).toBeDefined();
178
+ expect(entry.timestamp >= beforeTime).toBe(true);
179
+ expect(entry.timestamp <= afterTime).toBe(true);
180
+ });
181
+
182
+ it('log() should add entry to logs array', () => {
183
+ const script = new AdminScriptBase();
184
+
185
+ script.log('info', 'First');
186
+ script.log('error', 'Second');
187
+ script.log('warn', 'Third');
188
+
189
+ const logs = script.getLogs();
190
+
191
+ expect(logs).toHaveLength(3);
192
+ expect(logs[0].message).toBe('First');
193
+ expect(logs[1].message).toBe('Second');
194
+ expect(logs[2].message).toBe('Third');
195
+ });
196
+
197
+ it('log() should default data to empty object', () => {
198
+ const script = new AdminScriptBase();
199
+
200
+ const entry = script.log('info', 'No data');
201
+
202
+ expect(entry.data).toEqual({});
203
+ });
204
+
205
+ it('getLogs() should return logs array', () => {
206
+ const script = new AdminScriptBase();
207
+
208
+ script.log('info', 'Message 1');
209
+ script.log('error', 'Message 2');
210
+
211
+ const logs = script.getLogs();
212
+
213
+ expect(logs).toHaveLength(2);
214
+ expect(logs[0].level).toBe('info');
215
+ expect(logs[1].level).toBe('error');
216
+ });
217
+
218
+ it('clearLogs() should empty logs array', () => {
219
+ const script = new AdminScriptBase();
220
+
221
+ script.log('info', 'Message 1');
222
+ script.log('info', 'Message 2');
223
+ expect(script.getLogs()).toHaveLength(2);
224
+
225
+ script.clearLogs();
226
+
227
+ expect(script.getLogs()).toHaveLength(0);
228
+ });
229
+ });
230
+
231
+ describe('Integration with child classes', () => {
232
+ it('should support full lifecycle', async () => {
233
+ class MyScript extends AdminScriptBase {
234
+ static Definition = {
235
+ name: 'my-script',
236
+ version: '1.0.0',
237
+ description: 'My test script',
238
+ config: {
239
+ requiresIntegrationFactory: true,
240
+ },
241
+ };
242
+
243
+ async execute(frigg, params) {
244
+ this.log('info', 'Starting execution');
245
+ this.log('debug', 'Processing', params);
246
+
247
+ if (this.integrationFactory) {
248
+ this.log('info', 'Integration factory available');
249
+ }
250
+
251
+ return { processed: true };
252
+ }
253
+ }
254
+
255
+ const mockFactory = { getInstanceById: jest.fn() };
256
+ const script = new MyScript({
257
+ executionId: 'exec_789',
258
+ integrationFactory: mockFactory,
259
+ });
260
+
261
+ const frigg = {};
262
+ const result = await script.execute(frigg, { test: 'data' });
263
+
264
+ expect(result).toEqual({ processed: true });
265
+
266
+ const logs = script.getLogs();
267
+ expect(logs).toHaveLength(3);
268
+ expect(logs[0].message).toBe('Starting execution');
269
+ expect(logs[1].message).toBe('Processing');
270
+ expect(logs[2].message).toBe('Integration factory available');
271
+ });
272
+ });
273
+ });
@@ -0,0 +1,313 @@
1
+ const {
2
+ createDryRunHttpClient,
3
+ injectDryRunHttpClient,
4
+ sanitizeHeaders,
5
+ sanitizeData,
6
+ detectService,
7
+ } = require('../dry-run-http-interceptor');
8
+
9
+ describe('Dry-Run HTTP Interceptor', () => {
10
+ describe('sanitizeHeaders', () => {
11
+ test('should redact authorization headers', () => {
12
+ const headers = {
13
+ 'Content-Type': 'application/json',
14
+ Authorization: 'Bearer secret-token',
15
+ 'X-API-Key': 'api-key-123',
16
+ 'User-Agent': 'frigg/1.0',
17
+ };
18
+
19
+ const sanitized = sanitizeHeaders(headers);
20
+
21
+ expect(sanitized['Content-Type']).toBe('application/json');
22
+ expect(sanitized['User-Agent']).toBe('frigg/1.0');
23
+ expect(sanitized.Authorization).toBe('[REDACTED]');
24
+ expect(sanitized['X-API-Key']).toBe('[REDACTED]');
25
+ });
26
+
27
+ test('should handle case variations', () => {
28
+ const headers = {
29
+ authorization: 'Bearer token',
30
+ Authorization: 'Bearer token',
31
+ 'x-api-key': 'key1',
32
+ 'X-API-Key': 'key2',
33
+ };
34
+
35
+ const sanitized = sanitizeHeaders(headers);
36
+
37
+ expect(sanitized.authorization).toBe('[REDACTED]');
38
+ expect(sanitized.Authorization).toBe('[REDACTED]');
39
+ expect(sanitized['x-api-key']).toBe('[REDACTED]');
40
+ expect(sanitized['X-API-Key']).toBe('[REDACTED]');
41
+ });
42
+
43
+ test('should handle null/undefined', () => {
44
+ expect(sanitizeHeaders(null)).toEqual({});
45
+ expect(sanitizeHeaders(undefined)).toEqual({});
46
+ expect(sanitizeHeaders({})).toEqual({});
47
+ });
48
+ });
49
+
50
+ describe('detectService', () => {
51
+ test('should detect CRM services', () => {
52
+ expect(detectService('https://api.hubapi.com')).toBe('HubSpot');
53
+ expect(detectService('https://login.salesforce.com')).toBe('Salesforce');
54
+ expect(detectService('https://api.pipedrive.com')).toBe('Pipedrive');
55
+ expect(detectService('https://api.attio.com')).toBe('Attio');
56
+ });
57
+
58
+ test('should detect communication services', () => {
59
+ expect(detectService('https://slack.com/api')).toBe('Slack');
60
+ expect(detectService('https://discord.com/api')).toBe('Discord');
61
+ expect(detectService('https://graph.teams.microsoft.com')).toBe('Microsoft Teams');
62
+ });
63
+
64
+ test('should detect project management tools', () => {
65
+ expect(detectService('https://app.asana.com/api')).toBe('Asana');
66
+ expect(detectService('https://api.monday.com')).toBe('Monday.com');
67
+ expect(detectService('https://api.trello.com')).toBe('Trello');
68
+ });
69
+
70
+ test('should return unknown for unrecognized services', () => {
71
+ expect(detectService('https://example.com/api')).toBe('unknown');
72
+ expect(detectService(null)).toBe('unknown');
73
+ expect(detectService(undefined)).toBe('unknown');
74
+ });
75
+
76
+ test('should be case insensitive', () => {
77
+ expect(detectService('HTTPS://API.HUBSPOT.COM')).toBe('HubSpot');
78
+ expect(detectService('https://API.SLACK.COM')).toBe('Slack');
79
+ });
80
+ });
81
+
82
+ describe('sanitizeData', () => {
83
+ test('should redact sensitive fields', () => {
84
+ const data = {
85
+ name: 'Test User',
86
+ email: 'test@example.com',
87
+ password: 'secret123',
88
+ apiToken: 'token-abc',
89
+ authKey: 'key-xyz',
90
+ };
91
+
92
+ const sanitized = sanitizeData(data);
93
+
94
+ expect(sanitized.name).toBe('Test User');
95
+ expect(sanitized.email).toBe('test@example.com');
96
+ expect(sanitized.password).toBe('[REDACTED]');
97
+ expect(sanitized.apiToken).toBe('[REDACTED]');
98
+ expect(sanitized.authKey).toBe('[REDACTED]');
99
+ });
100
+
101
+ test('should handle nested objects', () => {
102
+ const data = {
103
+ user: {
104
+ name: 'Test',
105
+ credentials: {
106
+ password: 'secret',
107
+ token: 'abc123',
108
+ },
109
+ },
110
+ };
111
+
112
+ const sanitized = sanitizeData(data);
113
+
114
+ expect(sanitized.user.name).toBe('Test');
115
+ expect(sanitized.user.credentials.password).toBe('[REDACTED]');
116
+ expect(sanitized.user.credentials.token).toBe('[REDACTED]');
117
+ });
118
+
119
+ test('should handle arrays', () => {
120
+ const data = [
121
+ { id: '1', password: 'secret1' },
122
+ { id: '2', apiKey: 'key2' },
123
+ ];
124
+
125
+ const sanitized = sanitizeData(data);
126
+
127
+ expect(sanitized[0].id).toBe('1');
128
+ expect(sanitized[0].password).toBe('[REDACTED]');
129
+ expect(sanitized[1].apiKey).toBe('[REDACTED]');
130
+ });
131
+
132
+ test('should preserve primitives', () => {
133
+ expect(sanitizeData('string')).toBe('string');
134
+ expect(sanitizeData(123)).toBe(123);
135
+ expect(sanitizeData(true)).toBe(true);
136
+ expect(sanitizeData(null)).toBe(null);
137
+ expect(sanitizeData(undefined)).toBe(undefined);
138
+ });
139
+ });
140
+
141
+ describe('createDryRunHttpClient', () => {
142
+ let operationLog;
143
+
144
+ beforeEach(() => {
145
+ operationLog = [];
146
+ });
147
+
148
+ test('should log GET requests', async () => {
149
+ const client = createDryRunHttpClient(operationLog);
150
+
151
+ const response = await client.get('/contacts', {
152
+ baseURL: 'https://api.hubapi.com',
153
+ headers: { Authorization: 'Bearer token' },
154
+ });
155
+
156
+ expect(operationLog).toHaveLength(1);
157
+ expect(operationLog[0]).toMatchObject({
158
+ operation: 'HTTP_REQUEST',
159
+ method: 'GET',
160
+ url: 'https://api.hubapi.com/contacts',
161
+ service: 'HubSpot',
162
+ });
163
+
164
+ expect(operationLog[0].headers.Authorization).toBe('[REDACTED]');
165
+ expect(response.data._dryRun).toBe(true);
166
+ });
167
+
168
+ test('should log POST requests with data', async () => {
169
+ const client = createDryRunHttpClient(operationLog);
170
+
171
+ const postData = {
172
+ name: 'John Doe',
173
+ email: 'john@example.com',
174
+ password: 'secret123',
175
+ };
176
+
177
+ await client.post('/users', postData, {
178
+ baseURL: 'https://api.example.com',
179
+ });
180
+
181
+ expect(operationLog).toHaveLength(1);
182
+ expect(operationLog[0].method).toBe('POST');
183
+ expect(operationLog[0].data.name).toBe('John Doe');
184
+ expect(operationLog[0].data.email).toBe('john@example.com');
185
+ expect(operationLog[0].data.password).toBe('[REDACTED]');
186
+ });
187
+
188
+ test('should log PUT requests', async () => {
189
+ const client = createDryRunHttpClient(operationLog);
190
+
191
+ await client.put('/users/123', { status: 'active' }, {
192
+ baseURL: 'https://api.example.com',
193
+ });
194
+
195
+ expect(operationLog).toHaveLength(1);
196
+ expect(operationLog[0].method).toBe('PUT');
197
+ expect(operationLog[0].data.status).toBe('active');
198
+ });
199
+
200
+ test('should log PATCH requests', async () => {
201
+ const client = createDryRunHttpClient(operationLog);
202
+
203
+ await client.patch('/users/123', { name: 'Updated' });
204
+
205
+ expect(operationLog).toHaveLength(1);
206
+ expect(operationLog[0].method).toBe('PATCH');
207
+ });
208
+
209
+ test('should log DELETE requests', async () => {
210
+ const client = createDryRunHttpClient(operationLog);
211
+
212
+ await client.delete('/users/123', {
213
+ baseURL: 'https://api.example.com',
214
+ });
215
+
216
+ expect(operationLog).toHaveLength(1);
217
+ expect(operationLog[0].method).toBe('DELETE');
218
+ });
219
+
220
+ test('should return mock response', async () => {
221
+ const client = createDryRunHttpClient(operationLog);
222
+
223
+ const response = await client.get('/test');
224
+
225
+ expect(response.status).toBe(200);
226
+ expect(response.statusText).toContain('Dry-Run');
227
+ expect(response.data._dryRun).toBe(true);
228
+ expect(response.headers['x-dry-run']).toBe('true');
229
+ });
230
+
231
+ test('should include query params in log', async () => {
232
+ const client = createDryRunHttpClient(operationLog);
233
+
234
+ await client.get('/search', {
235
+ baseURL: 'https://api.example.com',
236
+ params: { q: 'test', limit: 10 },
237
+ });
238
+
239
+ expect(operationLog[0].params).toEqual({ q: 'test', limit: 10 });
240
+ });
241
+ });
242
+
243
+ describe('injectDryRunHttpClient', () => {
244
+ let operationLog;
245
+ let dryRunClient;
246
+
247
+ beforeEach(() => {
248
+ operationLog = [];
249
+ dryRunClient = createDryRunHttpClient(operationLog);
250
+ });
251
+
252
+ test('should inject into primary API module', () => {
253
+ const integrationInstance = {
254
+ primary: {
255
+ api: {
256
+ _httpClient: { get: jest.fn() },
257
+ },
258
+ },
259
+ };
260
+
261
+ injectDryRunHttpClient(integrationInstance, dryRunClient);
262
+
263
+ expect(integrationInstance.primary.api._httpClient).toBe(dryRunClient);
264
+ });
265
+
266
+ test('should inject into target API module', () => {
267
+ const integrationInstance = {
268
+ target: {
269
+ api: {
270
+ _httpClient: { get: jest.fn() },
271
+ },
272
+ },
273
+ };
274
+
275
+ injectDryRunHttpClient(integrationInstance, dryRunClient);
276
+
277
+ expect(integrationInstance.target.api._httpClient).toBe(dryRunClient);
278
+ });
279
+
280
+ test('should inject into both primary and target', () => {
281
+ const integrationInstance = {
282
+ primary: {
283
+ api: { _httpClient: { get: jest.fn() } },
284
+ },
285
+ target: {
286
+ api: { _httpClient: { get: jest.fn() } },
287
+ },
288
+ };
289
+
290
+ injectDryRunHttpClient(integrationInstance, dryRunClient);
291
+
292
+ expect(integrationInstance.primary.api._httpClient).toBe(dryRunClient);
293
+ expect(integrationInstance.target.api._httpClient).toBe(dryRunClient);
294
+ });
295
+
296
+ test('should handle missing api modules gracefully', () => {
297
+ const integrationInstance = {
298
+ primary: {},
299
+ target: null,
300
+ };
301
+
302
+ expect(() => {
303
+ injectDryRunHttpClient(integrationInstance, dryRunClient);
304
+ }).not.toThrow();
305
+ });
306
+
307
+ test('should handle null integration instance', () => {
308
+ expect(() => {
309
+ injectDryRunHttpClient(null, dryRunClient);
310
+ }).not.toThrow();
311
+ });
312
+ });
313
+ });