@friggframework/admin-scripts 2.0.0--canary.522.cbd3d5a.0 → 2.0.0--canary.517.35ee143.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 (30) hide show
  1. package/index.js +2 -2
  2. package/package.json +6 -9
  3. package/src/application/__tests__/admin-frigg-commands.test.js +19 -19
  4. package/src/application/__tests__/admin-script-base.test.js +2 -2
  5. package/src/application/__tests__/script-runner.test.js +146 -16
  6. package/src/application/admin-frigg-commands.js +8 -8
  7. package/src/application/admin-script-base.js +3 -5
  8. package/src/application/script-runner.js +125 -129
  9. package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +168 -0
  10. package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +114 -0
  11. package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +201 -0
  12. package/src/application/use-cases/delete-schedule-use-case.js +108 -0
  13. package/src/application/use-cases/get-effective-schedule-use-case.js +78 -0
  14. package/src/application/use-cases/index.js +18 -0
  15. package/src/application/use-cases/upsert-schedule-use-case.js +127 -0
  16. package/src/builtins/__tests__/integration-health-check.test.js +1 -1
  17. package/src/builtins/__tests__/oauth-token-refresh.test.js +1 -1
  18. package/src/builtins/integration-health-check.js +1 -1
  19. package/src/builtins/oauth-token-refresh.js +1 -1
  20. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
  21. package/src/infrastructure/__tests__/admin-script-router.test.js +46 -47
  22. package/src/infrastructure/admin-auth-middleware.js +5 -43
  23. package/src/infrastructure/admin-script-router.js +38 -32
  24. package/src/infrastructure/script-executor-handler.js +2 -2
  25. package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
  26. package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
  27. package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
  28. package/src/application/dry-run-http-interceptor.js +0 -296
  29. package/src/application/dry-run-repository-wrapper.js +0 -261
  30. package/src/application/schedule-management-use-case.js +0 -230
@@ -1,28 +1,36 @@
1
1
  const express = require('express');
2
2
  const serverless = require('serverless-http');
3
- const { adminAuthMiddleware } = require('./admin-auth-middleware');
3
+ const { validateAdminApiKey } = require('./admin-auth-middleware');
4
4
  const { getScriptFactory } = require('../application/script-factory');
5
5
  const { createScriptRunner } = require('../application/script-runner');
6
6
  const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
7
7
  const { QueuerUtil } = require('@friggframework/core/queues');
8
8
  const { createSchedulerAdapter } = require('../adapters/scheduler-adapter-factory');
9
- const { ScheduleManagementUseCase } = require('../application/schedule-management-use-case');
9
+ const {
10
+ GetEffectiveScheduleUseCase,
11
+ UpsertScheduleUseCase,
12
+ DeleteScheduleUseCase,
13
+ } = require('../application/use-cases');
10
14
 
11
15
  const router = express.Router();
12
16
 
13
17
  // Apply auth middleware to all admin routes
14
- router.use(adminAuthMiddleware);
18
+ router.use(validateAdminApiKey);
15
19
 
16
20
  /**
17
- * Create ScheduleManagementUseCase instance
21
+ * Create schedule use case instances
18
22
  * @private
19
23
  */
20
- function createScheduleManagementUseCase() {
21
- return new ScheduleManagementUseCase({
22
- commands: createAdminScriptCommands(),
23
- schedulerAdapter: createSchedulerAdapter(),
24
- scriptFactory: getScriptFactory(),
25
- });
24
+ function createScheduleUseCases() {
25
+ const commands = createAdminScriptCommands();
26
+ const schedulerAdapter = createSchedulerAdapter();
27
+ const scriptFactory = getScriptFactory();
28
+
29
+ return {
30
+ getEffectiveSchedule: new GetEffectiveScheduleUseCase({ commands, scriptFactory }),
31
+ upsertSchedule: new UpsertScheduleUseCase({ commands, schedulerAdapter, scriptFactory }),
32
+ deleteSchedule: new DeleteScheduleUseCase({ commands, schedulerAdapter, scriptFactory }),
33
+ };
26
34
  }
27
35
 
28
36
  /**
@@ -40,8 +48,8 @@ router.get('/scripts', async (req, res) => {
40
48
  version: s.definition.version,
41
49
  description: s.definition.description,
42
50
  category: s.definition.display?.category || 'custom',
43
- requiresIntegrationFactory:
44
- s.definition.config?.requiresIntegrationFactory || false,
51
+ requireIntegrationInstance:
52
+ s.definition.config?.requireIntegrationInstance || false,
45
53
  schedule: s.definition.schedule || null,
46
54
  })),
47
55
  });
@@ -87,10 +95,10 @@ router.get('/scripts/:scriptName', async (req, res) => {
87
95
  });
88
96
 
89
97
  /**
90
- * POST /admin/scripts/:scriptName/execute
98
+ * POST /admin/scripts/:scriptName
91
99
  * Execute a script (sync, async, or dry-run)
92
100
  */
93
- router.post('/scripts/:scriptName/execute', async (req, res) => {
101
+ router.post('/scripts/:scriptName', async (req, res) => {
94
102
  try {
95
103
  const { scriptName } = req.params;
96
104
  const { params = {}, mode = 'async', dryRun = false } = req.body;
@@ -110,7 +118,6 @@ router.post('/scripts/:scriptName/execute', async (req, res) => {
110
118
  trigger: 'MANUAL',
111
119
  mode: 'sync',
112
120
  dryRun: true,
113
- audit: req.adminAudit,
114
121
  });
115
122
  return res.json(result);
116
123
  }
@@ -121,20 +128,18 @@ router.post('/scripts/:scriptName/execute', async (req, res) => {
121
128
  const result = await runner.execute(scriptName, params, {
122
129
  trigger: 'MANUAL',
123
130
  mode: 'sync',
124
- audit: req.adminAudit,
125
131
  });
126
132
  return res.json(result);
127
133
  }
128
134
 
129
135
  // Async execution - queue and return immediately
130
136
  const commands = createAdminScriptCommands();
131
- const execution = await commands.createScriptExecution({
137
+ const execution = await commands.createAdminProcess({
132
138
  scriptName,
133
139
  scriptVersion: factory.get(scriptName).Definition.version,
134
140
  trigger: 'MANUAL',
135
141
  mode: 'async',
136
142
  input: params,
137
- audit: req.adminAudit,
138
143
  });
139
144
 
140
145
  // Queue the execution
@@ -161,14 +166,14 @@ router.post('/scripts/:scriptName/execute', async (req, res) => {
161
166
  });
162
167
 
163
168
  /**
164
- * GET /admin/executions/:executionId
165
- * Get execution status
169
+ * GET /admin/scripts/:scriptName/executions/:executionId
170
+ * Get execution status for specific script
166
171
  */
167
- router.get('/executions/:executionId', async (req, res) => {
172
+ router.get('/scripts/:scriptName/executions/:executionId', async (req, res) => {
168
173
  try {
169
174
  const { executionId } = req.params;
170
175
  const commands = createAdminScriptCommands();
171
- const execution = await commands.findScriptExecutionById(executionId);
176
+ const execution = await commands.findAdminProcessById(executionId);
172
177
 
173
178
  if (execution.error) {
174
179
  return res.status(execution.error).json({
@@ -185,12 +190,13 @@ router.get('/executions/:executionId', async (req, res) => {
185
190
  });
186
191
 
187
192
  /**
188
- * GET /admin/executions
189
- * List recent executions
193
+ * GET /admin/scripts/:scriptName/executions
194
+ * List recent executions for specific script
190
195
  */
191
- router.get('/executions', async (req, res) => {
196
+ router.get('/scripts/:scriptName/executions', async (req, res) => {
192
197
  try {
193
- const { scriptName, status, limit = 50 } = req.query;
198
+ const { scriptName } = req.params;
199
+ const { status, limit = 50 } = req.query;
194
200
  const commands = createAdminScriptCommands();
195
201
 
196
202
  const executions = await commands.findRecentExecutions({
@@ -213,9 +219,9 @@ router.get('/executions', async (req, res) => {
213
219
  router.get('/scripts/:scriptName/schedule', async (req, res) => {
214
220
  try {
215
221
  const { scriptName } = req.params;
216
- const useCase = createScheduleManagementUseCase();
222
+ const { getEffectiveSchedule } = createScheduleUseCases();
217
223
 
218
- const result = await useCase.getEffectiveSchedule(scriptName);
224
+ const result = await getEffectiveSchedule.execute(scriptName);
219
225
 
220
226
  res.json({
221
227
  source: result.source,
@@ -242,9 +248,9 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
242
248
  try {
243
249
  const { scriptName } = req.params;
244
250
  const { enabled, cronExpression, timezone } = req.body;
245
- const useCase = createScheduleManagementUseCase();
251
+ const { upsertSchedule } = createScheduleUseCases();
246
252
 
247
- const result = await useCase.upsertSchedule(scriptName, {
253
+ const result = await upsertSchedule.execute(scriptName, {
248
254
  enabled,
249
255
  cronExpression,
250
256
  timezone,
@@ -283,9 +289,9 @@ router.put('/scripts/:scriptName/schedule', async (req, res) => {
283
289
  router.delete('/scripts/:scriptName/schedule', async (req, res) => {
284
290
  try {
285
291
  const { scriptName } = req.params;
286
- const useCase = createScheduleManagementUseCase();
292
+ const { deleteSchedule } = createScheduleUseCases();
287
293
 
288
- const result = await useCase.deleteSchedule(scriptName);
294
+ const result = await deleteSchedule.execute(scriptName);
289
295
 
290
296
  res.json(result);
291
297
  } catch (error) {
@@ -21,7 +21,7 @@ async function handler(event) {
21
21
 
22
22
  // If executionId provided (async from API), update existing record
23
23
  if (executionId) {
24
- await commands.updateScriptExecutionStatus(executionId, 'RUNNING');
24
+ await commands.updateAdminProcessState(executionId, 'RUNNING');
25
25
  }
26
26
 
27
27
  const result = await runner.execute(scriptName, params, {
@@ -45,7 +45,7 @@ async function handler(event) {
45
45
  if (executionId) {
46
46
  const commands = createAdminScriptCommands();
47
47
  await commands
48
- .completeScriptExecution(executionId, {
48
+ .completeAdminProcess(executionId, {
49
49
  status: 'FAILED',
50
50
  error: {
51
51
  name: error.name,
@@ -1,313 +0,0 @@
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
- });