@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.
Files changed (150) hide show
  1. package/README.md +662 -658
  2. package/package.json +11 -4
  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/handler-loader.js +50 -5
  100. package/src/services/lambda/index.js +26 -0
  101. package/src/services/lambda/server.js +18 -12
  102. package/src/services/lambda/simulator.js +73 -17
  103. package/src/services/opensearch/index.js +105 -0
  104. package/src/services/opensearch/models/document.js +115 -0
  105. package/src/services/opensearch/models/index.js +73 -0
  106. package/src/services/opensearch/server.js +310 -0
  107. package/src/services/opensearch/simulator.js +605 -0
  108. package/src/services/opensearch/store.js +162 -0
  109. package/src/services/organizations/control-tower-simulator.js +321 -0
  110. package/src/services/organizations/control-tower-store.js +182 -0
  111. package/src/services/organizations/iam-integration.js +413 -0
  112. package/src/services/organizations/index.js +172 -0
  113. package/src/services/organizations/models/index.js +13 -0
  114. package/src/services/organizations/models/invitation.js +48 -0
  115. package/src/services/organizations/models/organization.js +566 -0
  116. package/src/services/organizations/models/role.js +244 -0
  117. package/src/services/organizations/models/scp.js +131 -0
  118. package/src/services/organizations/server.js +736 -0
  119. package/src/services/organizations/simulator.js +593 -0
  120. package/src/services/organizations/store.js +235 -0
  121. package/src/services/organizations/vpc-integration.js +453 -0
  122. package/src/services/route53/index.js +67 -0
  123. package/src/services/route53/models/healthCheck.js +101 -0
  124. package/src/services/route53/models/hostedZone.js +37 -0
  125. package/src/services/route53/models/resourceRecord.js +106 -0
  126. package/src/services/route53/server.js +841 -0
  127. package/src/services/route53/simulator.js +350 -0
  128. package/src/services/s3/index.js +62 -0
  129. package/src/services/s3/presigned-url-generator.js +337 -0
  130. package/src/services/s3/server.js +231 -0
  131. package/src/services/s3/simulator.js +551 -4
  132. package/src/services/sns/index.js +31 -0
  133. package/src/services/sns/server.js +9 -0
  134. package/src/services/sqs/index.js +38 -0
  135. package/src/services/sqs/server.js +57 -4
  136. package/src/services/sqs/simulator.js +423 -33
  137. package/src/services/sts/simulator.js +209 -17
  138. package/src/services/vpc/README.md +459 -0
  139. package/src/services/vpc/ec2ResponseFormatter.js +431 -0
  140. package/src/services/vpc/index.js +90 -0
  141. package/src/services/vpc/models/internetGateway.js +71 -0
  142. package/src/services/vpc/models/routeTable.js +160 -0
  143. package/src/services/vpc/models/subnet.js +48 -0
  144. package/src/services/vpc/models/vpc.js +37 -0
  145. package/src/services/vpc/models/vpcEndpoint.js +100 -0
  146. package/src/services/vpc/models/vpcGateway.js +134 -0
  147. package/src/services/vpc/server.js +836 -0
  148. package/src/services/vpc/simulator.js +613 -0
  149. package/src/utils/policy-cache.js +215 -0
  150. package/aws-config +0 -154
@@ -0,0 +1,514 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Data API Handler - Implements Aurora Data API (RDSDataService) operations.
5
+ *
6
+ * Handles: ExecuteStatement, BatchExecuteStatement, BeginTransaction,
7
+ * CommitTransaction, RollbackTransaction.
8
+ *
9
+ * Converts AWS typed parameters to SQLite bindings and formats results
10
+ * in the Aurora Data API response format.
11
+ */
12
+
13
+ class DataApiHandler {
14
+ /**
15
+ * @param {import('./db-engine').DbEngine} dbEngine
16
+ * @param {import('./sql-translator')} sqlTranslator
17
+ * @param {import('./cluster-manager').ClusterManager} clusterManager
18
+ * @param {object} logger
19
+ */
20
+ constructor(dbEngine, sqlTranslator, clusterManager, logger) {
21
+ this.dbEngine = dbEngine;
22
+ this.sqlTranslator = sqlTranslator;
23
+ this.clusterManager = clusterManager;
24
+ this.logger = logger;
25
+ this.transactions = new Map(); // transactionId → { dbName, startedAt, timer }
26
+
27
+ // Optional service dependencies (wired via dependency injection in task 9.1)
28
+ this.secretManagerService = null;
29
+ this.cloudWatchService = null;
30
+ this.cloudTrailService = null;
31
+ }
32
+
33
+ /**
34
+ * Routes incoming Data API requests to the appropriate handler method.
35
+ * @param {string} target - The x-amz-target value (e.g., 'RDSDataService.ExecuteStatement')
36
+ * @param {object} body - The request body
37
+ * @returns {Promise<object>} The response object
38
+ */
39
+ async handleRequest(target, body) {
40
+ switch (target) {
41
+ case 'RDSDataService.ExecuteStatement':
42
+ return this.executeStatement(body);
43
+ case 'RDSDataService.BatchExecuteStatement':
44
+ return this.batchExecuteStatement(body);
45
+ case 'RDSDataService.BeginTransaction':
46
+ return this.beginTransaction(body);
47
+ case 'RDSDataService.CommitTransaction':
48
+ return this.commitTransaction(body);
49
+ case 'RDSDataService.RollbackTransaction':
50
+ return this.rollbackTransaction(body);
51
+ default:
52
+ const error = new Error(`Unknown target: ${target}`);
53
+ error.code = 'BadRequestException';
54
+ error.statusCode = 400;
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Binds named parameters (:name) to positional placeholders (?).
61
+ * Does NOT mutate the original SQL string.
62
+ *
63
+ * @param {string} sql - SQL with :name placeholders
64
+ * @param {Array<{name: string, value: object}>} parameters - Typed parameter array
65
+ * @returns {{ boundSql: string, bindings: Array }} SQL with ? placeholders and positional bindings
66
+ */
67
+ bindParameters(sql, parameters = []) {
68
+ if (!parameters.length) return { boundSql: sql, bindings: [] };
69
+
70
+ const bindings = [];
71
+ let boundSql = sql;
72
+
73
+ // Sort parameters by name length descending (longest first to avoid partial matches)
74
+ const sorted = [...parameters].sort((a, b) => b.name.length - a.name.length);
75
+
76
+ for (const param of sorted) {
77
+ const placeholder = `:${param.name}`;
78
+ const value = this.extractValue(param.value);
79
+
80
+ // Replace all occurrences of :name with ?
81
+ while (boundSql.includes(placeholder)) {
82
+ const idx = boundSql.indexOf(placeholder);
83
+ boundSql = boundSql.substring(0, idx) + '?' + boundSql.substring(idx + placeholder.length);
84
+ bindings.push(value);
85
+ }
86
+ }
87
+
88
+ return { boundSql, bindings };
89
+ }
90
+
91
+ /**
92
+ * Extracts native JavaScript value from an AWS typed parameter value object.
93
+ *
94
+ * @param {object} typedValue - AWS typed value object
95
+ * @returns {*} The extracted native value
96
+ */
97
+ extractValue(typedValue) {
98
+ if (typedValue.isNull) return null;
99
+ if (typedValue.stringValue !== undefined) return typedValue.stringValue;
100
+ if (typedValue.longValue !== undefined) return Number(typedValue.longValue);
101
+ if (typedValue.doubleValue !== undefined) return Number(typedValue.doubleValue);
102
+ if (typedValue.booleanValue !== undefined) return typedValue.booleanValue ? 1 : 0;
103
+ if (typedValue.blobValue !== undefined) return Buffer.from(typedValue.blobValue, 'base64');
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Executes a single SQL statement via the Data API.
109
+ * @param {object} params - ExecuteStatement parameters
110
+ * @returns {Promise<object>} ExecuteStatement response
111
+ */
112
+ async executeStatement({ resourceArn, secretArn, database, sql, parameters, transactionId, includeResultMetadata }) {
113
+ // 1. Validate secretArn (if secretManagerService is available)
114
+ if (this.secretManagerService) {
115
+ await this._validateSecretArn(secretArn);
116
+ }
117
+
118
+ // 2. Resolve cluster from resourceArn
119
+ const cluster = this._resolveCluster(resourceArn);
120
+
121
+ // 3. Determine database name
122
+ const dbName = database || cluster.DatabaseName;
123
+ if (!dbName) {
124
+ const error = new Error('No database specified and cluster has no default database');
125
+ error.code = 'BadRequestException';
126
+ error.statusCode = 400;
127
+ throw error;
128
+ }
129
+
130
+ // 4. Determine engine mode and create translator
131
+ const engineMode = cluster.Engine; // 'aurora-mysql' or 'aurora-postgresql'
132
+ const SqlTranslator = require('./sql-translator');
133
+ const translator = new SqlTranslator(engineMode);
134
+
135
+ // 5. Translate SQL
136
+ const translatedSql = translator.translate(sql);
137
+
138
+ // 6. Bind parameters
139
+ const { boundSql, bindings } = this.bindParameters(translatedSql, parameters);
140
+
141
+ // 7. Execute
142
+ let result;
143
+ if (transactionId) {
144
+ // Check transaction exists
145
+ const txn = this.transactions.get(transactionId);
146
+ if (!txn) {
147
+ const error = new Error(`Transaction ${transactionId} is not found`);
148
+ error.code = 'BadRequestException';
149
+ error.statusCode = 400;
150
+ throw error;
151
+ }
152
+ result = this.dbEngine.executeInTransaction(transactionId, boundSql, bindings);
153
+ } else {
154
+ result = this.dbEngine.execute(dbName, boundSql, bindings);
155
+ }
156
+
157
+ // 8. Format response
158
+ const response = {};
159
+
160
+ if (result.type === 'select') {
161
+ response.records = this.formatResults(result.rows, result.columns).records;
162
+ if (includeResultMetadata) {
163
+ response.columnMetadata = this.formatResults(result.rows, result.columns).columnMetadata;
164
+ }
165
+ response.numberOfRecordsUpdated = 0;
166
+ } else if (result.type === 'dml') {
167
+ response.numberOfRecordsUpdated = result.changes;
168
+ response.generatedFields = [];
169
+ // Only include lastInsertRowid for INSERT statements
170
+ const isInsert = boundSql.trim().toUpperCase().startsWith('INSERT');
171
+ if (isInsert && result.lastInsertRowid && result.lastInsertRowid > 0) {
172
+ response.generatedFields.push({ longValue: result.lastInsertRowid });
173
+ }
174
+ if (includeResultMetadata) {
175
+ response.columnMetadata = [];
176
+ }
177
+ } else {
178
+ // DDL
179
+ response.numberOfRecordsUpdated = 0;
180
+ response.generatedFields = [];
181
+ if (includeResultMetadata) {
182
+ response.columnMetadata = [];
183
+ }
184
+ }
185
+
186
+ // 9. Emit CloudWatch metric (if available)
187
+ if (this.cloudWatchService) {
188
+ try {
189
+ this.cloudWatchService.putMetricData({
190
+ namespace: 'AWS/RDS',
191
+ metricData: [{
192
+ metricName: 'DataAPIRequests',
193
+ value: 1,
194
+ unit: 'Count',
195
+ dimensions: [{ name: 'DBClusterIdentifier', value: cluster.DBClusterIdentifier }]
196
+ }]
197
+ });
198
+ } catch (e) {
199
+ // Don't fail the request if CloudWatch fails
200
+ }
201
+ }
202
+
203
+ return response;
204
+ }
205
+
206
+ /**
207
+ * Executes a SQL statement with multiple parameter sets.
208
+ * @param {object} params - BatchExecuteStatement parameters
209
+ * @returns {Promise<object>} BatchExecuteStatement response with updateResults array
210
+ */
211
+ async batchExecuteStatement({ resourceArn, secretArn, database, sql, parameterSets, transactionId }) {
212
+ // 1. Validate secretArn (if secretManagerService is available)
213
+ if (this.secretManagerService) {
214
+ await this._validateSecretArn(secretArn);
215
+ }
216
+
217
+ // 2. Resolve cluster from resourceArn
218
+ const cluster = this._resolveCluster(resourceArn);
219
+
220
+ // 3. Determine database name
221
+ const dbName = database || cluster.DatabaseName;
222
+ if (!dbName) {
223
+ const error = new Error('No database specified and cluster has no default database');
224
+ error.code = 'BadRequestException';
225
+ error.statusCode = 400;
226
+ throw error;
227
+ }
228
+
229
+ // 4. Translate SQL
230
+ const engineMode = cluster.Engine;
231
+ const SqlTranslator = require('./sql-translator');
232
+ const translator = new SqlTranslator(engineMode);
233
+ const translatedSql = translator.translate(sql);
234
+
235
+ // 5. Execute once for each parameter set
236
+ const updateResults = [];
237
+ const paramSets = parameterSets || [];
238
+
239
+ for (let i = 0; i < paramSets.length; i++) {
240
+ try {
241
+ const { boundSql, bindings } = this.bindParameters(translatedSql, paramSets[i]);
242
+ let result;
243
+ if (transactionId) {
244
+ const txn = this.transactions.get(transactionId);
245
+ if (!txn) {
246
+ const error = new Error(`Transaction ${transactionId} is not found`);
247
+ error.code = 'BadRequestException';
248
+ error.statusCode = 400;
249
+ throw error;
250
+ }
251
+ result = this.dbEngine.executeInTransaction(transactionId, boundSql, bindings);
252
+ } else {
253
+ result = this.dbEngine.execute(dbName, boundSql, bindings);
254
+ }
255
+
256
+ const updateResult = {};
257
+ if (result.lastInsertRowid && result.lastInsertRowid > 0) {
258
+ updateResult.generatedFields = [{ longValue: result.lastInsertRowid }];
259
+ } else {
260
+ updateResult.generatedFields = [];
261
+ }
262
+ updateResults.push(updateResult);
263
+ } catch (err) {
264
+ // Report error with indication of which parameter set failed
265
+ const error = new Error(`Error at parameter set index ${i}: ${err.message}`);
266
+ error.code = err.code || 'BadRequestException';
267
+ error.statusCode = err.statusCode || 400;
268
+ throw error;
269
+ }
270
+ }
271
+
272
+ // 6. Emit CloudWatch metric (if available)
273
+ if (this.cloudWatchService) {
274
+ try {
275
+ this.cloudWatchService.putMetricData({
276
+ namespace: 'AWS/RDS',
277
+ metricData: [{
278
+ metricName: 'DataAPIRequests',
279
+ value: 1,
280
+ unit: 'Count',
281
+ dimensions: [{ name: 'DBClusterIdentifier', value: cluster.DBClusterIdentifier }]
282
+ }]
283
+ });
284
+ } catch (e) { /* Don't fail if CloudWatch fails */ }
285
+ }
286
+
287
+ return { updateResults };
288
+ }
289
+
290
+ /**
291
+ * Begins a new transaction.
292
+ * @param {object} params - BeginTransaction parameters
293
+ * @returns {Promise<object>} BeginTransaction response with transactionId
294
+ */
295
+ async beginTransaction({ resourceArn, secretArn, database }) {
296
+ // 1. Validate
297
+ if (this.secretManagerService) {
298
+ await this._validateSecretArn(secretArn);
299
+ }
300
+ const cluster = this._resolveCluster(resourceArn);
301
+
302
+ const dbName = database || cluster.DatabaseName;
303
+ if (!dbName) {
304
+ const error = new Error('No database specified and cluster has no default database');
305
+ error.code = 'BadRequestException';
306
+ error.statusCode = 400;
307
+ throw error;
308
+ }
309
+
310
+ // 2. Begin transaction via DbEngine
311
+ const transactionId = this.dbEngine.beginTransaction(dbName);
312
+
313
+ // 3. Store in transactions Map with timeout timer
314
+ const TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
315
+ const timer = setTimeout(() => {
316
+ // Auto-rollback on timeout
317
+ try {
318
+ this.dbEngine.rollback(transactionId);
319
+ } catch (e) { /* already rolled back */ }
320
+ this.transactions.delete(transactionId);
321
+ }, TIMEOUT_MS);
322
+
323
+ // Don't hold the event loop open
324
+ if (timer.unref) timer.unref();
325
+
326
+ this.transactions.set(transactionId, { dbName, startedAt: Date.now(), timer });
327
+
328
+ return { transactionId };
329
+ }
330
+
331
+ /**
332
+ * Commits an active transaction.
333
+ * @param {object} params - CommitTransaction parameters
334
+ * @returns {Promise<object>} CommitTransaction response with transactionStatus
335
+ */
336
+ async commitTransaction({ resourceArn, secretArn, transactionId }) {
337
+ // Validate
338
+ if (this.secretManagerService) {
339
+ await this._validateSecretArn(secretArn);
340
+ }
341
+
342
+ const txn = this.transactions.get(transactionId);
343
+ if (!txn) {
344
+ const error = new Error(`Transaction ${transactionId} is not found`);
345
+ error.code = 'BadRequestException';
346
+ error.statusCode = 400;
347
+ throw error;
348
+ }
349
+
350
+ // Commit via DbEngine
351
+ this.dbEngine.commit(transactionId);
352
+
353
+ // Cleanup
354
+ if (txn.timer) clearTimeout(txn.timer);
355
+ this.transactions.delete(transactionId);
356
+
357
+ return { transactionStatus: 'Transaction Committed' };
358
+ }
359
+
360
+ /**
361
+ * Rolls back an active transaction.
362
+ * @param {object} params - RollbackTransaction parameters
363
+ * @returns {Promise<object>} RollbackTransaction response with transactionStatus
364
+ */
365
+ async rollbackTransaction({ resourceArn, secretArn, transactionId }) {
366
+ // Validate
367
+ if (this.secretManagerService) {
368
+ await this._validateSecretArn(secretArn);
369
+ }
370
+
371
+ const txn = this.transactions.get(transactionId);
372
+ if (!txn) {
373
+ const error = new Error(`Transaction ${transactionId} is not found`);
374
+ error.code = 'BadRequestException';
375
+ error.statusCode = 400;
376
+ throw error;
377
+ }
378
+
379
+ // Rollback via DbEngine
380
+ this.dbEngine.rollback(transactionId);
381
+
382
+ // Cleanup
383
+ if (txn.timer) clearTimeout(txn.timer);
384
+ this.transactions.delete(transactionId);
385
+
386
+ return { transactionStatus: 'Transaction Rolledback' };
387
+ }
388
+
389
+ /**
390
+ * Resolves a cluster from its ARN.
391
+ * @param {string} resourceArn
392
+ * @returns {object} Cluster object
393
+ */
394
+ _resolveCluster(resourceArn) {
395
+ if (!resourceArn) {
396
+ const error = new Error('resourceArn is required');
397
+ error.code = 'BadRequestException';
398
+ error.statusCode = 400;
399
+ throw error;
400
+ }
401
+
402
+ const cluster = this.clusterManager.getClusterByArn(resourceArn);
403
+ if (!cluster) {
404
+ const error = new Error(`Cluster not found for ARN: ${resourceArn}`);
405
+ error.code = 'NotFoundException';
406
+ error.statusCode = 404;
407
+ throw error;
408
+ }
409
+ return cluster;
410
+ }
411
+
412
+ /**
413
+ * Validates a secretArn against Secret Manager.
414
+ * If the Secret Manager service is available, checks that the secret actually exists.
415
+ * For local development, accepts any secret ARN matching the Aurora pattern.
416
+ * @param {string} secretArn
417
+ */
418
+ async _validateSecretArn(secretArn) {
419
+ if (!secretArn) {
420
+ const error = new Error('secretArn is required');
421
+ error.code = 'BadRequestException';
422
+ error.statusCode = 400;
423
+ throw error;
424
+ }
425
+
426
+ // Always accept Aurora-style secret ARNs for local development
427
+ // This allows the simulator to work without requiring Secret Manager
428
+ const auroraPattern = /^arn:aws:secretsmanager:[^:]+:\d+:secret:(aurora-|rds-)/i;
429
+ if (auroraPattern.test(secretArn)) {
430
+ // Accept Aurora-style secrets for local development
431
+ return;
432
+ }
433
+
434
+ // If Secret Manager service is available, validate non-Aurora secrets
435
+ if (this.secretManagerService) {
436
+ try {
437
+ // getSecretValue validates the secret exists and is accessible
438
+ await this.secretManagerService.getSecretValue({ SecretId: secretArn });
439
+ } catch (e) {
440
+ const error = new Error('Invalid secretArn');
441
+ error.code = 'BadRequestException';
442
+ error.statusCode = 400;
443
+ throw error;
444
+ }
445
+ }
446
+ // If no Secret Manager service, accept any secret ARN for local development
447
+ }
448
+
449
+ /**
450
+ * Formats SQLite query results into Aurora Data API format.
451
+ * @param {Array<object>} sqliteRows - Row objects from better-sqlite3
452
+ * @param {Array<object>} sqliteColumns - Column metadata from stmt.columns()
453
+ * @returns {{ records: Array[], columnMetadata: Array<object> }}
454
+ */
455
+ formatResults(sqliteRows, sqliteColumns) {
456
+ const columnMetadata = sqliteColumns.map(col => ({
457
+ name: col.name,
458
+ label: col.name,
459
+ typeName: this._inferTypeName(col),
460
+ type: this._inferJdbcType(col),
461
+ nullable: true,
462
+ precision: 0,
463
+ scale: 0,
464
+ }));
465
+
466
+ const records = sqliteRows.map(row => {
467
+ return sqliteColumns.map(col => {
468
+ const value = row[col.name];
469
+ if (value === null || value === undefined) return { isNull: true };
470
+ if (typeof value === 'number' && Number.isInteger(value)) return { longValue: value };
471
+ if (typeof value === 'number') return { doubleValue: value };
472
+ if (typeof value === 'boolean') return { booleanValue: value };
473
+ if (Buffer.isBuffer(value)) return { blobValue: value.toString('base64') };
474
+ return { stringValue: String(value) };
475
+ });
476
+ });
477
+
478
+ return { records, columnMetadata };
479
+ }
480
+
481
+ /**
482
+ * Infers SQL type name from a better-sqlite3 column descriptor.
483
+ * @param {object} col - Column object from stmt.columns()
484
+ * @returns {string} SQL type name
485
+ */
486
+ _inferTypeName(col) {
487
+ const type = col.type ? col.type.toUpperCase() : null;
488
+ switch (type) {
489
+ case 'INTEGER': return 'INTEGER';
490
+ case 'REAL': return 'DOUBLE';
491
+ case 'TEXT': return 'VARCHAR';
492
+ case 'BLOB': return 'BLOB';
493
+ default: return 'VARCHAR';
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Infers JDBC type number from a better-sqlite3 column descriptor.
499
+ * @param {object} col - Column object from stmt.columns()
500
+ * @returns {number} JDBC type number
501
+ */
502
+ _inferJdbcType(col) {
503
+ const type = col.type ? col.type.toUpperCase() : null;
504
+ switch (type) {
505
+ case 'INTEGER': return 4; // JDBC INTEGER
506
+ case 'REAL': return 8; // JDBC DOUBLE
507
+ case 'TEXT': return 12; // JDBC VARCHAR
508
+ case 'BLOB': return 2004; // JDBC BLOB
509
+ default: return 12; // JDBC VARCHAR (default)
510
+ }
511
+ }
512
+ }
513
+
514
+ module.exports = { DataApiHandler };