@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.
@@ -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, ${this.resources.size} resources, ${this.methods.size} methods`);
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.raw({ type: '*/*', limit: '10mb' }));
22
- this.app.use((req, res, next) => {
23
- if (req.body && Buffer.isBuffer(req.body)) {
24
- try {
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(result);
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: new Date().toISOString(),
131
- LastModifiedDate: new Date().toISOString(),
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 = new Date().toISOString();
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: new Date().toISOString(),
428
- ExpiresAt: new Date(Date.now() + 3600000).toISOString(),
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: new Date().toISOString(),
511
- ExpiresAt: new Date(Date.now() + 3600000).toISOString(),
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: new Date().toISOString(),
713
- LastModifiedDate: new Date().toISOString(),
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: new Date().toISOString(),
768
- LastModifiedDate: new Date().toISOString(),
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: usa GSI se IndexName estiver presente, caso contrário usa a tabela principal
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
- // Filtra pela chave de partição
532
- const hashValueMatch = KeyConditionExpression.match(new RegExp(`${hashKey}\\s*=\\s*([^\\s]+)`));
533
-
534
- if (hashValueMatch) {
535
- const hashValuePlaceholder = hashValueMatch[1];
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
- if (rangeConditionMatch) {
546
- const operator = rangeConditionMatch[1];
547
- const rangeValuePlaceholder = rangeConditionMatch[2];
548
- const rawRangeValue = ExpressionAttributeValues[rangeValuePlaceholder];
549
- const rangeValue = rawRangeValue && typeof rawRangeValue === 'object' ? Object.values(rawRangeValue)[0] : rawRangeValue;
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
- items = items.filter((item) => {
552
- const itemValue = item[rangeKey];
553
- switch (operator) {
554
- case "=":
555
- return itemValue === rangeValue;
556
- case ">":
557
- return itemValue > rangeValue;
558
- case "<":
559
- return itemValue < rangeValue;
560
- case ">=":
561
- return itemValue >= rangeValue;
562
- case "<=":
563
- return itemValue <= rangeValue;
564
- default:
565
- return true;
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
- // Implementação simplificada
725
- return items.filter((item) => {
726
- const match = expression.match(/([^\s]+)\s*=\s*([^\s]+)/);
727
- if (match) {
728
- const [, attribute, placeholder] = match;
729
- const rawValue = values[placeholder];
730
- const expectedValue = rawValue && typeof rawValue === 'object' ? Object.values(rawValue)[0] : rawValue;
731
- const actualValue = item[attribute];
732
- return actualValue === expectedValue;
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 true;
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) => { const r = await this.simulator.listKeys({}); res.json(r); });
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'];