@gugananuvem/aws-local-simulator 1.0.35 → 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.
Files changed (149) hide show
  1. package/README.md +662 -658
  2. package/package.json +10 -3
  3. package/src/config/default-config.js +71 -0
  4. package/src/config/env-loader.js +4 -2
  5. package/src/middleware/authorization.js +271 -0
  6. package/src/middleware/cloudtrail-logger.js +333 -0
  7. package/src/middleware/error-handler.js +138 -0
  8. package/src/middleware/index.js +79 -0
  9. package/src/middleware/policy-enforcement.js +407 -0
  10. package/src/server.js +74 -4
  11. package/src/services/acm/index.js +66 -0
  12. package/src/services/acm/models/certificate.js +202 -0
  13. package/src/services/acm/server.js +224 -0
  14. package/src/services/acm/simulator.js +293 -0
  15. package/src/services/alb/index.js +104 -0
  16. package/src/services/alb/models/loadBalancer.js +88 -0
  17. package/src/services/alb/models/targetGroup.js +128 -0
  18. package/src/services/alb/server.js +160 -0
  19. package/src/services/alb/simulator.js +262 -0
  20. package/src/services/apigateway/index.js +22 -0
  21. package/src/services/apigateway/server.js +247 -35
  22. package/src/services/apigateway/simulator.js +780 -26
  23. package/src/services/asg/index.js +100 -0
  24. package/src/services/asg/models/autoScalingGroup.js +110 -0
  25. package/src/services/asg/models/launchTemplate.js +41 -0
  26. package/src/services/asg/models/scalingPolicy.js +51 -0
  27. package/src/services/asg/server.js +236 -0
  28. package/src/services/asg/simulator.js +410 -0
  29. package/src/services/aurora/cluster-manager.js +731 -0
  30. package/src/services/aurora/data-api.js +514 -0
  31. package/src/services/aurora/db-engine.js +270 -0
  32. package/src/services/aurora/index.js +159 -0
  33. package/src/services/aurora/rds-api.js +189 -0
  34. package/src/services/aurora/server.js +213 -0
  35. package/src/services/aurora/sql-translator.js +334 -0
  36. package/src/services/cloudformation/index.js +2 -2
  37. package/src/services/cloudformation/server.js +1 -0
  38. package/src/services/cloudfront/cache-engine.js +225 -0
  39. package/src/services/cloudfront/geo-restrictions.js +377 -0
  40. package/src/services/cloudfront/index.js +220 -0
  41. package/src/services/cloudfront/models/behavior.js +158 -0
  42. package/src/services/cloudfront/models/distribution.js +284 -0
  43. package/src/services/cloudfront/models/index.js +99 -0
  44. package/src/services/cloudfront/models/invalidation.js +109 -0
  45. package/src/services/cloudfront/models/origin.js +155 -0
  46. package/src/services/cloudfront/origin-router.js +552 -0
  47. package/src/services/cloudfront/origin-validator.js +398 -0
  48. package/src/services/cloudfront/server.js +512 -0
  49. package/src/services/cloudfront/signed-url-generator.js +377 -0
  50. package/src/services/cloudfront/simulator.js +425 -0
  51. package/src/services/cloudfront/store.js +326 -0
  52. package/src/services/cloudshell/env-builder.js +27 -0
  53. package/src/services/cloudshell/index.js +48 -0
  54. package/src/services/cloudshell/routes.js +60 -0
  55. package/src/services/cloudshell/session-manager.js +226 -0
  56. package/src/services/cloudshell/static/.gitkeep +0 -0
  57. package/src/services/cloudshell/static/index.html +406 -0
  58. package/src/services/cloudshell/websocket-handler.js +107 -0
  59. package/src/services/cloudtrail/index.js +2 -3
  60. package/src/services/cloudtrail/server.js +68 -0
  61. package/src/services/cognito/server.js +4 -1
  62. package/src/services/dynamodb/server.js +4 -1
  63. package/src/services/dynamodb/simulator.js +2 -1
  64. package/src/services/ec2/index.js +101 -0
  65. package/src/services/ec2/instanceServer.js +311 -0
  66. package/src/services/ec2/models/ami.js +39 -0
  67. package/src/services/ec2/models/instance.js +98 -0
  68. package/src/services/ec2/models/networkInterface.js +74 -0
  69. package/src/services/ec2/models/securityGroup.js +71 -0
  70. package/src/services/ec2/server.js +350 -0
  71. package/src/services/ec2/simulator.js +436 -0
  72. package/src/services/eventbridge/server.js +36 -0
  73. package/src/services/eventbridge/simulator.js +230 -45
  74. package/src/services/iam/index.js +65 -0
  75. package/src/services/iam/middleware.js +99 -0
  76. package/src/services/iam/server.js +331 -0
  77. package/src/services/iam/simulator.js +1432 -0
  78. package/src/services/index.js +28 -0
  79. package/src/services/kinesis-analytics/index.js +105 -0
  80. package/src/services/kinesis-analytics/models/application.js +325 -0
  81. package/src/services/kinesis-analytics/models/index.js +12 -0
  82. package/src/services/kinesis-analytics/server.js +250 -0
  83. package/src/services/kinesis-analytics/simulator.js +407 -0
  84. package/src/services/kinesis-analytics/store.js +176 -0
  85. package/src/services/kinesis-data-streams/index.js +106 -0
  86. package/src/services/kinesis-data-streams/models/index.js +12 -0
  87. package/src/services/kinesis-data-streams/models/record.js +70 -0
  88. package/src/services/kinesis-data-streams/models/stream.js +280 -0
  89. package/src/services/kinesis-data-streams/server.js +255 -0
  90. package/src/services/kinesis-data-streams/simulator.js +514 -0
  91. package/src/services/kinesis-data-streams/store.js +163 -0
  92. package/src/services/kinesis-firehose/index.js +106 -0
  93. package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
  94. package/src/services/kinesis-firehose/models/index.js +10 -0
  95. package/src/services/kinesis-firehose/server.js +215 -0
  96. package/src/services/kinesis-firehose/simulator.js +388 -0
  97. package/src/services/kinesis-firehose/store.js +163 -0
  98. package/src/services/kms/server.js +4 -1
  99. package/src/services/lambda/index.js +26 -0
  100. package/src/services/lambda/server.js +12 -1
  101. package/src/services/lambda/simulator.js +4 -0
  102. package/src/services/opensearch/index.js +105 -0
  103. package/src/services/opensearch/models/document.js +115 -0
  104. package/src/services/opensearch/models/index.js +73 -0
  105. package/src/services/opensearch/server.js +310 -0
  106. package/src/services/opensearch/simulator.js +605 -0
  107. package/src/services/opensearch/store.js +162 -0
  108. package/src/services/organizations/control-tower-simulator.js +321 -0
  109. package/src/services/organizations/control-tower-store.js +182 -0
  110. package/src/services/organizations/iam-integration.js +413 -0
  111. package/src/services/organizations/index.js +172 -0
  112. package/src/services/organizations/models/index.js +13 -0
  113. package/src/services/organizations/models/invitation.js +48 -0
  114. package/src/services/organizations/models/organization.js +566 -0
  115. package/src/services/organizations/models/role.js +244 -0
  116. package/src/services/organizations/models/scp.js +131 -0
  117. package/src/services/organizations/server.js +736 -0
  118. package/src/services/organizations/simulator.js +593 -0
  119. package/src/services/organizations/store.js +235 -0
  120. package/src/services/organizations/vpc-integration.js +453 -0
  121. package/src/services/route53/index.js +67 -0
  122. package/src/services/route53/models/healthCheck.js +101 -0
  123. package/src/services/route53/models/hostedZone.js +37 -0
  124. package/src/services/route53/models/resourceRecord.js +106 -0
  125. package/src/services/route53/server.js +841 -0
  126. package/src/services/route53/simulator.js +350 -0
  127. package/src/services/s3/index.js +62 -0
  128. package/src/services/s3/presigned-url-generator.js +337 -0
  129. package/src/services/s3/server.js +231 -0
  130. package/src/services/s3/simulator.js +551 -4
  131. package/src/services/sns/index.js +31 -0
  132. package/src/services/sns/server.js +9 -0
  133. package/src/services/sqs/index.js +38 -0
  134. package/src/services/sqs/server.js +57 -4
  135. package/src/services/sqs/simulator.js +423 -33
  136. package/src/services/sts/simulator.js +209 -17
  137. package/src/services/vpc/README.md +459 -0
  138. package/src/services/vpc/ec2ResponseFormatter.js +431 -0
  139. package/src/services/vpc/index.js +90 -0
  140. package/src/services/vpc/models/internetGateway.js +71 -0
  141. package/src/services/vpc/models/routeTable.js +160 -0
  142. package/src/services/vpc/models/subnet.js +48 -0
  143. package/src/services/vpc/models/vpc.js +37 -0
  144. package/src/services/vpc/models/vpcEndpoint.js +100 -0
  145. package/src/services/vpc/models/vpcGateway.js +134 -0
  146. package/src/services/vpc/server.js +836 -0
  147. package/src/services/vpc/simulator.js +613 -0
  148. package/src/utils/policy-cache.js +215 -0
  149. 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('/', rootResource);
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, // Simplified
293
+ parentId: null,
242
294
  resourceMethods: new Map()
243
295
  };
244
296
  api.resources.set(resourceId, resource);
245
297
  }
246
298
 
247
- // Put method
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
- // Simula integração com Lambda
978
- const uriParts = integration.uri.split(':');
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();