@friggframework/admin-scripts 2.0.0--canary.517.a37d697.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.
package/package.json CHANGED
@@ -1,23 +1,21 @@
1
1
  {
2
2
  "name": "@friggframework/admin-scripts",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.517.a37d697.0",
4
+ "version": "2.0.0--canary.517.300ded3.0",
5
5
  "description": "Admin Script Runner for Frigg - Execute maintenance and operational scripts in hosted environments",
6
6
  "dependencies": {
7
7
  "@aws-sdk/client-scheduler": "^3.588.0",
8
- "@friggframework/core": "2.0.0--canary.517.a37d697.0",
8
+ "@friggframework/core": "2.0.0--canary.517.300ded3.0",
9
9
  "bcryptjs": "^2.4.3",
10
10
  "express": "^4.18.2",
11
11
  "lodash": "4.17.21",
12
- "mongoose": "6.11.6",
13
12
  "serverless-http": "^3.2.0",
14
13
  "uuid": "^9.0.1"
15
14
  },
16
15
  "devDependencies": {
17
- "@friggframework/eslint-config": "2.0.0--canary.517.a37d697.0",
18
- "@friggframework/prettier-config": "2.0.0--canary.517.a37d697.0",
19
- "@friggframework/test": "2.0.0--canary.517.a37d697.0",
20
- "chai": "^4.3.6",
16
+ "@friggframework/eslint-config": "2.0.0--canary.517.300ded3.0",
17
+ "@friggframework/prettier-config": "2.0.0--canary.517.300ded3.0",
18
+ "@friggframework/test": "2.0.0--canary.517.300ded3.0",
21
19
  "eslint": "^8.22.0",
22
20
  "jest": "^29.7.0",
23
21
  "prettier": "^2.7.1",
@@ -49,5 +47,5 @@
49
47
  "maintenance",
50
48
  "operations"
51
49
  ],
52
- "gitHead": "a37d697613667fff814938b1f30ee2d834c4ffcd"
50
+ "gitHead": "300ded3ac35558075a081104b4b362b85cf0756f"
53
51
  }
@@ -93,7 +93,7 @@ describe('ScriptRunner', () => {
93
93
  expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
94
94
  'exec-123',
95
95
  expect.objectContaining({
96
- status: 'COMPLETED',
96
+ state: 'COMPLETED',
97
97
  output: { success: true, params: { foo: 'bar' } },
98
98
  metrics: expect.objectContaining({
99
99
  durationMs: expect.any(Number),
@@ -131,7 +131,7 @@ describe('ScriptRunner', () => {
131
131
  expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
132
132
  'exec-123',
133
133
  expect.objectContaining({
134
- status: 'FAILED',
134
+ state: 'FAILED',
135
135
  error: expect.objectContaining({
136
136
  message: 'Script failed',
137
137
  }),
@@ -186,6 +186,136 @@ describe('ScriptRunner', () => {
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
+
189
319
  describe('createScriptRunner()', () => {
190
320
  it('should create runner with default factory', () => {
191
321
  const runner = createScriptRunner();
@@ -1,8 +1,6 @@
1
1
  const { getScriptFactory } = require('./script-factory');
2
2
  const { createAdminFriggCommands } = require('./admin-frigg-commands');
3
3
  const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
4
- const { wrapAdminFriggCommandsForDryRun } = require('./dry-run-repository-wrapper');
5
- const { createDryRunHttpClient, injectDryRunHttpClient } = require('./dry-run-http-interceptor');
6
4
 
7
5
  /**
8
6
  * Script Runner
@@ -30,7 +28,7 @@ class ScriptRunner {
30
28
  * @param {string} options.mode - 'sync' | 'async'
31
29
  * @param {Object} options.audit - Audit info { apiKeyName, apiKeyLast4, ipAddress }
32
30
  * @param {string} options.executionId - Reuse existing execution ID
33
- * @param {boolean} options.dryRun - Execute in dry-run mode (no writes, log operations)
31
+ * @param {boolean} options.dryRun - Dry-run mode: validate and preview without executing
34
32
  */
35
33
  async execute(scriptName, params = {}, options = {}) {
36
34
  const { trigger = 'MANUAL', audit = {}, executionId: existingExecutionId, dryRun = false } = options;
@@ -46,6 +44,11 @@ class ScriptRunner {
46
44
  );
47
45
  }
48
46
 
47
+ // Dry-run mode: validate and return preview without executing
48
+ if (dryRun) {
49
+ return this.createDryRunPreview(scriptName, definition, params);
50
+ }
51
+
49
52
  let executionId = existingExecutionId;
50
53
 
51
54
  // Create execution record if not provided
@@ -64,25 +67,13 @@ class ScriptRunner {
64
67
  const startTime = new Date();
65
68
 
66
69
  try {
67
- // Update status to RUNNING (skip in dry-run)
68
- if (!dryRun) {
69
- await this.commands.updateAdminProcessState(executionId, 'RUNNING');
70
- }
70
+ await this.commands.updateAdminProcessState(executionId, 'RUNNING');
71
71
 
72
72
  // Create frigg commands for the script
73
- let frigg;
74
- let operationLog = [];
75
-
76
- if (dryRun) {
77
- // Dry-run mode: wrap commands to intercept writes
78
- frigg = this.createDryRunFriggCommands(operationLog);
79
- } else {
80
- // Normal mode: create real commands
81
- frigg = createAdminFriggCommands({
82
- executionId,
83
- integrationFactory: this.integrationFactory,
84
- });
85
- }
73
+ const frigg = createAdminFriggCommands({
74
+ executionId,
75
+ integrationFactory: this.integrationFactory,
76
+ });
86
77
 
87
78
  // Create script instance
88
79
  const script = this.scriptFactory.createInstance(scriptName, {
@@ -97,34 +88,15 @@ class ScriptRunner {
97
88
  const endTime = new Date();
98
89
  const durationMs = endTime - startTime;
99
90
 
100
- // Complete execution (skip in dry-run)
101
- if (!dryRun) {
102
- await this.commands.completeAdminProcess(executionId, {
103
- status: 'COMPLETED',
104
- output,
105
- metrics: {
106
- startTime: startTime.toISOString(),
107
- endTime: endTime.toISOString(),
108
- durationMs,
109
- },
110
- });
111
- }
112
-
113
- // Return dry-run preview if in dry-run mode
114
- if (dryRun) {
115
- return {
116
- executionId,
117
- dryRun: true,
118
- status: 'DRY_RUN_COMPLETED',
119
- scriptName,
120
- preview: {
121
- operations: operationLog,
122
- summary: this.summarizeOperations(operationLog),
123
- scriptOutput: output,
124
- },
125
- metrics: { durationMs },
126
- };
127
- }
91
+ await this.commands.completeAdminProcess(executionId, {
92
+ state: 'COMPLETED',
93
+ output,
94
+ metrics: {
95
+ startTime: startTime.toISOString(),
96
+ endTime: endTime.toISOString(),
97
+ durationMs,
98
+ },
99
+ });
128
100
 
129
101
  return {
130
102
  executionId,
@@ -134,31 +106,26 @@ class ScriptRunner {
134
106
  metrics: { durationMs },
135
107
  };
136
108
  } catch (error) {
137
- // Calculate metrics even on failure
138
109
  const endTime = new Date();
139
110
  const durationMs = endTime - startTime;
140
111
 
141
- // Record failure (skip in dry-run)
142
- if (!dryRun) {
143
- await this.commands.completeAdminProcess(executionId, {
144
- status: 'FAILED',
145
- error: {
146
- name: error.name,
147
- message: error.message,
148
- stack: error.stack,
149
- },
150
- metrics: {
151
- startTime: startTime.toISOString(),
152
- endTime: endTime.toISOString(),
153
- durationMs,
154
- },
155
- });
156
- }
112
+ await this.commands.completeAdminProcess(executionId, {
113
+ state: 'FAILED',
114
+ error: {
115
+ name: error.name,
116
+ message: error.message,
117
+ stack: error.stack,
118
+ },
119
+ metrics: {
120
+ startTime: startTime.toISOString(),
121
+ endTime: endTime.toISOString(),
122
+ durationMs,
123
+ },
124
+ });
157
125
 
158
126
  return {
159
127
  executionId,
160
- dryRun,
161
- status: dryRun ? 'DRY_RUN_FAILED' : 'FAILED',
128
+ status: 'FAILED',
162
129
  scriptName,
163
130
  error: {
164
131
  name: error.name,
@@ -170,80 +137,107 @@ class ScriptRunner {
170
137
  }
171
138
 
172
139
  /**
173
- * Create dry-run version of AdminFriggCommands
174
- * Intercepts all write operations and logs them
140
+ * Create dry-run preview without executing the script
141
+ * Validates inputs and shows what would be executed
175
142
  *
176
- * @param {Array} operationLog - Array to collect logged operations
177
- * @returns {Object} Wrapped AdminFriggCommands
143
+ * @param {string} scriptName - Script name
144
+ * @param {Object} definition - Script definition
145
+ * @param {Object} params - Input parameters
146
+ * @returns {Object} Dry-run preview
178
147
  */
179
- createDryRunFriggCommands(operationLog) {
180
- // Create real commands (for read operations)
181
- const realCommands = createAdminFriggCommands({
182
- executionId: null, // Don't persist logs in dry-run
183
- integrationFactory: this.integrationFactory,
184
- });
185
-
186
- // Wrap commands to intercept writes
187
- const wrappedCommands = wrapAdminFriggCommandsForDryRun(realCommands, operationLog);
148
+ createDryRunPreview(scriptName, definition, params) {
149
+ const validation = this.validateParams(definition, params);
150
+
151
+ return {
152
+ dryRun: true,
153
+ status: validation.valid ? 'DRY_RUN_VALID' : 'DRY_RUN_INVALID',
154
+ scriptName,
155
+ preview: {
156
+ script: {
157
+ name: definition.name,
158
+ version: definition.version,
159
+ description: definition.description,
160
+ requiresIntegrationFactory: definition.config?.requiresIntegrationFactory || false,
161
+ },
162
+ input: params,
163
+ inputSchema: definition.inputSchema || null,
164
+ validation,
165
+ },
166
+ message: validation.valid
167
+ ? 'Dry-run validation passed. Script is ready to execute with provided parameters.'
168
+ : `Dry-run validation failed: ${validation.errors.join(', ')}`,
169
+ };
170
+ }
188
171
 
189
- // Create dry-run HTTP client
190
- const dryRunHttpClient = createDryRunHttpClient(operationLog);
172
+ /**
173
+ * Validate parameters against script's input schema
174
+ *
175
+ * @param {Object} definition - Script definition
176
+ * @param {Object} params - Input parameters
177
+ * @returns {Object} Validation result { valid, errors }
178
+ */
179
+ validateParams(definition, params) {
180
+ const errors = [];
181
+ const schema = definition.inputSchema;
191
182
 
192
- // Override instantiate to inject dry-run HTTP client
193
- const originalInstantiate = wrappedCommands.instantiate.bind(wrappedCommands);
194
- wrappedCommands.instantiate = async (integrationId) => {
195
- const instance = await originalInstantiate(integrationId);
183
+ if (!schema) {
184
+ return { valid: true, errors: [] };
185
+ }
196
186
 
197
- // Inject dry-run HTTP client into the integration instance
198
- injectDryRunHttpClient(instance, dryRunHttpClient);
187
+ // Check required fields
188
+ if (schema.required && Array.isArray(schema.required)) {
189
+ for (const field of schema.required) {
190
+ if (params[field] === undefined || params[field] === null) {
191
+ errors.push(`Missing required parameter: ${field}`);
192
+ }
193
+ }
194
+ }
199
195
 
200
- return instance;
201
- };
196
+ // Basic type validation for properties
197
+ if (schema.properties) {
198
+ for (const [key, prop] of Object.entries(schema.properties)) {
199
+ const value = params[key];
200
+ if (value !== undefined && value !== null) {
201
+ const typeError = this.validateType(key, value, prop);
202
+ if (typeError) {
203
+ errors.push(typeError);
204
+ }
205
+ }
206
+ }
207
+ }
202
208
 
203
- return wrappedCommands;
209
+ return { valid: errors.length === 0, errors };
204
210
  }
205
211
 
206
212
  /**
207
- * Summarize operations from dry-run log
208
- *
209
- * @param {Array} log - Operation log
210
- * @returns {Object} Summary statistics
213
+ * Validate a single parameter type
211
214
  */
212
- summarizeOperations(log) {
213
- const summary = {
214
- totalOperations: log.length,
215
- databaseWrites: 0,
216
- httpRequests: 0,
217
- byOperation: {},
218
- byModel: {},
219
- byService: {},
220
- };
215
+ validateType(key, value, schema) {
216
+ const expectedType = schema.type;
217
+ if (!expectedType) return null;
221
218
 
222
- for (const op of log) {
223
- // Count by operation type
224
- const operation = op.operation || op.method || 'UNKNOWN';
225
- summary.byOperation[operation] = (summary.byOperation[operation] || 0) + 1;
226
-
227
- // Database operations
228
- if (op.model) {
229
- summary.databaseWrites++;
230
- summary.byModel[op.model] = summary.byModel[op.model] || [];
231
- summary.byModel[op.model].push({
232
- operation: op.operation,
233
- method: op.method,
234
- timestamp: op.timestamp,
235
- });
236
- }
219
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
237
220
 
238
- // HTTP requests
239
- if (op.operation === 'HTTP_REQUEST') {
240
- summary.httpRequests++;
241
- const service = op.service || 'unknown';
242
- summary.byService[service] = (summary.byService[service] || 0) + 1;
243
- }
221
+ if (expectedType === 'integer' && (typeof value !== 'number' || !Number.isInteger(value))) {
222
+ return `Parameter "${key}" must be an integer`;
223
+ }
224
+ if (expectedType === 'number' && typeof value !== 'number') {
225
+ return `Parameter "${key}" must be a number`;
226
+ }
227
+ if (expectedType === 'string' && typeof value !== 'string') {
228
+ return `Parameter "${key}" must be a string`;
229
+ }
230
+ if (expectedType === 'boolean' && typeof value !== 'boolean') {
231
+ return `Parameter "${key}" must be a boolean`;
232
+ }
233
+ if (expectedType === 'array' && !Array.isArray(value)) {
234
+ return `Parameter "${key}" must be an array`;
235
+ }
236
+ if (expectedType === 'object' && (typeof value !== 'object' || Array.isArray(value))) {
237
+ return `Parameter "${key}" must be an object`;
244
238
  }
245
239
 
246
- return summary;
240
+ return null;
247
241
  }
248
242
  }
249
243