@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.
- package/README.md +662 -658
- package/package.json +10 -3
- 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/index.js +26 -0
- package/src/services/lambda/server.js +12 -1
- package/src/services/lambda/simulator.js +4 -0
- 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
|
@@ -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 };
|