@gugananuvem/aws-local-simulator 1.0.34 → 1.0.39
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/README.md +662 -658
- package/package.json +11 -4
- package/src/config/default-config.js +71 -0
- package/src/config/env-loader.js +4 -2
- package/src/middleware/authorization.js +271 -0
- package/src/middleware/cloudtrail-logger.js +333 -0
- package/src/middleware/error-handler.js +138 -0
- package/src/middleware/index.js +79 -0
- package/src/middleware/policy-enforcement.js +407 -0
- package/src/server.js +74 -4
- package/src/services/acm/index.js +66 -0
- package/src/services/acm/models/certificate.js +202 -0
- package/src/services/acm/server.js +224 -0
- package/src/services/acm/simulator.js +293 -0
- package/src/services/alb/index.js +104 -0
- package/src/services/alb/models/loadBalancer.js +88 -0
- package/src/services/alb/models/targetGroup.js +128 -0
- package/src/services/alb/server.js +160 -0
- package/src/services/alb/simulator.js +262 -0
- package/src/services/apigateway/index.js +22 -0
- package/src/services/apigateway/server.js +247 -35
- package/src/services/apigateway/simulator.js +780 -26
- package/src/services/asg/index.js +100 -0
- package/src/services/asg/models/autoScalingGroup.js +110 -0
- package/src/services/asg/models/launchTemplate.js +41 -0
- package/src/services/asg/models/scalingPolicy.js +51 -0
- package/src/services/asg/server.js +236 -0
- package/src/services/asg/simulator.js +410 -0
- package/src/services/aurora/cluster-manager.js +731 -0
- package/src/services/aurora/data-api.js +514 -0
- package/src/services/aurora/db-engine.js +270 -0
- package/src/services/aurora/index.js +159 -0
- package/src/services/aurora/rds-api.js +189 -0
- package/src/services/aurora/server.js +213 -0
- package/src/services/aurora/sql-translator.js +334 -0
- package/src/services/cloudformation/index.js +2 -2
- package/src/services/cloudformation/server.js +1 -0
- package/src/services/cloudfront/cache-engine.js +225 -0
- package/src/services/cloudfront/geo-restrictions.js +377 -0
- package/src/services/cloudfront/index.js +220 -0
- package/src/services/cloudfront/models/behavior.js +158 -0
- package/src/services/cloudfront/models/distribution.js +284 -0
- package/src/services/cloudfront/models/index.js +99 -0
- package/src/services/cloudfront/models/invalidation.js +109 -0
- package/src/services/cloudfront/models/origin.js +155 -0
- package/src/services/cloudfront/origin-router.js +552 -0
- package/src/services/cloudfront/origin-validator.js +398 -0
- package/src/services/cloudfront/server.js +512 -0
- package/src/services/cloudfront/signed-url-generator.js +377 -0
- package/src/services/cloudfront/simulator.js +425 -0
- package/src/services/cloudfront/store.js +326 -0
- package/src/services/cloudshell/env-builder.js +27 -0
- package/src/services/cloudshell/index.js +48 -0
- package/src/services/cloudshell/routes.js +60 -0
- package/src/services/cloudshell/session-manager.js +226 -0
- package/src/services/cloudshell/static/.gitkeep +0 -0
- package/src/services/cloudshell/static/index.html +406 -0
- package/src/services/cloudshell/websocket-handler.js +107 -0
- package/src/services/cloudtrail/index.js +2 -3
- package/src/services/cloudtrail/server.js +68 -0
- package/src/services/cognito/server.js +4 -1
- package/src/services/dynamodb/server.js +4 -1
- package/src/services/dynamodb/simulator.js +2 -1
- package/src/services/ec2/index.js +101 -0
- package/src/services/ec2/instanceServer.js +311 -0
- package/src/services/ec2/models/ami.js +39 -0
- package/src/services/ec2/models/instance.js +98 -0
- package/src/services/ec2/models/networkInterface.js +74 -0
- package/src/services/ec2/models/securityGroup.js +71 -0
- package/src/services/ec2/server.js +350 -0
- package/src/services/ec2/simulator.js +436 -0
- package/src/services/eventbridge/server.js +36 -0
- package/src/services/eventbridge/simulator.js +230 -45
- package/src/services/iam/index.js +65 -0
- package/src/services/iam/middleware.js +99 -0
- package/src/services/iam/server.js +331 -0
- package/src/services/iam/simulator.js +1432 -0
- package/src/services/index.js +28 -0
- package/src/services/kinesis-analytics/index.js +105 -0
- package/src/services/kinesis-analytics/models/application.js +325 -0
- package/src/services/kinesis-analytics/models/index.js +12 -0
- package/src/services/kinesis-analytics/server.js +250 -0
- package/src/services/kinesis-analytics/simulator.js +407 -0
- package/src/services/kinesis-analytics/store.js +176 -0
- package/src/services/kinesis-data-streams/index.js +106 -0
- package/src/services/kinesis-data-streams/models/index.js +12 -0
- package/src/services/kinesis-data-streams/models/record.js +70 -0
- package/src/services/kinesis-data-streams/models/stream.js +280 -0
- package/src/services/kinesis-data-streams/server.js +255 -0
- package/src/services/kinesis-data-streams/simulator.js +514 -0
- package/src/services/kinesis-data-streams/store.js +163 -0
- package/src/services/kinesis-firehose/index.js +106 -0
- package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
- package/src/services/kinesis-firehose/models/index.js +10 -0
- package/src/services/kinesis-firehose/server.js +215 -0
- package/src/services/kinesis-firehose/simulator.js +388 -0
- package/src/services/kinesis-firehose/store.js +163 -0
- package/src/services/kms/server.js +4 -1
- package/src/services/lambda/handler-loader.js +50 -5
- package/src/services/lambda/index.js +26 -0
- package/src/services/lambda/server.js +18 -12
- package/src/services/lambda/simulator.js +73 -17
- package/src/services/opensearch/index.js +105 -0
- package/src/services/opensearch/models/document.js +115 -0
- package/src/services/opensearch/models/index.js +73 -0
- package/src/services/opensearch/server.js +310 -0
- package/src/services/opensearch/simulator.js +605 -0
- package/src/services/opensearch/store.js +162 -0
- package/src/services/organizations/control-tower-simulator.js +321 -0
- package/src/services/organizations/control-tower-store.js +182 -0
- package/src/services/organizations/iam-integration.js +413 -0
- package/src/services/organizations/index.js +172 -0
- package/src/services/organizations/models/index.js +13 -0
- package/src/services/organizations/models/invitation.js +48 -0
- package/src/services/organizations/models/organization.js +566 -0
- package/src/services/organizations/models/role.js +244 -0
- package/src/services/organizations/models/scp.js +131 -0
- package/src/services/organizations/server.js +736 -0
- package/src/services/organizations/simulator.js +593 -0
- package/src/services/organizations/store.js +235 -0
- package/src/services/organizations/vpc-integration.js +453 -0
- package/src/services/route53/index.js +67 -0
- package/src/services/route53/models/healthCheck.js +101 -0
- package/src/services/route53/models/hostedZone.js +37 -0
- package/src/services/route53/models/resourceRecord.js +106 -0
- package/src/services/route53/server.js +841 -0
- package/src/services/route53/simulator.js +350 -0
- package/src/services/s3/index.js +62 -0
- package/src/services/s3/presigned-url-generator.js +337 -0
- package/src/services/s3/server.js +231 -0
- package/src/services/s3/simulator.js +551 -4
- package/src/services/sns/index.js +31 -0
- package/src/services/sns/server.js +9 -0
- package/src/services/sqs/index.js +38 -0
- package/src/services/sqs/server.js +57 -4
- package/src/services/sqs/simulator.js +423 -33
- package/src/services/sts/simulator.js +209 -17
- package/src/services/vpc/README.md +459 -0
- package/src/services/vpc/ec2ResponseFormatter.js +431 -0
- package/src/services/vpc/index.js +90 -0
- package/src/services/vpc/models/internetGateway.js +71 -0
- package/src/services/vpc/models/routeTable.js +160 -0
- package/src/services/vpc/models/subnet.js +48 -0
- package/src/services/vpc/models/vpc.js +37 -0
- package/src/services/vpc/models/vpcEndpoint.js +100 -0
- package/src/services/vpc/models/vpcGateway.js +134 -0
- package/src/services/vpc/server.js +836 -0
- package/src/services/vpc/simulator.js +613 -0
- package/src/utils/policy-cache.js +215 -0
- package/aws-config +0 -154
|
@@ -27,9 +27,23 @@ class APIGatewaySimulator {
|
|
|
27
27
|
this.usagePlans = new Map();
|
|
28
28
|
this.apiKeys = new Map();
|
|
29
29
|
this.domainNames = new Map();
|
|
30
|
+
this.requestValidators = new Map();
|
|
30
31
|
this.audit = new CloudTrailAudit('execute-api.amazonaws.com');
|
|
32
|
+
this.lambdaService = null;
|
|
33
|
+
this.sqsService = null;
|
|
34
|
+
this.snsService = null;
|
|
35
|
+
this.eventbridgeService = null;
|
|
36
|
+
this.region = 'local';
|
|
37
|
+
this.accountId = '000000000000';
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
setLambdaService(service) { this.lambdaService = service; }
|
|
41
|
+
setSqsService(service) { this.sqsService = service; }
|
|
42
|
+
setSnsService(service) { this.snsService = service; }
|
|
43
|
+
setEventbridgeService(service) { this.eventbridgeService = service; }
|
|
44
|
+
setRegion(region) { this.region = region || this.region; }
|
|
45
|
+
setAccountId(accountId) { this.accountId = accountId || this.accountId; }
|
|
46
|
+
|
|
33
47
|
async initialize() {
|
|
34
48
|
logger.debug('Inicializando API Gateway Simulator...');
|
|
35
49
|
this.loadAPIs();
|
|
@@ -66,12 +80,52 @@ class APIGatewaySimulator {
|
|
|
66
80
|
stages: new Map(),
|
|
67
81
|
deployments: new Map(),
|
|
68
82
|
models: new Map(),
|
|
69
|
-
authorizers: new Map()
|
|
83
|
+
authorizers: new Map(),
|
|
84
|
+
requestValidators: new Map()
|
|
70
85
|
};
|
|
71
86
|
|
|
72
|
-
// Adiciona recursos a partir dos endpoints configurados
|
|
73
87
|
(apiConfig.endpoints || []).forEach((ep, epIndex) => {
|
|
74
88
|
const resId = `res_${apiId}_${epIndex}`;
|
|
89
|
+
const intType = (ep.integrationType || '').toLowerCase();
|
|
90
|
+
let integration;
|
|
91
|
+
|
|
92
|
+
if (intType === 'sqs' || ep.queueArn || ep.queueUrl) {
|
|
93
|
+
integration = {
|
|
94
|
+
type: 'AWS',
|
|
95
|
+
awsService: 'sqs',
|
|
96
|
+
action: 'SendMessage',
|
|
97
|
+
uri: ep.queueArn || ep.queueUrl || `arn:aws:apigateway:local:sqs:action/SendMessage`,
|
|
98
|
+
queueArn: ep.queueArn,
|
|
99
|
+
queueUrl: ep.queueUrl,
|
|
100
|
+
integrationHttpMethod: 'POST'
|
|
101
|
+
};
|
|
102
|
+
} else if (intType === 'sns' || ep.topicArn) {
|
|
103
|
+
integration = {
|
|
104
|
+
type: 'AWS',
|
|
105
|
+
awsService: 'sns',
|
|
106
|
+
action: 'Publish',
|
|
107
|
+
uri: ep.topicArn || `arn:aws:apigateway:local:sns:action/Publish`,
|
|
108
|
+
topicArn: ep.topicArn,
|
|
109
|
+
integrationHttpMethod: 'POST'
|
|
110
|
+
};
|
|
111
|
+
} else if (intType === 'eventbridge' || intType === 'events' || ep.eventBusName) {
|
|
112
|
+
integration = {
|
|
113
|
+
type: 'AWS',
|
|
114
|
+
awsService: 'events',
|
|
115
|
+
action: 'PutEvents',
|
|
116
|
+
uri: `arn:aws:apigateway:local:events:action/PutEvents`,
|
|
117
|
+
eventBusName: ep.eventBusName,
|
|
118
|
+
integrationHttpMethod: 'POST'
|
|
119
|
+
};
|
|
120
|
+
} else {
|
|
121
|
+
integration = {
|
|
122
|
+
type: 'AWS_PROXY',
|
|
123
|
+
awsService: 'lambda',
|
|
124
|
+
uri: ep.lambdaName || '',
|
|
125
|
+
integrationHttpMethod: 'POST'
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
75
129
|
api.resources.set(resId, {
|
|
76
130
|
id: resId,
|
|
77
131
|
path: ep.path,
|
|
@@ -80,11 +134,7 @@ class APIGatewaySimulator {
|
|
|
80
134
|
httpMethod: ep.method,
|
|
81
135
|
authorizationType: ep.authorizerRequired ? 'COGNITO_USER_POOLS' : 'NONE',
|
|
82
136
|
apiKeyRequired: false,
|
|
83
|
-
integration
|
|
84
|
-
type: ep.integrationType === 'lambda' ? 'AWS_PROXY' : 'HTTP',
|
|
85
|
-
uri: ep.lambdaName,
|
|
86
|
-
integrationHttpMethod: 'POST'
|
|
87
|
-
}
|
|
137
|
+
integration
|
|
88
138
|
}]])
|
|
89
139
|
});
|
|
90
140
|
});
|
|
@@ -124,7 +174,8 @@ class APIGatewaySimulator {
|
|
|
124
174
|
models: new Map(),
|
|
125
175
|
authorizers: new Map(),
|
|
126
176
|
gatewayResponses: new Map(),
|
|
127
|
-
documentationParts: new Map()
|
|
177
|
+
documentationParts: new Map(),
|
|
178
|
+
requestValidators: new Map()
|
|
128
179
|
};
|
|
129
180
|
|
|
130
181
|
// Cria recurso raiz
|
|
@@ -136,7 +187,9 @@ class APIGatewaySimulator {
|
|
|
136
187
|
resourceMethods: new Map()
|
|
137
188
|
};
|
|
138
189
|
|
|
139
|
-
api.resources.set(
|
|
190
|
+
api.resources.set(rootResource.id, rootResource);
|
|
191
|
+
rootResource.apiId = apiId;
|
|
192
|
+
api.rootResourceId = rootResource.id;
|
|
140
193
|
|
|
141
194
|
this.apis.set(apiId, api);
|
|
142
195
|
this.persistAPIs();
|
|
@@ -224,13 +277,12 @@ class APIGatewaySimulator {
|
|
|
224
277
|
// ============ Simplified Dashboard Endpoint Operations ============
|
|
225
278
|
|
|
226
279
|
putEndpoint(params) {
|
|
227
|
-
const { restApiId, path, method, integrationType, lambdaName, authorizerRequired } = params;
|
|
280
|
+
const { restApiId, path, method, integrationType, lambdaName, authorizerRequired, queueArn, queueUrl, topicArn, eventBusName } = params;
|
|
228
281
|
const api = this.apis.get(restApiId);
|
|
229
282
|
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
230
283
|
|
|
231
284
|
if (api.isStatic) api.isStatic = false;
|
|
232
285
|
|
|
233
|
-
// Ensure resource exists
|
|
234
286
|
let resource = Array.from(api.resources.values()).find(r => r.path === path);
|
|
235
287
|
if (!resource) {
|
|
236
288
|
const resourceId = `res_${Date.now()}`;
|
|
@@ -238,22 +290,57 @@ class APIGatewaySimulator {
|
|
|
238
290
|
id: resourceId,
|
|
239
291
|
path: path,
|
|
240
292
|
pathPart: path.split('/').pop() || '/',
|
|
241
|
-
parentId: null,
|
|
293
|
+
parentId: null,
|
|
242
294
|
resourceMethods: new Map()
|
|
243
295
|
};
|
|
244
296
|
api.resources.set(resourceId, resource);
|
|
245
297
|
}
|
|
246
298
|
|
|
247
|
-
|
|
299
|
+
const intType = (integrationType || '').toLowerCase();
|
|
300
|
+
let integration;
|
|
301
|
+
|
|
302
|
+
if (intType === 'sqs' || queueArn || queueUrl) {
|
|
303
|
+
integration = {
|
|
304
|
+
type: 'AWS',
|
|
305
|
+
awsService: 'sqs',
|
|
306
|
+
action: 'SendMessage',
|
|
307
|
+
uri: queueArn || queueUrl || `arn:aws:apigateway:local:sqs:action/SendMessage`,
|
|
308
|
+
queueArn,
|
|
309
|
+
queueUrl,
|
|
310
|
+
integrationHttpMethod: 'POST'
|
|
311
|
+
};
|
|
312
|
+
} else if (intType === 'sns' || topicArn) {
|
|
313
|
+
integration = {
|
|
314
|
+
type: 'AWS',
|
|
315
|
+
awsService: 'sns',
|
|
316
|
+
action: 'Publish',
|
|
317
|
+
uri: topicArn || `arn:aws:apigateway:local:sns:action/Publish`,
|
|
318
|
+
topicArn,
|
|
319
|
+
integrationHttpMethod: 'POST'
|
|
320
|
+
};
|
|
321
|
+
} else if (intType === 'eventbridge' || intType === 'events' || eventBusName) {
|
|
322
|
+
integration = {
|
|
323
|
+
type: 'AWS',
|
|
324
|
+
awsService: 'events',
|
|
325
|
+
action: 'PutEvents',
|
|
326
|
+
uri: `arn:aws:apigateway:local:events:action/PutEvents`,
|
|
327
|
+
eventBusName,
|
|
328
|
+
integrationHttpMethod: 'POST'
|
|
329
|
+
};
|
|
330
|
+
} else {
|
|
331
|
+
integration = {
|
|
332
|
+
type: 'AWS_PROXY',
|
|
333
|
+
awsService: 'lambda',
|
|
334
|
+
uri: lambdaName || '',
|
|
335
|
+
integrationHttpMethod: 'POST'
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
248
339
|
resource.resourceMethods.set(method.toUpperCase(), {
|
|
249
340
|
httpMethod: method.toUpperCase(),
|
|
250
341
|
authorizationType: authorizerRequired ? 'COGNITO_USER_POOLS' : 'NONE',
|
|
251
342
|
apiKeyRequired: false,
|
|
252
|
-
integration
|
|
253
|
-
type: integrationType === 'lambda' ? 'AWS_PROXY' : 'HTTP',
|
|
254
|
-
uri: lambdaName,
|
|
255
|
-
integrationHttpMethod: 'POST'
|
|
256
|
-
}
|
|
343
|
+
integration
|
|
257
344
|
});
|
|
258
345
|
|
|
259
346
|
this.persistAPIs();
|
|
@@ -369,7 +456,7 @@ class APIGatewaySimulator {
|
|
|
369
456
|
// ============ Method Operations ============
|
|
370
457
|
|
|
371
458
|
putMethod(params) {
|
|
372
|
-
const { restApiId, resourceId, httpMethod, authorizationType, apiKeyRequired, requestParameters, requestModels, authorizerId } = params;
|
|
459
|
+
const { restApiId, resourceId, httpMethod, authorizationType, apiKeyRequired, requestParameters, requestModels, authorizerId, requestValidatorId } = params;
|
|
373
460
|
const api = this.apis.get(restApiId);
|
|
374
461
|
|
|
375
462
|
if (!api) {
|
|
@@ -388,6 +475,7 @@ class APIGatewaySimulator {
|
|
|
388
475
|
requestParameters: requestParameters || {},
|
|
389
476
|
requestModels: requestModels || {},
|
|
390
477
|
authorizerId: authorizerId || null,
|
|
478
|
+
requestValidatorId: requestValidatorId || null,
|
|
391
479
|
methodResponses: new Map(),
|
|
392
480
|
integration: null
|
|
393
481
|
};
|
|
@@ -846,7 +934,24 @@ class APIGatewaySimulator {
|
|
|
846
934
|
if (usagePlan && !this.checkThrottle(usagePlan)) {
|
|
847
935
|
return this.createResponse(429, 'Too Many Requests');
|
|
848
936
|
}
|
|
849
|
-
|
|
937
|
+
|
|
938
|
+
// Valida request parâmetros e body (Request Validator / Models)
|
|
939
|
+
const pathParams = resource ? this.extractPathParams(resource.path, path) : {};
|
|
940
|
+
const validationError = this.validateRequest({
|
|
941
|
+
api,
|
|
942
|
+
methodDef,
|
|
943
|
+
headers,
|
|
944
|
+
queryString,
|
|
945
|
+
pathParams,
|
|
946
|
+
body
|
|
947
|
+
});
|
|
948
|
+
if (validationError) {
|
|
949
|
+
return this.createResponse(400, {
|
|
950
|
+
__type: 'BadRequestException',
|
|
951
|
+
message: validationError
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
850
955
|
// Processa integração
|
|
851
956
|
const integration = methodDef.integration;
|
|
852
957
|
if (!integration) {
|
|
@@ -860,6 +965,7 @@ class APIGatewaySimulator {
|
|
|
860
965
|
headers,
|
|
861
966
|
body,
|
|
862
967
|
queryString,
|
|
968
|
+
pathParams,
|
|
863
969
|
resource,
|
|
864
970
|
stage
|
|
865
971
|
});
|
|
@@ -974,19 +1080,297 @@ class APIGatewaySimulator {
|
|
|
974
1080
|
}
|
|
975
1081
|
|
|
976
1082
|
async executeAWSIntegration(integration, context, body, headers) {
|
|
977
|
-
|
|
978
|
-
const
|
|
1083
|
+
const uri = integration.uri || '';
|
|
1084
|
+
const action = integration.action;
|
|
1085
|
+
const explicitService = (integration.awsService || '').toLowerCase();
|
|
1086
|
+
|
|
1087
|
+
// Detecta serviço AWS alvo pela URI (formato: arn:aws:apigateway:{region}:{service}:path|action/{action})
|
|
1088
|
+
// Ex.: arn:aws:apigateway:us-east-1:sqs:action/SendMessage
|
|
1089
|
+
// Ex.: arn:aws:apigateway:us-east-1:events:action/PutEvents
|
|
1090
|
+
// Ou URI estilo AWS service path /2012-11-05/queues/{queueName}/sendMessage
|
|
1091
|
+
// Ou por configuração explícita (integrações de dashboard / config estática)
|
|
1092
|
+
let awsService = explicitService || null;
|
|
1093
|
+
let awsAction = action;
|
|
1094
|
+
const arnMatch = uri.match(/arn:aws:apigateway:[^:]*:([^:]+):(?:action|path)\/([^?/]+)/);
|
|
1095
|
+
if (arnMatch) {
|
|
1096
|
+
awsService = awsService || arnMatch[1];
|
|
1097
|
+
awsAction = awsAction || arnMatch[2];
|
|
1098
|
+
} else if (integration.queueArn || integration.queueUrl || (/sqs/i.test(uri) || /SendMessage/i.test(uri))) {
|
|
1099
|
+
awsService = awsService || 'sqs';
|
|
1100
|
+
awsAction = awsAction || 'SendMessage';
|
|
1101
|
+
} else if (integration.eventBusName || (/events|eventbridge/i.test(uri) || /PutEvents/i.test(uri))) {
|
|
1102
|
+
awsService = awsService || 'events';
|
|
1103
|
+
awsAction = awsAction || 'PutEvents';
|
|
1104
|
+
} else if (integration.topicArn || (/sns/i.test(uri) || /Publish/i.test(uri))) {
|
|
1105
|
+
awsService = awsService || 'sns';
|
|
1106
|
+
awsAction = awsAction || 'Publish';
|
|
1107
|
+
} else if (/lambda/i.test(uri) || integration.type === 'AWS_PROXY') {
|
|
1108
|
+
awsService = awsService || 'lambda';
|
|
1109
|
+
}
|
|
1110
|
+
awsService = awsService || 'lambda';
|
|
1111
|
+
awsAction = awsAction || '';
|
|
1112
|
+
|
|
1113
|
+
const ctx = context || {};
|
|
1114
|
+
const parsedBody = (typeof body === 'string' && body.length > 0)
|
|
1115
|
+
? (() => { try { return JSON.parse(body); } catch { return null; } })()
|
|
1116
|
+
: body || ctx.body || null;
|
|
1117
|
+
|
|
1118
|
+
if (awsService === 'sqs') {
|
|
1119
|
+
return this._executeSQSIntegration(integration, awsAction, parsedBody || {}, ctx);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (awsService === 'events') {
|
|
1123
|
+
return this._executeEventBridgeIntegration(integration, awsAction, parsedBody || {}, ctx);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (awsService === 'sns') {
|
|
1127
|
+
return this._executeSNSIntegration(integration, awsAction, parsedBody || {}, ctx);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Lambda (padrão)
|
|
1131
|
+
const uriParts = uri.split(':');
|
|
979
1132
|
const functionName = uriParts[uriParts.length - 1];
|
|
980
|
-
|
|
981
|
-
// Aqui poderia chamar o simulador Lambda
|
|
982
1133
|
logger.debug(`Invoking Lambda: ${functionName}`);
|
|
983
|
-
|
|
1134
|
+
|
|
1135
|
+
if (this.lambdaService?.simulator) {
|
|
1136
|
+
const lambdaEvent = {
|
|
1137
|
+
...(ctx.headers ? { headers: ctx.headers } : {}),
|
|
1138
|
+
...(ctx.queryString ? { queryStringParameters: ctx.queryString } : {}),
|
|
1139
|
+
...(ctx.pathParams ? { pathParameters: ctx.pathParams } : {}),
|
|
1140
|
+
...(parsedBody ? { body: JSON.stringify(parsedBody) } : { body: body || null }),
|
|
1141
|
+
httpMethod: ctx.method || 'POST',
|
|
1142
|
+
path: ctx.path || '/',
|
|
1143
|
+
isBase64Encoded: false,
|
|
1144
|
+
requestContext: { stage: ctx.stage?.stageName || 'local' }
|
|
1145
|
+
};
|
|
1146
|
+
const result = await this.lambdaService.simulator.invoke(functionName, lambdaEvent);
|
|
1147
|
+
const payload = result.Payload || {};
|
|
1148
|
+
return this.createResponse(
|
|
1149
|
+
payload.statusCode || 200,
|
|
1150
|
+
typeof payload.body === 'string'
|
|
1151
|
+
? (() => { try { return JSON.parse(payload.body); } catch { return payload.body; } })()
|
|
1152
|
+
: payload
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
984
1156
|
return this.createResponse(200, {
|
|
985
1157
|
message: `Lambda ${functionName} invoked`,
|
|
986
1158
|
event: context
|
|
987
1159
|
});
|
|
988
1160
|
}
|
|
989
1161
|
|
|
1162
|
+
_resolveSQSQueueArnOrName(queueNameOrUrlOrArn) {
|
|
1163
|
+
if (!queueNameOrUrlOrArn) return null;
|
|
1164
|
+
const s = String(queueNameOrUrlOrArn);
|
|
1165
|
+
const arnMatch = s.match(/arn:aws:sqs:[^:]+:[^:]+:([^:]+)$/);
|
|
1166
|
+
if (arnMatch) return { name: arnMatch[1], arn: s };
|
|
1167
|
+
const urlMatch = s.match(/\/queue\/([^/?]+)/);
|
|
1168
|
+
if (urlMatch) return { name: urlMatch[1], arn: null };
|
|
1169
|
+
return { name: s, arn: null };
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
async _executeSQSIntegration(integration, action, payload, ctx) {
|
|
1173
|
+
if (!this.sqsService?.simulator) {
|
|
1174
|
+
return this.createResponse(503, { __type: 'ServiceUnavailable', message: 'SQS service not available' });
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const sqsSim = this.sqsService.simulator;
|
|
1178
|
+
const actionLower = (action || '').toLowerCase();
|
|
1179
|
+
|
|
1180
|
+
if (actionLower === 'sendmessage' || actionLower === '') {
|
|
1181
|
+
let queueUrl = payload.QueueUrl;
|
|
1182
|
+
let messageBody = payload.MessageBody;
|
|
1183
|
+
let delaySeconds = payload.DelaySeconds;
|
|
1184
|
+
let messageAttributes = payload.MessageAttributes;
|
|
1185
|
+
|
|
1186
|
+
if (!queueUrl && integration.queueUrl) {
|
|
1187
|
+
queueUrl = integration.queueUrl;
|
|
1188
|
+
}
|
|
1189
|
+
if (!queueUrl && integration.queueArn) {
|
|
1190
|
+
const qInfo = this._resolveSQSQueueArnOrName(integration.queueArn);
|
|
1191
|
+
if (qInfo?.name) {
|
|
1192
|
+
const q = sqsSim.queues.get(qInfo.name);
|
|
1193
|
+
queueUrl = q && q.url;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (!queueUrl && ctx.pathParams) {
|
|
1197
|
+
const qName = ctx.pathParams.queueName || ctx.pathParams.name;
|
|
1198
|
+
if (qName) {
|
|
1199
|
+
const queue = sqsSim.queues.get(qName);
|
|
1200
|
+
queueUrl = queue && queue.url;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (!queueUrl && integration.requestParameters) {
|
|
1204
|
+
for (const [k, v] of Object.entries(integration.requestParameters)) {
|
|
1205
|
+
if (k.toLowerCase() === 'integration.request.querystring.queueurl' || k.toLowerCase() === 'integration.request.path.queueurl') {
|
|
1206
|
+
const val = this.resolveParameter(v, { ...ctx, headers: ctx.headers || {}, queryString: ctx.queryString || {} });
|
|
1207
|
+
if (val) queueUrl = val;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (!messageBody) {
|
|
1212
|
+
if (ctx.body && typeof ctx.body === 'string' && ctx.body.length > 0) {
|
|
1213
|
+
messageBody = ctx.body;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (!messageBody) {
|
|
1217
|
+
if (payload && Object.keys(payload).length > 0 && payload.MessageBody === undefined) {
|
|
1218
|
+
messageBody = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const queueInfo = this._resolveSQSQueueArnOrName(queueUrl || ctx.queryString?.queueUrl || '');
|
|
1223
|
+
if (!queueUrl && queueInfo?.name) {
|
|
1224
|
+
const q = sqsSim.queues.get(queueInfo.name);
|
|
1225
|
+
queueUrl = q && q.url;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (!queueUrl) {
|
|
1229
|
+
return this.createResponse(400, { __type: 'MissingParameter', message: 'QueueUrl is required' });
|
|
1230
|
+
}
|
|
1231
|
+
if (!messageBody) {
|
|
1232
|
+
return this.createResponse(400, { __type: 'MissingParameter', message: 'MessageBody is required' });
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
try {
|
|
1236
|
+
const result = sqsSim.sendMessage({ QueueUrl: queueUrl, MessageBody: messageBody, DelaySeconds: delaySeconds, MessageAttributes: messageAttributes });
|
|
1237
|
+
if (result.error) {
|
|
1238
|
+
return this.createResponse(result.status || 400, { __type: result.error.code || 'InternalError', message: result.error.message });
|
|
1239
|
+
}
|
|
1240
|
+
return this.createResponse(200, {
|
|
1241
|
+
SendMessageResponse: {
|
|
1242
|
+
SendMessageResult: {
|
|
1243
|
+
MessageId: result.messageId,
|
|
1244
|
+
MD5OfMessageBody: result.md5
|
|
1245
|
+
},
|
|
1246
|
+
ResponseMetadata: { RequestId: crypto.randomUUID() }
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
logger.error('APIGateway SQS integration error:', err);
|
|
1251
|
+
return this.createResponse(502, { __type: 'InternalFailure', message: err.message });
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (actionLower === 'sendmessagebatch') {
|
|
1256
|
+
return this.createResponse(501, { __type: 'NotImplemented', message: 'SendMessageBatch not implemented' });
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
return this.createResponse(400, { __type: 'InvalidAction', message: `Action ${action} not supported for SQS` });
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
async _executeEventBridgeIntegration(integration, action, payload, ctx) {
|
|
1263
|
+
if (!this.eventbridgeService?.simulator) {
|
|
1264
|
+
return this.createResponse(503, { __type: 'ServiceUnavailable', message: 'EventBridge service not available' });
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const ebSim = this.eventbridgeService.simulator;
|
|
1268
|
+
const actionLower = (action || '').toLowerCase();
|
|
1269
|
+
const defaultEventBusName = integration.eventBusName;
|
|
1270
|
+
|
|
1271
|
+
if (actionLower === 'putevents' || actionLower === '') {
|
|
1272
|
+
let entries = payload.Entries;
|
|
1273
|
+
if (!entries && payload && Object.keys(payload).length > 0) {
|
|
1274
|
+
entries = [{
|
|
1275
|
+
Source: payload.Source || payload.source || 'aws.apigateway',
|
|
1276
|
+
DetailType: payload.DetailType || payload.detailType || 'API Gateway Request',
|
|
1277
|
+
Detail: JSON.stringify(payload.Detail || payload.detail || payload),
|
|
1278
|
+
EventBusName: payload.EventBusName || payload.eventBusName || defaultEventBusName || 'default',
|
|
1279
|
+
Time: payload.Time || new Date().toISOString(),
|
|
1280
|
+
Resources: payload.Resources || []
|
|
1281
|
+
}];
|
|
1282
|
+
} else if (entries && defaultEventBusName) {
|
|
1283
|
+
entries = entries.map(e => ({
|
|
1284
|
+
...e,
|
|
1285
|
+
EventBusName: e.EventBusName || defaultEventBusName
|
|
1286
|
+
}));
|
|
1287
|
+
}
|
|
1288
|
+
if (!entries || !Array.isArray(entries) || entries.length === 0) {
|
|
1289
|
+
return this.createResponse(400, { __type: 'ValidationException', message: 'Entries list is required and must not be empty' });
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const normalizedEntries = entries.map(e => ({
|
|
1293
|
+
Time: e.Time || new Date().toISOString(),
|
|
1294
|
+
Source: e.Source || 'aws.apigateway',
|
|
1295
|
+
Resources: e.Resources || [],
|
|
1296
|
+
DetailType: e.DetailType || 'API Gateway Request',
|
|
1297
|
+
Detail: typeof e.Detail === 'string' ? e.Detail : JSON.stringify(e.Detail || {}),
|
|
1298
|
+
EventBusName: e.EventBusName || defaultEventBusName || 'default',
|
|
1299
|
+
TraceHeader: e.TraceHeader
|
|
1300
|
+
}));
|
|
1301
|
+
|
|
1302
|
+
try {
|
|
1303
|
+
const result = await ebSim.putEvents({ Entries: normalizedEntries });
|
|
1304
|
+
return this.createResponse(200, {
|
|
1305
|
+
Entries: (result.Entries || []).map(e => ({
|
|
1306
|
+
EventId: e.EventId || crypto.randomUUID(),
|
|
1307
|
+
ErrorCode: e.ErrorCode,
|
|
1308
|
+
ErrorMessage: e.ErrorMessage
|
|
1309
|
+
})),
|
|
1310
|
+
FailedEntryCount: result.FailedEntryCount || 0
|
|
1311
|
+
});
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
logger.error('APIGateway EventBridge integration error:', err);
|
|
1314
|
+
return this.createResponse(err.name && err.name !== 'Error' ? 400 : 502, {
|
|
1315
|
+
__type: err.__type || err.name || 'InternalFailure',
|
|
1316
|
+
message: err.message
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
return this.createResponse(400, { __type: 'InvalidAction', message: `Action ${action} not supported for EventBridge` });
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async _executeSNSIntegration(integration, action, payload, ctx) {
|
|
1325
|
+
if (!this.snsService?.simulator) {
|
|
1326
|
+
return this.createResponse(503, { __type: 'ServiceUnavailable', message: 'SNS service not available' });
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const snsSim = this.snsService.simulator;
|
|
1330
|
+
const actionLower = (action || '').toLowerCase();
|
|
1331
|
+
|
|
1332
|
+
if (actionLower === 'publish' || actionLower === '') {
|
|
1333
|
+
let topicArn = payload.TopicArn || payload.TargetArn || ctx.queryString?.TopicArn;
|
|
1334
|
+
if (!topicArn && integration.topicArn) {
|
|
1335
|
+
topicArn = integration.topicArn;
|
|
1336
|
+
}
|
|
1337
|
+
let message = payload.Message;
|
|
1338
|
+
if (!message && ctx.body && typeof ctx.body === 'string' && ctx.body.length > 0) {
|
|
1339
|
+
message = ctx.body;
|
|
1340
|
+
}
|
|
1341
|
+
if (!message && payload && Object.keys(payload).length > 0 && payload.Message === undefined) {
|
|
1342
|
+
message = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
1343
|
+
}
|
|
1344
|
+
const subject = payload.Subject;
|
|
1345
|
+
if (!message) return this.createResponse(400, { __type: 'MissingParameter', message: 'Message is required' });
|
|
1346
|
+
|
|
1347
|
+
try {
|
|
1348
|
+
const result = await snsSim.publish({
|
|
1349
|
+
TopicArn: topicArn,
|
|
1350
|
+
TargetArn: payload.TargetArn,
|
|
1351
|
+
Message: typeof message === 'string' ? message : JSON.stringify(message),
|
|
1352
|
+
Subject: subject,
|
|
1353
|
+
MessageAttributes: payload.MessageAttributes
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
return this.createResponse(200, {
|
|
1357
|
+
PublishResponse: {
|
|
1358
|
+
PublishResult: { MessageId: result.messageId || crypto.randomUUID() },
|
|
1359
|
+
ResponseMetadata: { RequestId: crypto.randomUUID() }
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
logger.error('APIGateway SNS integration error:', err);
|
|
1364
|
+
return this.createResponse(err.__type ? 400 : 502, {
|
|
1365
|
+
__type: err.__type || err.name || 'InternalFailure',
|
|
1366
|
+
message: err.message
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
return this.createResponse(400, { __type: 'InvalidAction', message: `Action ${action} not supported for SNS` });
|
|
1372
|
+
}
|
|
1373
|
+
|
|
990
1374
|
async executeHTTPIntegration(integration, context, body, headers) {
|
|
991
1375
|
const { uri, integrationHttpMethod } = integration;
|
|
992
1376
|
|
|
@@ -1200,6 +1584,7 @@ class APIGatewaySimulator {
|
|
|
1200
1584
|
data.deployments = new Map(Object.entries(data.deployments || {}));
|
|
1201
1585
|
data.models = new Map(Object.entries(data.models || {}));
|
|
1202
1586
|
data.authorizers = new Map(Object.entries(data.authorizers || {}));
|
|
1587
|
+
data.requestValidators = new Map(Object.entries(data.requestValidators || {}));
|
|
1203
1588
|
this.apis.set(id, data);
|
|
1204
1589
|
}
|
|
1205
1590
|
}
|
|
@@ -1306,7 +1691,8 @@ class APIGatewaySimulator {
|
|
|
1306
1691
|
stages: Object.fromEntries(api.stages),
|
|
1307
1692
|
deployments: Object.fromEntries(api.deployments),
|
|
1308
1693
|
models: Object.fromEntries(api.models),
|
|
1309
|
-
authorizers: Object.fromEntries(api.authorizers)
|
|
1694
|
+
authorizers: Object.fromEntries(api.authorizers),
|
|
1695
|
+
requestValidators: Object.fromEntries(api.requestValidators)
|
|
1310
1696
|
};
|
|
1311
1697
|
}
|
|
1312
1698
|
this.store.write('__apis__', apisObj);
|
|
@@ -1352,6 +1738,373 @@ class APIGatewaySimulator {
|
|
|
1352
1738
|
this.store.write('__usage_plans__', plansObj);
|
|
1353
1739
|
}
|
|
1354
1740
|
|
|
1741
|
+
// ============ JSON Schema Validator (API Gateway style - JSON Schema draft-04) ============
|
|
1742
|
+
|
|
1743
|
+
_jsonSchemaTypeMatches(value, type) {
|
|
1744
|
+
if (value === null) return type === 'null' || type === undefined;
|
|
1745
|
+
switch (type) {
|
|
1746
|
+
case 'string': return typeof value === 'string';
|
|
1747
|
+
case 'integer': return Number.isInteger(value);
|
|
1748
|
+
case 'number': return typeof value === 'number' && Number.isFinite(value);
|
|
1749
|
+
case 'boolean': return typeof value === 'boolean';
|
|
1750
|
+
case 'array': return Array.isArray(value);
|
|
1751
|
+
case 'object': return typeof value === 'object' && !Array.isArray(value);
|
|
1752
|
+
case 'null': return value === null;
|
|
1753
|
+
default: return true;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
_validateJsonSchema(value, schema, path = '$') {
|
|
1758
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
1759
|
+
|
|
1760
|
+
if (schema.type !== undefined) {
|
|
1761
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
1762
|
+
if (!types.some(t => this._jsonSchemaTypeMatches(value, t))) {
|
|
1763
|
+
return `${path} should be ${types.join(' or ')}`;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (schema.enum !== undefined && Array.isArray(schema.enum)) {
|
|
1768
|
+
const match = schema.enum.some(opt => JSON.stringify(opt) === JSON.stringify(value));
|
|
1769
|
+
if (!match) return `${path} should be equal to one of the allowed values`;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
if (typeof value === 'string') {
|
|
1773
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
1774
|
+
return `${path} should NOT be shorter than ${schema.minLength} characters`;
|
|
1775
|
+
}
|
|
1776
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
1777
|
+
return `${path} should NOT be longer than ${schema.maxLength} characters`;
|
|
1778
|
+
}
|
|
1779
|
+
if (schema.pattern !== undefined) {
|
|
1780
|
+
try {
|
|
1781
|
+
const re = new RegExp(schema.pattern);
|
|
1782
|
+
if (!re.test(value)) return `${path} should match pattern "${schema.pattern}"`;
|
|
1783
|
+
} catch (_e) { /* invalid pattern, skip */ }
|
|
1784
|
+
}
|
|
1785
|
+
if (schema.format !== undefined) {
|
|
1786
|
+
const fmt = schema.format.toLowerCase();
|
|
1787
|
+
if (fmt === 'date' && !/^\d{4}-\d{2}-\d{2}$/.test(value)) return `${path} should be format "date"`;
|
|
1788
|
+
if (fmt === 'date-time') {
|
|
1789
|
+
if (Number.isNaN(Date.parse(value))) return `${path} should be format "date-time"`;
|
|
1790
|
+
}
|
|
1791
|
+
if (fmt === 'email' && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) return `${path} should be format "email"`;
|
|
1792
|
+
if ((fmt === 'uri' || fmt === 'uriref') && !/^[a-zA-Z][a-zA-Z0-9+.\-]*:/.test(value)) {
|
|
1793
|
+
if (fmt === 'uri') return `${path} should be format "uri"`;
|
|
1794
|
+
}
|
|
1795
|
+
if (fmt === 'uuid' && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
|
|
1796
|
+
return `${path} should be format "uuid"`;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (typeof value === 'number') {
|
|
1802
|
+
if (schema.minimum !== undefined) {
|
|
1803
|
+
if (schema.exclusiveMinimum ? value <= schema.minimum : value < schema.minimum) {
|
|
1804
|
+
return `${path} should be ${schema.exclusiveMinimum ? '>' : '>='} ${schema.minimum}`;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
if (schema.maximum !== undefined) {
|
|
1808
|
+
if (schema.exclusiveMaximum ? value >= schema.maximum : value > schema.maximum) {
|
|
1809
|
+
return `${path} should be ${schema.exclusiveMaximum ? '<' : '<='} ${schema.maximum}`;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (schema.multipleOf !== undefined && value % schema.multipleOf !== 0) {
|
|
1813
|
+
return `${path} should be multiple of ${schema.multipleOf}`;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
if (Array.isArray(value)) {
|
|
1818
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
1819
|
+
return `${path} should NOT have fewer than ${schema.minItems} items`;
|
|
1820
|
+
}
|
|
1821
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
1822
|
+
return `${path} should NOT have more than ${schema.maxItems} items`;
|
|
1823
|
+
}
|
|
1824
|
+
if (schema.uniqueItems === true) {
|
|
1825
|
+
const seen = new Set();
|
|
1826
|
+
for (const item of value) {
|
|
1827
|
+
const key = JSON.stringify(item);
|
|
1828
|
+
if (seen.has(key)) return `${path} should NOT have duplicate items`;
|
|
1829
|
+
seen.add(key);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
if (schema.items !== undefined) {
|
|
1833
|
+
if (Array.isArray(schema.items)) {
|
|
1834
|
+
for (let i = 0; i < Math.min(schema.items.length, value.length); i++) {
|
|
1835
|
+
const err = this._validateJsonSchema(value[i], schema.items[i], `${path}[${i}]`);
|
|
1836
|
+
if (err) return err;
|
|
1837
|
+
}
|
|
1838
|
+
} else {
|
|
1839
|
+
for (let i = 0; i < value.length; i++) {
|
|
1840
|
+
const err = this._validateJsonSchema(value[i], schema.items, `${path}[${i}]`);
|
|
1841
|
+
if (err) return err;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1848
|
+
if (schema.required !== undefined && Array.isArray(schema.required)) {
|
|
1849
|
+
for (const reqField of schema.required) {
|
|
1850
|
+
if (!(reqField in value) || value[reqField] === undefined) {
|
|
1851
|
+
return `${path} should have required property '${reqField}'`;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (schema.minProperties !== undefined && Object.keys(value).length < schema.minProperties) {
|
|
1856
|
+
return `${path} should NOT have fewer than ${schema.minProperties} properties`;
|
|
1857
|
+
}
|
|
1858
|
+
if (schema.maxProperties !== undefined && Object.keys(value).length > schema.maxProperties) {
|
|
1859
|
+
return `${path} should NOT have more than ${schema.maxProperties} properties`;
|
|
1860
|
+
}
|
|
1861
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
1862
|
+
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
1863
|
+
if (key in value && value[key] !== undefined) {
|
|
1864
|
+
const err = this._validateJsonSchema(value[key], subSchema, `${path}.${key}`);
|
|
1865
|
+
if (err) return err;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (schema.additionalProperties === false && schema.properties) {
|
|
1870
|
+
const allowed = new Set(Object.keys(schema.properties));
|
|
1871
|
+
for (const k of Object.keys(value)) {
|
|
1872
|
+
if (!allowed.has(k)) return `${path} should NOT have additional properties '${k}'`;
|
|
1873
|
+
}
|
|
1874
|
+
} else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
1875
|
+
if (schema.properties) {
|
|
1876
|
+
const defined = new Set(Object.keys(schema.properties));
|
|
1877
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1878
|
+
if (!defined.has(k)) {
|
|
1879
|
+
const err = this._validateJsonSchema(v, schema.additionalProperties, `${path}.${k}`);
|
|
1880
|
+
if (err) return err;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (schema.patternProperties && typeof schema.patternProperties === 'object') {
|
|
1886
|
+
for (const [pattern, subSchema] of Object.entries(schema.patternProperties)) {
|
|
1887
|
+
try {
|
|
1888
|
+
const re = new RegExp(pattern);
|
|
1889
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1890
|
+
if (re.test(k)) {
|
|
1891
|
+
const err = this._validateJsonSchema(v, subSchema, `${path}.${k}`);
|
|
1892
|
+
if (err) return err;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
} catch (_e) { /* ignore bad pattern */ }
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
1899
|
+
for (let i = 0; i < schema.allOf.length; i++) {
|
|
1900
|
+
const err = this._validateJsonSchema(value, schema.allOf[i], path);
|
|
1901
|
+
if (err) return err;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (schema.oneOf && Array.isArray(schema.oneOf)) {
|
|
1905
|
+
let matchCount = 0;
|
|
1906
|
+
for (const sub of schema.oneOf) if (!this._validateJsonSchema(value, sub, path)) matchCount++;
|
|
1907
|
+
if (matchCount !== 1) return `${path} should match exactly one schema in oneOf`;
|
|
1908
|
+
}
|
|
1909
|
+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
|
1910
|
+
let ok = false;
|
|
1911
|
+
for (const sub of schema.anyOf) if (!this._validateJsonSchema(value, sub, path)) { ok = true; break; }
|
|
1912
|
+
if (!ok) return `${path} should match some schema in anyOf`;
|
|
1913
|
+
}
|
|
1914
|
+
if (schema.not) {
|
|
1915
|
+
if (!this._validateJsonSchema(value, schema.not, path)) return `${path} should NOT be valid against the schema in "not"`;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
return null;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// ============ Request Validation (AWS API Gateway style) ============
|
|
1923
|
+
|
|
1924
|
+
validateRequest({ api, methodDef, headers, queryString, pathParams, body }) {
|
|
1925
|
+
const validator = (methodDef && methodDef.requestValidatorId && api?.requestValidators)
|
|
1926
|
+
? api.requestValidators.get(methodDef.requestValidatorId)
|
|
1927
|
+
: null;
|
|
1928
|
+
|
|
1929
|
+
const validateParams = !!validator?.validateRequestParameters;
|
|
1930
|
+
const validateBody = !!validator?.validateRequestBody;
|
|
1931
|
+
|
|
1932
|
+
if (!validateParams && !validateBody) return null;
|
|
1933
|
+
|
|
1934
|
+
if (validateParams && methodDef.requestParameters) {
|
|
1935
|
+
for (const [paramKey, required] of Object.entries(methodDef.requestParameters)) {
|
|
1936
|
+
if (!required) continue;
|
|
1937
|
+
const key = String(paramKey);
|
|
1938
|
+
let value = null;
|
|
1939
|
+
if (key.startsWith('method.request.querystring.')) {
|
|
1940
|
+
const name = key.slice('method.request.querystring.'.length);
|
|
1941
|
+
value = (queryString && name in queryString) ? queryString[name] : null;
|
|
1942
|
+
} else if (key.startsWith('method.request.path.')) {
|
|
1943
|
+
const name = key.slice('method.request.path.'.length);
|
|
1944
|
+
value = (pathParams && name in pathParams) ? pathParams[name] : null;
|
|
1945
|
+
} else if (key.startsWith('method.request.header.')) {
|
|
1946
|
+
const name = key.slice('method.request.header.'.length);
|
|
1947
|
+
value = null;
|
|
1948
|
+
if (headers) {
|
|
1949
|
+
value = headers[name] ?? headers[name.toLowerCase()] ?? null;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
if (value === null || value === undefined || value === '') {
|
|
1953
|
+
return `Missing required request parameters: [${key}]`;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
if (validateBody && methodDef.requestModels) {
|
|
1959
|
+
const contentType = (headers && (headers['Content-Type'] || headers['content-type'])) || 'application/json';
|
|
1960
|
+
const modelName = methodDef.requestModels[contentType] || methodDef.requestModels['application/json'];
|
|
1961
|
+
if (!modelName) return null;
|
|
1962
|
+
|
|
1963
|
+
const model = (api?.models && api.models.get(modelName)) || this.models.get(modelName);
|
|
1964
|
+
if (!model) {
|
|
1965
|
+
logger.warn(`APIGateway: Model "${modelName}" referenced but not found for validation`);
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
const schema = typeof model.schema === 'string'
|
|
1970
|
+
? (() => { try { return JSON.parse(model.schema); } catch (_e) { return null; } })()
|
|
1971
|
+
: model.schema;
|
|
1972
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
1973
|
+
|
|
1974
|
+
let parsed = body;
|
|
1975
|
+
if (typeof parsed === 'string') {
|
|
1976
|
+
try { parsed = JSON.parse(parsed); }
|
|
1977
|
+
catch (_e) { return 'Invalid request body: expected valid JSON'; }
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
const err = this._validateJsonSchema(parsed, schema, '$');
|
|
1981
|
+
if (err) return `Invalid request body: ${err}`;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// ============ Model Operations ============
|
|
1988
|
+
|
|
1989
|
+
createModel(params) {
|
|
1990
|
+
const { restApiId, name, description, schema, contentType } = params;
|
|
1991
|
+
const api = this.apis.get(restApiId);
|
|
1992
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
1993
|
+
if (!name) throw new Error('Model name is required');
|
|
1994
|
+
|
|
1995
|
+
const schemaStr = typeof schema === 'string' ? schema : JSON.stringify(schema || {});
|
|
1996
|
+
const model = {
|
|
1997
|
+
name,
|
|
1998
|
+
description: description || '',
|
|
1999
|
+
schema: schemaStr,
|
|
2000
|
+
contentType: contentType || 'application/json',
|
|
2001
|
+
createdDate: new Date().toISOString()
|
|
2002
|
+
};
|
|
2003
|
+
|
|
2004
|
+
api.models.set(name, model);
|
|
2005
|
+
this.persistAPIs();
|
|
2006
|
+
return { id: name, ...model };
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
getModel(params) {
|
|
2010
|
+
const { restApiId, modelName, flatten } = params;
|
|
2011
|
+
const api = this.apis.get(restApiId);
|
|
2012
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
2013
|
+
const model = api.models.get(modelName);
|
|
2014
|
+
if (!model) throw new Error(`Model ${modelName} not found`);
|
|
2015
|
+
|
|
2016
|
+
if (flatten === 'true' || flatten === true) {
|
|
2017
|
+
const parsed = typeof model.schema === 'string'
|
|
2018
|
+
? (() => { try { return JSON.parse(model.schema); } catch (_e) { return {}; } })()
|
|
2019
|
+
: model.schema;
|
|
2020
|
+
return { name: model.name, description: model.description, contentType: model.contentType, schema: parsed };
|
|
2021
|
+
}
|
|
2022
|
+
return { ...model };
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
getModels(params) {
|
|
2026
|
+
const { restApiId } = params;
|
|
2027
|
+
const api = this.apis.get(restApiId);
|
|
2028
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
2029
|
+
return {
|
|
2030
|
+
items: Array.from(api.models.values()).map(m => ({
|
|
2031
|
+
name: m.name,
|
|
2032
|
+
description: m.description,
|
|
2033
|
+
contentType: m.contentType,
|
|
2034
|
+
createdDate: m.createdDate
|
|
2035
|
+
}))
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
deleteModel(params) {
|
|
2040
|
+
const { restApiId, modelName } = params;
|
|
2041
|
+
const api = this.apis.get(restApiId);
|
|
2042
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
2043
|
+
if (!api.models.has(modelName)) throw new Error(`Model ${modelName} not found`);
|
|
2044
|
+
api.models.delete(modelName);
|
|
2045
|
+
this.persistAPIs();
|
|
2046
|
+
return {};
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// ============ Request Validator Operations ============
|
|
2050
|
+
|
|
2051
|
+
createRequestValidator(params) {
|
|
2052
|
+
const { restApiId, name, validateRequestBody, validateRequestParameters } = params;
|
|
2053
|
+
const api = this.apis.get(restApiId);
|
|
2054
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
2055
|
+
|
|
2056
|
+
const id = `rv_${crypto.randomBytes(6).toString('hex')}`;
|
|
2057
|
+
const validator = {
|
|
2058
|
+
id,
|
|
2059
|
+
name: name || `validator-${id}`,
|
|
2060
|
+
validateRequestBody: !!validateRequestBody,
|
|
2061
|
+
validateRequestParameters: !!validateRequestParameters
|
|
2062
|
+
};
|
|
2063
|
+
|
|
2064
|
+
api.requestValidators.set(id, validator);
|
|
2065
|
+
this.persistAPIs();
|
|
2066
|
+
return validator;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
getRequestValidator(params) {
|
|
2070
|
+
const { restApiId, requestValidatorId } = params;
|
|
2071
|
+
const api = this.apis.get(restApiId);
|
|
2072
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
2073
|
+
const v = api.requestValidators.get(requestValidatorId);
|
|
2074
|
+
if (!v) throw new Error(`Request validator ${requestValidatorId} not found`);
|
|
2075
|
+
return { ...v };
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
getRequestValidators(params) {
|
|
2079
|
+
const { restApiId } = params;
|
|
2080
|
+
const api = this.apis.get(restApiId);
|
|
2081
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
2082
|
+
return { items: Array.from(api.requestValidators.values()) };
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
updateRequestValidator(params) {
|
|
2086
|
+
const { restApiId, requestValidatorId, name, validateRequestBody, validateRequestParameters } = params;
|
|
2087
|
+
const api = this.apis.get(restApiId);
|
|
2088
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
2089
|
+
const v = api.requestValidators.get(requestValidatorId);
|
|
2090
|
+
if (!v) throw new Error(`Request validator ${requestValidatorId} not found`);
|
|
2091
|
+
if (name !== undefined) v.name = name;
|
|
2092
|
+
if (validateRequestBody !== undefined) v.validateRequestBody = !!validateRequestBody;
|
|
2093
|
+
if (validateRequestParameters !== undefined) v.validateRequestParameters = !!validateRequestParameters;
|
|
2094
|
+
this.persistAPIs();
|
|
2095
|
+
return { ...v };
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
deleteRequestValidator(params) {
|
|
2099
|
+
const { restApiId, requestValidatorId } = params;
|
|
2100
|
+
const api = this.apis.get(restApiId);
|
|
2101
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
2102
|
+
if (!api.requestValidators.has(requestValidatorId)) throw new Error(`Request validator ${requestValidatorId} not found`);
|
|
2103
|
+
api.requestValidators.delete(requestValidatorId);
|
|
2104
|
+
this.persistAPIs();
|
|
2105
|
+
return {};
|
|
2106
|
+
}
|
|
2107
|
+
|
|
1355
2108
|
async reset() {
|
|
1356
2109
|
this.apis.clear();
|
|
1357
2110
|
this.websocketApis.clear();
|
|
@@ -1364,6 +2117,7 @@ class APIGatewaySimulator {
|
|
|
1364
2117
|
this.usagePlans.clear();
|
|
1365
2118
|
this.apiKeys.clear();
|
|
1366
2119
|
this.domainNames.clear();
|
|
2120
|
+
this.requestValidators.clear();
|
|
1367
2121
|
|
|
1368
2122
|
this.persistAPIs();
|
|
1369
2123
|
this.persistApiKeys();
|