@gugananuvem/aws-local-simulator 1.0.25 → 1.0.26
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 +45 -0
- package/package.json +28 -83
- package/src/config/default-config.js +15 -4
- package/src/server.js +257 -21
- package/src/services/apigateway/server.js +37 -0
- package/src/services/apigateway/simulator.js +148 -4
- package/src/services/cognito/server.js +8 -14
- package/src/services/cognito/simulator.js +38 -11
- package/src/services/dynamodb/simulator.js +126 -49
- package/src/services/kms/server.js +15 -1
- package/src/services/kms/simulator.js +48 -28
- package/src/services/lambda/server.js +24 -0
- package/src/services/lambda/simulator.js +136 -12
- package/src/services/parameter-store/simulator.js +1 -1
- package/src/services/s3/server.js +21 -0
- package/src/services/s3/simulator.js +4 -1
- package/src/services/secret-manager/server.js +2 -1
- package/src/services/secret-manager/simulator.js +21 -10
- package/src/services/sns/server.js +32 -5
- package/src/services/sqs/server.js +11 -0
- package/src/services/sqs/simulator.js +74 -6
|
@@ -33,6 +33,7 @@ class APIGatewaySimulator {
|
|
|
33
33
|
async initialize() {
|
|
34
34
|
logger.debug('Inicializando API Gateway Simulator...');
|
|
35
35
|
this.loadAPIs();
|
|
36
|
+
this._loadStaticAPIs();
|
|
36
37
|
this.loadWebSocketAPIs();
|
|
37
38
|
this.loadDeployments();
|
|
38
39
|
this.loadStages();
|
|
@@ -44,9 +45,62 @@ class APIGatewaySimulator {
|
|
|
44
45
|
this.loadApiKeys();
|
|
45
46
|
this.loadDomainNames();
|
|
46
47
|
|
|
47
|
-
logger.debug(`✅ API Gateway Simulator inicializado com ${this.apis.size} APIs
|
|
48
|
+
logger.debug(`✅ API Gateway Simulator inicializado com ${this.apis.size} APIs (${Array.from(this.apis.values()).filter(a => a.isStatic).length} estáticas)`);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
_loadStaticAPIs() {
|
|
52
|
+
const staticApis = this.config.apigateway?.apis || [];
|
|
53
|
+
staticApis.forEach((apiConfig, index) => {
|
|
54
|
+
const apiId = `static_${index}`;
|
|
55
|
+
|
|
56
|
+
const api = {
|
|
57
|
+
id: apiId,
|
|
58
|
+
name: apiConfig.name,
|
|
59
|
+
description: apiConfig.description || 'Configured in aws-local-simulator.json',
|
|
60
|
+
version: 'config',
|
|
61
|
+
createdDate: new Date().toISOString(),
|
|
62
|
+
isStatic: true,
|
|
63
|
+
apiKeySource: 'HEADER',
|
|
64
|
+
endpointConfiguration: { types: ['REGIONAL'] },
|
|
65
|
+
resources: new Map(),
|
|
66
|
+
stages: new Map(),
|
|
67
|
+
deployments: new Map(),
|
|
68
|
+
models: new Map(),
|
|
69
|
+
authorizers: new Map()
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Adiciona recursos a partir dos endpoints configurados
|
|
73
|
+
(apiConfig.endpoints || []).forEach((ep, epIndex) => {
|
|
74
|
+
const resId = `res_${apiId}_${epIndex}`;
|
|
75
|
+
api.resources.set(resId, {
|
|
76
|
+
id: resId,
|
|
77
|
+
path: ep.path,
|
|
78
|
+
pathPart: ep.path.split('/').pop() || '/',
|
|
79
|
+
resourceMethods: new Map([[ep.method, {
|
|
80
|
+
httpMethod: ep.method,
|
|
81
|
+
authorizationType: ep.authorizerRequired ? 'COGNITO_USER_POOLS' : 'NONE',
|
|
82
|
+
apiKeyRequired: false,
|
|
83
|
+
integration: {
|
|
84
|
+
type: ep.integrationType === 'lambda' ? 'AWS_PROXY' : 'HTTP',
|
|
85
|
+
uri: ep.lambdaName,
|
|
86
|
+
integrationHttpMethod: 'POST'
|
|
87
|
+
}
|
|
88
|
+
}]])
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Adiciona um stage padrão
|
|
93
|
+
api.stages.set('local', {
|
|
94
|
+
stageName: 'local',
|
|
95
|
+
createdDate: new Date().toISOString(),
|
|
96
|
+
deploymentId: 'static-deploy'
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.apis.set(apiId, api);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
50
104
|
// ============ REST API Operations ============
|
|
51
105
|
|
|
52
106
|
createRestApi(params) {
|
|
@@ -104,11 +158,15 @@ class APIGatewaySimulator {
|
|
|
104
158
|
description: api.description,
|
|
105
159
|
version: api.version,
|
|
106
160
|
createdDate: api.createdDate,
|
|
107
|
-
apiKeySource: api.apiKeySource
|
|
161
|
+
apiKeySource: api.apiKeySource,
|
|
162
|
+
isStatic: api.isStatic || false,
|
|
163
|
+
resourceCount: api.resources.size,
|
|
164
|
+
stageCount: api.stages.size
|
|
108
165
|
}))
|
|
109
166
|
};
|
|
110
167
|
}
|
|
111
168
|
|
|
169
|
+
|
|
112
170
|
getRestApi(params) {
|
|
113
171
|
const { restApiId } = params;
|
|
114
172
|
const api = this.apis.get(restApiId);
|
|
@@ -129,6 +187,27 @@ class APIGatewaySimulator {
|
|
|
129
187
|
};
|
|
130
188
|
}
|
|
131
189
|
|
|
190
|
+
updateRestApi(params) {
|
|
191
|
+
const { restApiId, name, description } = params;
|
|
192
|
+
const api = this.apis.get(restApiId);
|
|
193
|
+
|
|
194
|
+
if (!api) {
|
|
195
|
+
throw new Error(`API ${restApiId} not found`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (name !== undefined) api.name = name;
|
|
199
|
+
if (description !== undefined) api.description = description;
|
|
200
|
+
|
|
201
|
+
if (api.isStatic) {
|
|
202
|
+
// Once edited, the API is no longer static and will be persisted
|
|
203
|
+
api.isStatic = false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.persistAPIs();
|
|
207
|
+
|
|
208
|
+
return this.getRestApi({ restApiId });
|
|
209
|
+
}
|
|
210
|
+
|
|
132
211
|
deleteRestApi(params) {
|
|
133
212
|
const { restApiId } = params;
|
|
134
213
|
|
|
@@ -142,8 +221,67 @@ class APIGatewaySimulator {
|
|
|
142
221
|
return {};
|
|
143
222
|
}
|
|
144
223
|
|
|
224
|
+
// ============ Simplified Dashboard Endpoint Operations ============
|
|
225
|
+
|
|
226
|
+
putEndpoint(params) {
|
|
227
|
+
const { restApiId, path, method, integrationType, lambdaName, authorizerRequired } = params;
|
|
228
|
+
const api = this.apis.get(restApiId);
|
|
229
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
230
|
+
|
|
231
|
+
if (api.isStatic) api.isStatic = false;
|
|
232
|
+
|
|
233
|
+
// Ensure resource exists
|
|
234
|
+
let resource = Array.from(api.resources.values()).find(r => r.path === path);
|
|
235
|
+
if (!resource) {
|
|
236
|
+
const resourceId = `res_${Date.now()}`;
|
|
237
|
+
resource = {
|
|
238
|
+
id: resourceId,
|
|
239
|
+
path: path,
|
|
240
|
+
pathPart: path.split('/').pop() || '/',
|
|
241
|
+
parentId: null, // Simplified
|
|
242
|
+
resourceMethods: new Map()
|
|
243
|
+
};
|
|
244
|
+
api.resources.set(resourceId, resource);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Put method
|
|
248
|
+
resource.resourceMethods.set(method.toUpperCase(), {
|
|
249
|
+
httpMethod: method.toUpperCase(),
|
|
250
|
+
authorizationType: authorizerRequired ? 'COGNITO_USER_POOLS' : 'NONE',
|
|
251
|
+
apiKeyRequired: false,
|
|
252
|
+
integration: {
|
|
253
|
+
type: integrationType === 'lambda' ? 'AWS_PROXY' : 'HTTP',
|
|
254
|
+
uri: lambdaName,
|
|
255
|
+
integrationHttpMethod: 'POST'
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
this.persistAPIs();
|
|
260
|
+
return { resourceId: resource.id, path, method };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
deleteEndpoint(params) {
|
|
264
|
+
const { restApiId, path, method } = params;
|
|
265
|
+
const api = this.apis.get(restApiId);
|
|
266
|
+
if (!api) throw new Error(`API ${restApiId} not found`);
|
|
267
|
+
|
|
268
|
+
if (api.isStatic) api.isStatic = false;
|
|
269
|
+
|
|
270
|
+
const resource = Array.from(api.resources.values()).find(r => r.path === path);
|
|
271
|
+
if (resource) {
|
|
272
|
+
resource.resourceMethods.delete(method.toUpperCase());
|
|
273
|
+
// If no methods left and not root, we could delete the resource, but keeping it is fine.
|
|
274
|
+
if (resource.resourceMethods.size === 0 && resource.path !== '/') {
|
|
275
|
+
api.resources.delete(resource.id);
|
|
276
|
+
}
|
|
277
|
+
this.persistAPIs();
|
|
278
|
+
}
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
|
|
145
282
|
// ============ Resource Operations ============
|
|
146
283
|
|
|
284
|
+
|
|
147
285
|
createResource(params) {
|
|
148
286
|
const { restApiId, parentId, pathPart } = params;
|
|
149
287
|
const api = this.apis.get(restApiId);
|
|
@@ -1054,7 +1192,10 @@ class APIGatewaySimulator {
|
|
|
1054
1192
|
if (saved) {
|
|
1055
1193
|
for (const [id, data] of Object.entries(saved)) {
|
|
1056
1194
|
// Reconstitui Maps
|
|
1057
|
-
data.resources = new Map(Object.entries(data.resources || {}))
|
|
1195
|
+
data.resources = new Map(Object.entries(data.resources || {}).map(([rid, r]) => {
|
|
1196
|
+
r.resourceMethods = new Map(Object.entries(r.resourceMethods || {}));
|
|
1197
|
+
return [rid, r];
|
|
1198
|
+
}));
|
|
1058
1199
|
data.stages = new Map(Object.entries(data.stages || {}));
|
|
1059
1200
|
data.deployments = new Map(Object.entries(data.deployments || {}));
|
|
1060
1201
|
data.models = new Map(Object.entries(data.models || {}));
|
|
@@ -1064,6 +1205,7 @@ class APIGatewaySimulator {
|
|
|
1064
1205
|
}
|
|
1065
1206
|
}
|
|
1066
1207
|
|
|
1208
|
+
|
|
1067
1209
|
loadWebSocketAPIs() {
|
|
1068
1210
|
const saved = this.store.read('__websocket_apis__');
|
|
1069
1211
|
if (saved) {
|
|
@@ -1157,9 +1299,10 @@ class APIGatewaySimulator {
|
|
|
1157
1299
|
persistAPIs() {
|
|
1158
1300
|
const apisObj = {};
|
|
1159
1301
|
for (const [id, api] of this.apis.entries()) {
|
|
1302
|
+
if (api.isStatic) continue;
|
|
1160
1303
|
apisObj[id] = {
|
|
1161
1304
|
...api,
|
|
1162
|
-
resources: Object.fromEntries(api.resources),
|
|
1305
|
+
resources: Object.fromEntries(Array.from(api.resources.entries()).map(([rid, r]) => [rid, { ...r, resourceMethods: Object.fromEntries(r.resourceMethods) }])),
|
|
1163
1306
|
stages: Object.fromEntries(api.stages),
|
|
1164
1307
|
deployments: Object.fromEntries(api.deployments),
|
|
1165
1308
|
models: Object.fromEntries(api.models),
|
|
@@ -1169,6 +1312,7 @@ class APIGatewaySimulator {
|
|
|
1169
1312
|
this.store.write('__apis__', apisObj);
|
|
1170
1313
|
}
|
|
1171
1314
|
|
|
1315
|
+
|
|
1172
1316
|
persistResources(apiId) {
|
|
1173
1317
|
const api = this.apis.get(apiId);
|
|
1174
1318
|
if (api) {
|
|
@@ -18,19 +18,10 @@ class CognitoServer {
|
|
|
18
18
|
|
|
19
19
|
setupMiddlewares() {
|
|
20
20
|
this.app.use(cors());
|
|
21
|
-
this.app.use(express.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
req.body = JSON.parse(req.body.toString('utf8'));
|
|
26
|
-
} catch (e) {
|
|
27
|
-
req.body = {};
|
|
28
|
-
}
|
|
29
|
-
} else if (!req.body) {
|
|
30
|
-
req.body = {};
|
|
31
|
-
}
|
|
32
|
-
next();
|
|
33
|
-
});
|
|
21
|
+
this.app.use(express.json({
|
|
22
|
+
limit: '10mb',
|
|
23
|
+
type: ['application/json', 'application/x-amz-json-1.1']
|
|
24
|
+
}));
|
|
34
25
|
|
|
35
26
|
if (logger.currentLogLevel === 'verboso') {
|
|
36
27
|
this.app.use((req, res, next) => {
|
|
@@ -73,7 +64,8 @@ class CognitoServer {
|
|
|
73
64
|
|
|
74
65
|
try {
|
|
75
66
|
const result = await this.handleRequest(target, req.body || {});
|
|
76
|
-
res.json
|
|
67
|
+
res.setHeader('Content-Type', 'application/x-amz-json-1.1');
|
|
68
|
+
res.send(JSON.stringify(result));
|
|
77
69
|
} catch (error) {
|
|
78
70
|
logger.error('Cognito Error:', error.message);
|
|
79
71
|
res.status(400).json({
|
|
@@ -108,6 +100,8 @@ class CognitoServer {
|
|
|
108
100
|
return this.simulator.describeUserPool(params);
|
|
109
101
|
case 'DeleteUserPool':
|
|
110
102
|
return this.simulator.deleteUserPool(params);
|
|
103
|
+
case 'UpdateUserPool':
|
|
104
|
+
return this.simulator.updateUserPool(params);
|
|
111
105
|
|
|
112
106
|
case 'ListUsers':
|
|
113
107
|
return this.simulator.listUsers(params);
|
|
@@ -127,8 +127,8 @@ class CognitoSimulator {
|
|
|
127
127
|
Name: PoolName,
|
|
128
128
|
Arn: `arn:aws:cognito:local:000000000000:userpool/${poolId}`,
|
|
129
129
|
Status: "ACTIVE",
|
|
130
|
-
CreationDate:
|
|
131
|
-
LastModifiedDate:
|
|
130
|
+
CreationDate: Math.floor(Date.now() / 1000),
|
|
131
|
+
LastModifiedDate: Math.floor(Date.now() / 1000),
|
|
132
132
|
Policies: Policies || {
|
|
133
133
|
PasswordPolicy: {
|
|
134
134
|
MinimumLength: 8,
|
|
@@ -171,6 +171,22 @@ class CognitoSimulator {
|
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
updateUserPool(params) {
|
|
175
|
+
const { UserPoolId, LambdaConfig, MfaConfiguration, AutoVerifiedAttributes, Policies } = params;
|
|
176
|
+
const userPool = this.userPools.get(UserPoolId);
|
|
177
|
+
if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
|
|
178
|
+
|
|
179
|
+
if (LambdaConfig !== undefined) userPool.LambdaConfig = LambdaConfig;
|
|
180
|
+
if (MfaConfiguration !== undefined) userPool.MfaConfiguration = MfaConfiguration;
|
|
181
|
+
if (AutoVerifiedAttributes !== undefined) userPool.AutoVerifiedAttributes = AutoVerifiedAttributes;
|
|
182
|
+
if (Policies !== undefined) userPool.Policies = Policies;
|
|
183
|
+
userPool.LastModifiedDate = Math.floor(Date.now() / 1000);
|
|
184
|
+
|
|
185
|
+
this.persistUserPools();
|
|
186
|
+
logger.debug(`✅ User Pool atualizado: ${UserPoolId}`);
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
|
|
174
190
|
listUserPools(params = {}) {
|
|
175
191
|
const { MaxResults = 60, NextToken } = params;
|
|
176
192
|
let userPools = Array.from(this.userPools.values());
|
|
@@ -386,7 +402,7 @@ class CognitoSimulator {
|
|
|
386
402
|
// 2. Aplica nova senha e confirma usuário
|
|
387
403
|
user.Password = this.hashPassword(newPassword);
|
|
388
404
|
user.UserStatus = "CONFIRMED";
|
|
389
|
-
user.LastModifiedDate =
|
|
405
|
+
user.LastModifiedDate = Math.floor(Date.now() / 1000);
|
|
390
406
|
this.persistUsers();
|
|
391
407
|
this.customAuthSessions.delete(params.Session);
|
|
392
408
|
|
|
@@ -424,8 +440,8 @@ class CognitoSimulator {
|
|
|
424
440
|
AccessToken: accessToken,
|
|
425
441
|
IdToken: idToken,
|
|
426
442
|
RefreshToken: refreshToken,
|
|
427
|
-
CreatedAt:
|
|
428
|
-
ExpiresAt:
|
|
443
|
+
CreatedAt: Math.floor(Date.now() / 1000),
|
|
444
|
+
ExpiresAt: Math.floor((Date.now() + 3600000) / 1000),
|
|
429
445
|
};
|
|
430
446
|
this.sessions.set(sessionId, authSession);
|
|
431
447
|
this.accessTokens.set(accessToken, authSession);
|
|
@@ -507,8 +523,8 @@ class CognitoSimulator {
|
|
|
507
523
|
AccessToken: accessToken,
|
|
508
524
|
IdToken: idToken,
|
|
509
525
|
RefreshToken: refreshToken,
|
|
510
|
-
CreatedAt:
|
|
511
|
-
ExpiresAt:
|
|
526
|
+
CreatedAt: Math.floor(Date.now() / 1000),
|
|
527
|
+
ExpiresAt: Math.floor((Date.now() + 3600000) / 1000),
|
|
512
528
|
};
|
|
513
529
|
|
|
514
530
|
this.sessions.set(sessionId, authSession);
|
|
@@ -709,8 +725,8 @@ class CognitoSimulator {
|
|
|
709
725
|
AllowedOAuthScopes: AllowedOAuthScopes || ["openid", "email", "profile"],
|
|
710
726
|
CallbackURLs: CallbackURLs || [],
|
|
711
727
|
LogoutURLs: LogoutURLs || [],
|
|
712
|
-
CreatedDate:
|
|
713
|
-
LastModifiedDate:
|
|
728
|
+
CreatedDate: Math.floor(Date.now() / 1000),
|
|
729
|
+
LastModifiedDate: Math.floor(Date.now() / 1000),
|
|
714
730
|
};
|
|
715
731
|
|
|
716
732
|
userPool.Clients.set(clientId, client);
|
|
@@ -764,8 +780,8 @@ class CognitoSimulator {
|
|
|
764
780
|
Attributes: this.normalizeUserAttributes(UserAttributes || []),
|
|
765
781
|
Enabled: true,
|
|
766
782
|
UserStatus: "UNCONFIRMED",
|
|
767
|
-
CreatedDate:
|
|
768
|
-
LastModifiedDate:
|
|
783
|
+
CreatedDate: Math.floor(Date.now() / 1000),
|
|
784
|
+
LastModifiedDate: Math.floor(Date.now() / 1000),
|
|
769
785
|
Password: this.hashPassword(Password),
|
|
770
786
|
ConfirmationCode: confirmationCode,
|
|
771
787
|
MfaOptions: [],
|
|
@@ -1485,7 +1501,16 @@ class CognitoSimulator {
|
|
|
1485
1501
|
const saved = this.store.read("__userpools__");
|
|
1486
1502
|
if (saved) {
|
|
1487
1503
|
for (const [id, data] of Object.entries(saved)) {
|
|
1504
|
+
// Sanitize dates
|
|
1505
|
+
if (typeof data.CreationDate === 'string') data.CreationDate = Math.floor(new Date(data.CreationDate).getTime() / 1000);
|
|
1506
|
+
if (typeof data.LastModifiedDate === 'string') data.LastModifiedDate = Math.floor(new Date(data.LastModifiedDate).getTime() / 1000);
|
|
1507
|
+
|
|
1488
1508
|
data.Clients = new Map(Object.entries(data.Clients || {}));
|
|
1509
|
+
for (const client of data.Clients.values()) {
|
|
1510
|
+
if (typeof client.CreatedDate === 'string') client.CreatedDate = Math.floor(new Date(client.CreatedDate).getTime() / 1000);
|
|
1511
|
+
if (typeof client.LastModifiedDate === 'string') client.LastModifiedDate = Math.floor(new Date(client.LastModifiedDate).getTime() / 1000);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1489
1514
|
data.Groups = new Map(Object.entries(data.Groups || {}));
|
|
1490
1515
|
data.IdentityProviders = new Map(Object.entries(data.IdentityProviders || {}));
|
|
1491
1516
|
data.ResourceServers = new Map(Object.entries(data.ResourceServers || {}));
|
|
@@ -1594,6 +1619,8 @@ class CognitoSimulator {
|
|
|
1594
1619
|
const saved = this.store.read("__users__");
|
|
1595
1620
|
if (saved) {
|
|
1596
1621
|
for (const [id, user] of Object.entries(saved)) {
|
|
1622
|
+
if (typeof user.CreatedDate === 'string') user.CreatedDate = Math.floor(new Date(user.CreatedDate).getTime() / 1000);
|
|
1623
|
+
if (typeof user.LastModifiedDate === 'string') user.LastModifiedDate = Math.floor(new Date(user.LastModifiedDate).getTime() / 1000);
|
|
1597
1624
|
this.users.set(id, user);
|
|
1598
1625
|
}
|
|
1599
1626
|
}
|
|
@@ -225,6 +225,19 @@ class DynamoDBSimulator {
|
|
|
225
225
|
})),
|
|
226
226
|
ItemCount: items.length,
|
|
227
227
|
TableSizeBytes: JSON.stringify(items).length,
|
|
228
|
+
GlobalSecondaryIndexes: Object.entries(table.globalSecondaryIndexes || {}).map(([indexName, gsi]) => ({
|
|
229
|
+
IndexName: indexName,
|
|
230
|
+
IndexStatus: "ACTIVE",
|
|
231
|
+
KeySchema: [
|
|
232
|
+
{ AttributeName: gsi.hashKey, KeyType: "HASH" },
|
|
233
|
+
...(gsi.rangeKey ? [{ AttributeName: gsi.rangeKey, KeyType: "RANGE" }] : []),
|
|
234
|
+
],
|
|
235
|
+
Projection: { ProjectionType: "ALL" },
|
|
236
|
+
ProvisionedThroughput: {
|
|
237
|
+
ReadCapacityUnits: 5,
|
|
238
|
+
WriteCapacityUnits: 5,
|
|
239
|
+
},
|
|
240
|
+
})),
|
|
228
241
|
ProvisionedThroughput: {
|
|
229
242
|
ReadCapacityUnits: 5,
|
|
230
243
|
WriteCapacityUnits: 5,
|
|
@@ -502,7 +515,7 @@ class DynamoDBSimulator {
|
|
|
502
515
|
}
|
|
503
516
|
|
|
504
517
|
query(params) {
|
|
505
|
-
const { TableName, KeyConditionExpression, ExpressionAttributeValues, IndexName } = params;
|
|
518
|
+
const { TableName, KeyConditionExpression, ExpressionAttributeValues, ExpressionAttributeNames = {}, IndexName } = params;
|
|
506
519
|
const table = this.tables.get(TableName);
|
|
507
520
|
|
|
508
521
|
if (!table) {
|
|
@@ -511,7 +524,7 @@ class DynamoDBSimulator {
|
|
|
511
524
|
|
|
512
525
|
let items = this.store.read(TableName);
|
|
513
526
|
|
|
514
|
-
// Resolve hash key e range key
|
|
527
|
+
// Resolve hash key e range key
|
|
515
528
|
let hashKey;
|
|
516
529
|
let rangeKey;
|
|
517
530
|
|
|
@@ -528,43 +541,56 @@ class DynamoDBSimulator {
|
|
|
528
541
|
rangeKey = table.rangeKey;
|
|
529
542
|
}
|
|
530
543
|
|
|
531
|
-
//
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const rawHashValue = ExpressionAttributeValues[hashValuePlaceholder];
|
|
537
|
-
const hashValue = rawHashValue && typeof rawHashValue === 'object' ? Object.values(rawHashValue)[0] : rawHashValue;
|
|
538
|
-
items = items.filter((item) => item[hashKey] === hashValue);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Filtra pela chave de ordenação se existir
|
|
542
|
-
if (rangeKey) {
|
|
543
|
-
const rangeConditionMatch = KeyConditionExpression.match(new RegExp(`${rangeKey}\\s*(=|>|<|>=|<=)\\s*([^\\s]+)`));
|
|
544
|
+
// Helper para resolver nomes de atributos (que podem ser placeholders como #n0)
|
|
545
|
+
const resolveAttributeName = (name) => {
|
|
546
|
+
if (name.startsWith("#")) return ExpressionAttributeNames[name] || name;
|
|
547
|
+
return name;
|
|
548
|
+
};
|
|
544
549
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
+
// Helper para extrair valor de placeholder (ex: :v0)
|
|
551
|
+
const resolveValue = (placeholder) => {
|
|
552
|
+
const rawValue = ExpressionAttributeValues[placeholder];
|
|
553
|
+
if (rawValue === undefined) return undefined;
|
|
554
|
+
// Se for formato DynamoDB { S: "..." }, desmembra. Se for nativo (DocumentClient), usa direto.
|
|
555
|
+
if (rawValue !== null && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
|
|
556
|
+
const keys = Object.keys(rawValue);
|
|
557
|
+
if (keys.length === 1 && ["S", "N", "BOOL", "NULL", "M", "L", "SS", "NS", "BS"].includes(keys[0])) {
|
|
558
|
+
return this.normalizeValue(rawValue, table);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return rawValue;
|
|
562
|
+
};
|
|
550
563
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
564
|
+
// Filtra pela KeyConditionExpression
|
|
565
|
+
// DynamoDB Query KeyConditionExpression tem formato restrito: PartitionKey = :val AND (SortKey operator :val)
|
|
566
|
+
if (KeyConditionExpression) {
|
|
567
|
+
const parts = KeyConditionExpression.split(/\s+AND\s+/i);
|
|
568
|
+
|
|
569
|
+
for (const part of parts) {
|
|
570
|
+
const match = part.match(/([^\s]+)\s*(=|>|<|>=|<=|BEGINS_WITH|BETWEEN)\s*([^\s]+)(?:\s+AND\s+([^\s]+))?/i);
|
|
571
|
+
if (match) {
|
|
572
|
+
const attrPlaceholder = match[1];
|
|
573
|
+
const operator = match[2].toUpperCase();
|
|
574
|
+
const valPlaceholder = match[3];
|
|
575
|
+
|
|
576
|
+
const attributeName = resolveAttributeName(attrPlaceholder);
|
|
577
|
+
const expectedValue = resolveValue(valPlaceholder);
|
|
578
|
+
|
|
579
|
+
if (operator === "=") {
|
|
580
|
+
items = items.filter(item => item[attributeName] === expectedValue);
|
|
581
|
+
} else if (operator === ">") {
|
|
582
|
+
items = items.filter(item => item[attributeName] > expectedValue);
|
|
583
|
+
} else if (operator === "<") {
|
|
584
|
+
items = items.filter(item => item[attributeName] < expectedValue);
|
|
585
|
+
} else if (operator === ">=") {
|
|
586
|
+
items = items.filter(item => item[attributeName] >= expectedValue);
|
|
587
|
+
} else if (operator === "<=") {
|
|
588
|
+
items = items.filter(item => item[attributeName] <= expectedValue);
|
|
589
|
+
} else if (operator === "BEGINS_WITH") {
|
|
590
|
+
const val = expectedValue;
|
|
591
|
+
items = items.filter(item => String(item[attributeName] || "").startsWith(String(val)));
|
|
566
592
|
}
|
|
567
|
-
}
|
|
593
|
+
}
|
|
568
594
|
}
|
|
569
595
|
}
|
|
570
596
|
|
|
@@ -578,7 +604,7 @@ class DynamoDBSimulator {
|
|
|
578
604
|
}
|
|
579
605
|
|
|
580
606
|
scan(params) {
|
|
581
|
-
const { TableName, FilterExpression, ExpressionAttributeValues, Limit } = params;
|
|
607
|
+
const { TableName, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames = {}, Limit } = params;
|
|
582
608
|
const table = this.tables.get(TableName);
|
|
583
609
|
|
|
584
610
|
if (!table) {
|
|
@@ -589,7 +615,7 @@ class DynamoDBSimulator {
|
|
|
589
615
|
|
|
590
616
|
// Aplica filtro se existir
|
|
591
617
|
if (FilterExpression) {
|
|
592
|
-
items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, table);
|
|
618
|
+
items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames, table);
|
|
593
619
|
}
|
|
594
620
|
|
|
595
621
|
// Aplica limite
|
|
@@ -606,6 +632,7 @@ class DynamoDBSimulator {
|
|
|
606
632
|
};
|
|
607
633
|
}
|
|
608
634
|
|
|
635
|
+
|
|
609
636
|
// Métodos auxiliares
|
|
610
637
|
normalizeItem(item, table) {
|
|
611
638
|
const normalized = { ...item };
|
|
@@ -720,21 +747,71 @@ class DynamoDBSimulator {
|
|
|
720
747
|
}
|
|
721
748
|
}
|
|
722
749
|
|
|
723
|
-
applyFilter(items, expression, values, table) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
if (
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
750
|
+
applyFilter(items, expression, values, names, table) {
|
|
751
|
+
if (!expression) return items;
|
|
752
|
+
|
|
753
|
+
const resolveAttributeName = (name) => {
|
|
754
|
+
if (name.startsWith("#")) return names[name] || name;
|
|
755
|
+
return name;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const resolveValue = (placeholder) => {
|
|
759
|
+
const rawValue = values[placeholder];
|
|
760
|
+
if (rawValue === undefined) return undefined;
|
|
761
|
+
if (rawValue !== null && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
|
|
762
|
+
const keys = Object.keys(rawValue);
|
|
763
|
+
if (keys.length === 1 && ["S", "N", "BOOL", "NULL", "M", "L", "SS", "NS", "BS"].includes(keys[0])) {
|
|
764
|
+
return this.normalizeValue(rawValue, table);
|
|
765
|
+
}
|
|
733
766
|
}
|
|
734
|
-
return
|
|
767
|
+
return rawValue;
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const conditions = expression.split(/\s+AND\s+/i);
|
|
771
|
+
|
|
772
|
+
return items.filter((item) => {
|
|
773
|
+
return conditions.every(cond => {
|
|
774
|
+
// Regex para match de funções como contains(#n, :v) ou begins_with(#n, :v)
|
|
775
|
+
const funcMatch = cond.match(/(contains|begins_with)\s*\(\s*([^\s,]+)\s*,\s*([^\s,)]+)\s*\)/i);
|
|
776
|
+
if (funcMatch) {
|
|
777
|
+
const func = funcMatch[1].toLowerCase();
|
|
778
|
+
const attrName = resolveAttributeName(funcMatch[2]);
|
|
779
|
+
const expectedVal = resolveValue(funcMatch[3]);
|
|
780
|
+
const actualVal = item[attrName];
|
|
781
|
+
|
|
782
|
+
if (func === 'contains') {
|
|
783
|
+
if (Array.isArray(actualVal)) return actualVal.includes(expectedVal);
|
|
784
|
+
return String(actualVal || "").includes(String(expectedVal));
|
|
785
|
+
}
|
|
786
|
+
if (func === 'begins_with') {
|
|
787
|
+
return String(actualVal || "").startsWith(String(expectedVal));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Regex para operadores básicos
|
|
792
|
+
const opMatch = cond.match(/([^\s]+)\s*(=|<>|<|<=|>|>=)\s*([^\s]+)/);
|
|
793
|
+
if (opMatch) {
|
|
794
|
+
const attrName = resolveAttributeName(opMatch[1]);
|
|
795
|
+
const operator = opMatch[2];
|
|
796
|
+
const expectedVal = resolveValue(opMatch[3]);
|
|
797
|
+
const actualVal = item[attrName];
|
|
798
|
+
|
|
799
|
+
switch (operator) {
|
|
800
|
+
case "=": return actualVal === expectedVal;
|
|
801
|
+
case "<>": return actualVal !== expectedVal;
|
|
802
|
+
case "<": return actualVal < expectedVal;
|
|
803
|
+
case "<=": return actualVal <= expectedVal;
|
|
804
|
+
case ">": return actualVal > expectedVal;
|
|
805
|
+
case ">=": return actualVal >= expectedVal;
|
|
806
|
+
default: return true;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return true;
|
|
810
|
+
});
|
|
735
811
|
});
|
|
736
812
|
}
|
|
737
813
|
|
|
814
|
+
|
|
738
815
|
persistTables() {
|
|
739
816
|
const tablesObj = {};
|
|
740
817
|
for (const [name, table] of this.tables.entries()) {
|
|
@@ -45,7 +45,21 @@ class KMSServer {
|
|
|
45
45
|
|
|
46
46
|
_setupRoutes() {
|
|
47
47
|
this.app.get('/__admin/health', (req, res) => res.json({ status: 'healthy', service: 'kms', timestamp: new Date().toISOString() }));
|
|
48
|
-
this.app.get('/__admin/keys', async (req, res) => {
|
|
48
|
+
this.app.get('/__admin/keys', async (req, res) => {
|
|
49
|
+
const keys = this.simulator.listKeysFull();
|
|
50
|
+
res.json(keys);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.app.post('/__admin/keys', async (req, res) => {
|
|
54
|
+
try {
|
|
55
|
+
const result = await this.simulator.createKey(req.body);
|
|
56
|
+
res.status(201).json(result);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
res.status(400).json({ __type: err.code || 'KMSInternalException', message: err.message });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
|
|
49
63
|
|
|
50
64
|
this.app.post('/', async (req, res) => {
|
|
51
65
|
const target = req.headers['x-amz-target'];
|