@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 +6 -8
- package/src/application/__tests__/script-runner.test.js +132 -2
- package/src/application/script-runner.js +120 -126
- package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
- package/src/application/dry-run-http-interceptor.js +0 -296
- package/src/application/dry-run-repository-wrapper.js +0 -261
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.
|
|
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.
|
|
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.
|
|
18
|
-
"@friggframework/prettier-config": "2.0.0--canary.517.
|
|
19
|
-
"@friggframework/test": "2.0.0--canary.517.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
174
|
-
*
|
|
140
|
+
* Create dry-run preview without executing the script
|
|
141
|
+
* Validates inputs and shows what would be executed
|
|
175
142
|
*
|
|
176
|
-
* @param {
|
|
177
|
-
* @
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const instance = await originalInstantiate(integrationId);
|
|
183
|
+
if (!schema) {
|
|
184
|
+
return { valid: true, errors: [] };
|
|
185
|
+
}
|
|
196
186
|
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
|
209
|
+
return { valid: errors.length === 0, errors };
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* @param {Array} log - Operation log
|
|
210
|
-
* @returns {Object} Summary statistics
|
|
213
|
+
* Validate a single parameter type
|
|
211
214
|
*/
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
240
|
+
return null;
|
|
247
241
|
}
|
|
248
242
|
}
|
|
249
243
|
|