@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.
@@ -0,0 +1,257 @@
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
+ });
@@ -36,9 +36,9 @@ describe('ScriptRunner', () => {
36
36
  scriptFactory = new ScriptFactory([TestScript]);
37
37
 
38
38
  mockCommands = {
39
- createAdminProcess: jest.fn(),
40
- updateAdminProcessState: jest.fn(),
41
- completeAdminProcess: jest.fn(),
39
+ createScriptExecution: jest.fn(),
40
+ updateScriptExecutionStatus: jest.fn(),
41
+ completeScriptExecution: jest.fn(),
42
42
  };
43
43
 
44
44
  mockFrigg = {
@@ -49,11 +49,11 @@ describe('ScriptRunner', () => {
49
49
  createAdminScriptCommands.mockReturnValue(mockCommands);
50
50
  createAdminFriggCommands.mockReturnValue(mockFrigg);
51
51
 
52
- mockCommands.createAdminProcess.mockResolvedValue({
52
+ mockCommands.createScriptExecution.mockResolvedValue({
53
53
  id: 'exec-123',
54
54
  });
55
- mockCommands.updateAdminProcessState.mockResolvedValue({});
56
- mockCommands.completeAdminProcess.mockResolvedValue({ success: true });
55
+ mockCommands.updateScriptExecutionStatus.mockResolvedValue({});
56
+ mockCommands.completeScriptExecution.mockResolvedValue({ success: true });
57
57
  });
58
58
 
59
59
  afterEach(() => {
@@ -76,7 +76,7 @@ describe('ScriptRunner', () => {
76
76
  expect(result.executionId).toBe('exec-123');
77
77
  expect(result.metrics.durationMs).toBeGreaterThanOrEqual(0);
78
78
 
79
- expect(mockCommands.createAdminProcess).toHaveBeenCalledWith({
79
+ expect(mockCommands.createScriptExecution).toHaveBeenCalledWith({
80
80
  scriptName: 'test-script',
81
81
  scriptVersion: '1.0.0',
82
82
  trigger: 'MANUAL',
@@ -85,15 +85,15 @@ describe('ScriptRunner', () => {
85
85
  audit: { apiKeyName: 'test-key' },
86
86
  });
87
87
 
88
- expect(mockCommands.updateAdminProcessState).toHaveBeenCalledWith(
88
+ expect(mockCommands.updateScriptExecutionStatus).toHaveBeenCalledWith(
89
89
  'exec-123',
90
90
  'RUNNING'
91
91
  );
92
92
 
93
- expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
93
+ expect(mockCommands.completeScriptExecution).toHaveBeenCalledWith(
94
94
  'exec-123',
95
95
  expect.objectContaining({
96
- state: 'COMPLETED',
96
+ status: 'COMPLETED',
97
97
  output: { success: true, params: { foo: 'bar' } },
98
98
  metrics: expect.objectContaining({
99
99
  durationMs: expect.any(Number),
@@ -128,10 +128,10 @@ describe('ScriptRunner', () => {
128
128
  expect(result.scriptName).toBe('failing-script');
129
129
  expect(result.error.message).toBe('Script failed');
130
130
 
131
- expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
131
+ expect(mockCommands.completeScriptExecution).toHaveBeenCalledWith(
132
132
  'exec-123',
133
133
  expect.objectContaining({
134
- state: 'FAILED',
134
+ status: 'FAILED',
135
135
  error: expect.objectContaining({
136
136
  message: 'Script failed',
137
137
  }),
@@ -178,144 +178,14 @@ describe('ScriptRunner', () => {
178
178
  });
179
179
 
180
180
  expect(result.executionId).toBe('existing-exec-456');
181
- expect(mockCommands.createAdminProcess).not.toHaveBeenCalled();
182
- expect(mockCommands.updateAdminProcessState).toHaveBeenCalledWith(
181
+ expect(mockCommands.createScriptExecution).not.toHaveBeenCalled();
182
+ expect(mockCommands.updateScriptExecutionStatus).toHaveBeenCalledWith(
183
183
  'existing-exec-456',
184
184
  'RUNNING'
185
185
  );
186
186
  });
187
187
  });
188
188
 
189
- describe('dry-run mode', () => {
190
- it('should return preview without executing script', async () => {
191
- const runner = new ScriptRunner({ scriptFactory, commands: mockCommands });
192
-
193
- const result = await runner.execute('test-script', { foo: 'bar' }, {
194
- trigger: 'MANUAL',
195
- dryRun: true,
196
- });
197
-
198
- expect(result.dryRun).toBe(true);
199
- expect(result.status).toBe('DRY_RUN_VALID');
200
- expect(result.scriptName).toBe('test-script');
201
- expect(result.preview.script.name).toBe('test-script');
202
- expect(result.preview.script.version).toBe('1.0.0');
203
- expect(result.preview.input).toEqual({ foo: 'bar' });
204
- expect(result.message).toContain('validation passed');
205
-
206
- // Should NOT create execution record or call commands
207
- expect(mockCommands.createAdminProcess).not.toHaveBeenCalled();
208
- expect(mockCommands.updateAdminProcessState).not.toHaveBeenCalled();
209
- expect(mockCommands.completeAdminProcess).not.toHaveBeenCalled();
210
- });
211
-
212
- it('should validate required parameters in dry-run', async () => {
213
- class SchemaScript extends AdminScriptBase {
214
- static Definition = {
215
- name: 'schema-script',
216
- version: '1.0.0',
217
- description: 'Script with schema',
218
- inputSchema: {
219
- type: 'object',
220
- required: ['requiredParam'],
221
- properties: {
222
- requiredParam: { type: 'string' },
223
- optionalParam: { type: 'number' },
224
- },
225
- },
226
- };
227
-
228
- async execute() {
229
- return {};
230
- }
231
- }
232
-
233
- scriptFactory.register(SchemaScript);
234
- const runner = new ScriptRunner({ scriptFactory, commands: mockCommands });
235
-
236
- // Missing required parameter
237
- const result = await runner.execute('schema-script', {}, {
238
- dryRun: true,
239
- });
240
-
241
- expect(result.status).toBe('DRY_RUN_INVALID');
242
- expect(result.preview.validation.valid).toBe(false);
243
- expect(result.preview.validation.errors).toContain('Missing required parameter: requiredParam');
244
- });
245
-
246
- it('should validate parameter types in dry-run', async () => {
247
- class TypedScript extends AdminScriptBase {
248
- static Definition = {
249
- name: 'typed-script',
250
- version: '1.0.0',
251
- description: 'Script with typed params',
252
- inputSchema: {
253
- type: 'object',
254
- properties: {
255
- count: { type: 'integer' },
256
- name: { type: 'string' },
257
- enabled: { type: 'boolean' },
258
- },
259
- },
260
- };
261
-
262
- async execute() {
263
- return {};
264
- }
265
- }
266
-
267
- scriptFactory.register(TypedScript);
268
- const runner = new ScriptRunner({ scriptFactory, commands: mockCommands });
269
-
270
- const result = await runner.execute('typed-script', {
271
- count: 'not-a-number',
272
- name: 123,
273
- enabled: 'true',
274
- }, {
275
- dryRun: true,
276
- });
277
-
278
- expect(result.status).toBe('DRY_RUN_INVALID');
279
- expect(result.preview.validation.errors).toHaveLength(3);
280
- });
281
-
282
- it('should pass validation with correct parameters', async () => {
283
- class ValidScript extends AdminScriptBase {
284
- static Definition = {
285
- name: 'valid-script',
286
- version: '1.0.0',
287
- description: 'Script for validation',
288
- inputSchema: {
289
- type: 'object',
290
- required: ['name'],
291
- properties: {
292
- name: { type: 'string' },
293
- count: { type: 'integer' },
294
- },
295
- },
296
- };
297
-
298
- async execute() {
299
- return {};
300
- }
301
- }
302
-
303
- scriptFactory.register(ValidScript);
304
- const runner = new ScriptRunner({ scriptFactory, commands: mockCommands });
305
-
306
- const result = await runner.execute('valid-script', {
307
- name: 'test',
308
- count: 42,
309
- }, {
310
- dryRun: true,
311
- });
312
-
313
- expect(result.status).toBe('DRY_RUN_VALID');
314
- expect(result.preview.validation.valid).toBe(true);
315
- expect(result.preview.validation.errors).toHaveLength(0);
316
- });
317
- });
318
-
319
189
  describe('createScriptRunner()', () => {
320
190
  it('should create runner with default factory', () => {
321
191
  const runner = createScriptRunner();
@@ -25,7 +25,7 @@ class AdminFriggCommands {
25
25
  this._userRepository = null;
26
26
  this._moduleRepository = null;
27
27
  this._credentialRepository = null;
28
- this._adminProcessRepository = null;
28
+ this._scriptExecutionRepository = null;
29
29
  }
30
30
 
31
31
  // ==================== LAZY-LOADED REPOSITORIES ====================
@@ -62,12 +62,12 @@ class AdminFriggCommands {
62
62
  return this._credentialRepository;
63
63
  }
64
64
 
65
- get adminProcessRepository() {
66
- if (!this._adminProcessRepository) {
67
- const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
68
- this._adminProcessRepository = createAdminProcessRepository();
65
+ get scriptExecutionRepository() {
66
+ if (!this._scriptExecutionRepository) {
67
+ const { createScriptExecutionRepository } = require('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
68
+ this._scriptExecutionRepository = createScriptExecutionRepository();
69
69
  }
70
- return this._adminProcessRepository;
70
+ return this._scriptExecutionRepository;
71
71
  }
72
72
 
73
73
  // ==================== INTEGRATION QUERIES ====================
@@ -209,7 +209,7 @@ class AdminFriggCommands {
209
209
 
210
210
  // Persist to execution record if we have an executionId
211
211
  if (this.executionId) {
212
- this.adminProcessRepository.appendProcessLog(this.executionId, entry)
212
+ this.scriptExecutionRepository.appendExecutionLog(this.executionId, entry)
213
213
  .catch(err => console.error('Failed to persist log:', err));
214
214
  }
215
215
 
@@ -1,4 +1,5 @@
1
- const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
1
+ const { createScriptExecutionRepository } = require('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
2
+ const { createAdminApiKeyRepository } = require('@friggframework/core/admin-scripts/repositories/admin-api-key-repository-factory');
2
3
 
3
4
  /**
4
5
  * Admin Script Base Class
@@ -86,7 +87,8 @@ class AdminScriptBase {
86
87
  this.integrationFactory = params.integrationFactory || null;
87
88
 
88
89
  // OPTIONAL: Injected repositories (for testing or custom implementations)
89
- this.adminProcessRepository = params.adminProcessRepository || null;
90
+ this.scriptExecutionRepository = params.scriptExecutionRepository || null;
91
+ this.adminApiKeyRepository = params.adminApiKeyRepository || null;
90
92
  }
91
93
 
92
94
  /**