@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,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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
52
|
+
mockCommands.createScriptExecution.mockResolvedValue({
|
|
53
53
|
id: 'exec-123',
|
|
54
54
|
});
|
|
55
|
-
mockCommands.
|
|
56
|
-
mockCommands.
|
|
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.
|
|
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.
|
|
88
|
+
expect(mockCommands.updateScriptExecutionStatus).toHaveBeenCalledWith(
|
|
89
89
|
'exec-123',
|
|
90
90
|
'RUNNING'
|
|
91
91
|
);
|
|
92
92
|
|
|
93
|
-
expect(mockCommands.
|
|
93
|
+
expect(mockCommands.completeScriptExecution).toHaveBeenCalledWith(
|
|
94
94
|
'exec-123',
|
|
95
95
|
expect.objectContaining({
|
|
96
|
-
|
|
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.
|
|
131
|
+
expect(mockCommands.completeScriptExecution).toHaveBeenCalledWith(
|
|
132
132
|
'exec-123',
|
|
133
133
|
expect.objectContaining({
|
|
134
|
-
|
|
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.
|
|
182
|
-
expect(mockCommands.
|
|
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.
|
|
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
|
|
66
|
-
if (!this.
|
|
67
|
-
const {
|
|
68
|
-
this.
|
|
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.
|
|
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.
|
|
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 {
|
|
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.
|
|
90
|
+
this.scriptExecutionRepository = params.scriptExecutionRepository || null;
|
|
91
|
+
this.adminApiKeyRepository = params.adminApiKeyRepository || null;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
/**
|