@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.
- package/README.md +662 -658
- package/package.json +11 -4
- 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/handler-loader.js +50 -5
- package/src/services/lambda/index.js +26 -0
- package/src/services/lambda/server.js +18 -12
- package/src/services/lambda/simulator.js +73 -17
- 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,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenSearch Simulator Core - Simulador Elasticsearch/OpenSearch
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const Index = require('./models/index');
|
|
6
|
+
const Document = require('./models/document');
|
|
7
|
+
const OpenSearchStore = require('./store');
|
|
8
|
+
const logger = require('../../utils/logger');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
class OpenSearchSimulator {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
const dataDir = process.env.AWS_LOCAL_SIMULATOR_DATA_DIR || config.dataDir || './.aws-local-simulator-data';
|
|
15
|
+
this.dataDir = path.join(dataDir, 'opensearch');
|
|
16
|
+
|
|
17
|
+
this.store = new OpenSearchStore(dataDir);
|
|
18
|
+
this.indices = new Map();
|
|
19
|
+
this.indexCache = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async initialize() {
|
|
23
|
+
logger.debug('Inicializando OpenSearch Simulator...');
|
|
24
|
+
|
|
25
|
+
// Carrega metadados persistidos
|
|
26
|
+
this.loadIndices();
|
|
27
|
+
|
|
28
|
+
// Cria índices da configuração
|
|
29
|
+
if (this.config.opensearch?.indices) {
|
|
30
|
+
for (const indexConfig of this.config.opensearch.indices) {
|
|
31
|
+
if (!this.indices.has(indexConfig.name)) {
|
|
32
|
+
this.createIndex(indexConfig.name, indexConfig);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger.debug(`✅ OpenSearch Simulator inicializado com ${this.indices.size} índices`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Carrega metadados de índices do disco
|
|
42
|
+
*/
|
|
43
|
+
loadIndices() {
|
|
44
|
+
const metadata = this.store.loadMetadata();
|
|
45
|
+
|
|
46
|
+
for (const [indexName, indexDef] of Object.entries(metadata)) {
|
|
47
|
+
const index = new Index(indexName, indexDef.settings);
|
|
48
|
+
index.createdAt = indexDef.createdAt;
|
|
49
|
+
index.docsCount = indexDef.docsCount || 0;
|
|
50
|
+
index.storeSize = indexDef.storeSize || 0;
|
|
51
|
+
this.indices.set(indexName, index);
|
|
52
|
+
|
|
53
|
+
logger.debug(`Índice carregado: ${indexName}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Persiste metadados de índices
|
|
59
|
+
*/
|
|
60
|
+
persistIndices() {
|
|
61
|
+
const metadata = {};
|
|
62
|
+
|
|
63
|
+
for (const [indexName, index] of this.indices) {
|
|
64
|
+
metadata[indexName] = {
|
|
65
|
+
name: index.name,
|
|
66
|
+
settings: index.settings,
|
|
67
|
+
mappings: index.mappings,
|
|
68
|
+
aliases: index.aliases,
|
|
69
|
+
createdAt: index.createdAt,
|
|
70
|
+
docsCount: index.docsCount,
|
|
71
|
+
storeSize: index.storeSize
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.store.saveMetadata(metadata);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ========== Operações de Índice ==========
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cria um novo índice
|
|
82
|
+
*/
|
|
83
|
+
createIndex(name, config = {}) {
|
|
84
|
+
if (this.indices.has(name)) {
|
|
85
|
+
throw new Error(`Index '${name}' already exists`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const index = new Index(name, config);
|
|
89
|
+
this.indices.set(name, index);
|
|
90
|
+
this.persistIndices();
|
|
91
|
+
|
|
92
|
+
logger.info(`✅ Índice criado: ${name}`);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
acknowledged: true,
|
|
96
|
+
shards_acknowledged: true,
|
|
97
|
+
index: name
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Deleta um índice
|
|
103
|
+
*/
|
|
104
|
+
deleteIndex(name) {
|
|
105
|
+
if (!this.indices.has(name)) {
|
|
106
|
+
throw new Error(`Index '${name}' does not exist`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.indices.delete(name);
|
|
110
|
+
this.indexCache.delete(name);
|
|
111
|
+
this.store.deleteIndex(name);
|
|
112
|
+
this.persistIndices();
|
|
113
|
+
|
|
114
|
+
logger.info(`🗑️ Índice deletado: ${name}`);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
acknowledged: true
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Obtém informações de um índice
|
|
123
|
+
*/
|
|
124
|
+
getIndex(name) {
|
|
125
|
+
const index = this.indices.get(name);
|
|
126
|
+
if (!index) {
|
|
127
|
+
throw new Error(`Index '${name}' does not exist`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Atualiza tamanho
|
|
131
|
+
index.updateSize(this.store.getIndexSize(name));
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
[name]: index.toJSON()
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Lista todos os índices
|
|
140
|
+
*/
|
|
141
|
+
listIndices() {
|
|
142
|
+
const indices = [];
|
|
143
|
+
|
|
144
|
+
for (const [name, index] of this.indices) {
|
|
145
|
+
index.updateSize(this.store.getIndexSize(name));
|
|
146
|
+
indices.push({
|
|
147
|
+
name: name,
|
|
148
|
+
status: index.status,
|
|
149
|
+
docs: index.docsCount,
|
|
150
|
+
size_mb: (index.storeSize / 1024 / 1024).toFixed(2),
|
|
151
|
+
created_at: index.createdAt
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return indices;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Abre um índice (simples - todos começam abertos)
|
|
160
|
+
*/
|
|
161
|
+
openIndex(name) {
|
|
162
|
+
const index = this.indices.get(name);
|
|
163
|
+
if (!index) throw new Error(`Index '${name}' does not exist`);
|
|
164
|
+
|
|
165
|
+
index.status = 'open';
|
|
166
|
+
this.persistIndices();
|
|
167
|
+
|
|
168
|
+
return { acknowledged: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Fecha um índice
|
|
173
|
+
*/
|
|
174
|
+
closeIndex(name) {
|
|
175
|
+
const index = this.indices.get(name);
|
|
176
|
+
if (!index) throw new Error(`Index '${name}' does not exist`);
|
|
177
|
+
|
|
178
|
+
index.status = 'closed';
|
|
179
|
+
this.persistIndices();
|
|
180
|
+
|
|
181
|
+
return { acknowledged: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ========== Operações de Documento ==========
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Indexa um documento (PUT/POST)
|
|
188
|
+
*/
|
|
189
|
+
indexDocument(indexName, document, documentId = null) {
|
|
190
|
+
if (!this.indices.has(indexName)) {
|
|
191
|
+
throw new Error(`Index '${indexName}' does not exist`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Cria objeto Document
|
|
195
|
+
const doc = new Document(document, {
|
|
196
|
+
_id: documentId || document._id,
|
|
197
|
+
_index: indexName,
|
|
198
|
+
_type: '_doc'
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Carrega documentos existentes
|
|
202
|
+
let documents = this.store.loadDocuments(indexName);
|
|
203
|
+
const existingIndex = documents.findIndex(d => d._id === doc._id);
|
|
204
|
+
|
|
205
|
+
const isNew = existingIndex === -1;
|
|
206
|
+
if (isNew) {
|
|
207
|
+
documents.push(doc.toJSON());
|
|
208
|
+
this.indices.get(indexName).addDocument();
|
|
209
|
+
} else {
|
|
210
|
+
documents[existingIndex] = doc.toJSON();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Salva
|
|
214
|
+
this.store.saveDocuments(indexName, documents);
|
|
215
|
+
this.indexCache.delete(indexName);
|
|
216
|
+
|
|
217
|
+
logger.verboso(`Documento ${isNew ? 'criado' : 'atualizado'}: ${indexName}/${doc._id}`);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
_index: indexName,
|
|
221
|
+
_type: '_doc',
|
|
222
|
+
_id: doc._id,
|
|
223
|
+
_version: doc._version,
|
|
224
|
+
result: isNew ? 'created' : 'updated',
|
|
225
|
+
_shards: { total: 2, successful: 1, failed: 0 },
|
|
226
|
+
_seq_no: doc._seq_no,
|
|
227
|
+
_primary_term: doc._primary_term
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Obtém um documento por ID
|
|
233
|
+
*/
|
|
234
|
+
getDocument(indexName, documentId) {
|
|
235
|
+
if (!this.indices.has(indexName)) {
|
|
236
|
+
throw new Error(`Index '${indexName}' does not exist`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const documents = this.store.loadDocuments(indexName);
|
|
240
|
+
const doc = documents.find(d => d._id === documentId);
|
|
241
|
+
|
|
242
|
+
if (!doc) {
|
|
243
|
+
return {
|
|
244
|
+
_index: indexName,
|
|
245
|
+
_type: '_doc',
|
|
246
|
+
_id: documentId,
|
|
247
|
+
found: false
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
logger.verboso(`Documento encontrado: ${indexName}/${documentId}`);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
_index: indexName,
|
|
255
|
+
_type: '_doc',
|
|
256
|
+
_id: documentId,
|
|
257
|
+
_version: doc._version,
|
|
258
|
+
_seq_no: doc._seq_no,
|
|
259
|
+
_primary_term: doc._primary_term,
|
|
260
|
+
found: true,
|
|
261
|
+
_source: doc._source
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Deleta um documento
|
|
267
|
+
*/
|
|
268
|
+
deleteDocument(indexName, documentId) {
|
|
269
|
+
if (!this.indices.has(indexName)) {
|
|
270
|
+
throw new Error(`Index '${indexName}' does not exist`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const deleted = this.store.removeDocument(indexName, documentId);
|
|
274
|
+
|
|
275
|
+
if (deleted) {
|
|
276
|
+
this.indices.get(indexName).removeDocument();
|
|
277
|
+
this.indexCache.delete(indexName);
|
|
278
|
+
logger.verboso(`Documento deletado: ${indexName}/${documentId}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
_index: indexName,
|
|
283
|
+
_type: '_doc',
|
|
284
|
+
_id: documentId,
|
|
285
|
+
_version: 2,
|
|
286
|
+
result: deleted ? 'deleted' : 'not_found',
|
|
287
|
+
_shards: { total: 2, successful: 1, failed: 0 },
|
|
288
|
+
_seq_no: 1,
|
|
289
|
+
_primary_term: 1
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Busca documentos com query
|
|
295
|
+
*/
|
|
296
|
+
search(indexName, queryParams = {}) {
|
|
297
|
+
if (!this.indices.has(indexName)) {
|
|
298
|
+
throw new Error(`Index '${indexName}' does not exist`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let documents = this.store.loadDocuments(indexName);
|
|
302
|
+
const {
|
|
303
|
+
query = {},
|
|
304
|
+
from = 0,
|
|
305
|
+
size = 10,
|
|
306
|
+
sort = [],
|
|
307
|
+
highlight = false
|
|
308
|
+
} = queryParams;
|
|
309
|
+
|
|
310
|
+
let results = documents;
|
|
311
|
+
|
|
312
|
+
// Aplicar query filter
|
|
313
|
+
if (query.match) {
|
|
314
|
+
const [field, value] = Object.entries(query.match)[0];
|
|
315
|
+
results = results.filter(doc => {
|
|
316
|
+
const fieldValue = doc._source[field];
|
|
317
|
+
if (!fieldValue) return false;
|
|
318
|
+
return String(fieldValue).toLowerCase().includes(String(value).toLowerCase());
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (query.bool) {
|
|
323
|
+
// Suporte básico para bool queries
|
|
324
|
+
if (query.bool.must) {
|
|
325
|
+
for (const mustClause of query.bool.must) {
|
|
326
|
+
if (mustClause.match) {
|
|
327
|
+
const [field, value] = Object.entries(mustClause.match)[0];
|
|
328
|
+
results = results.filter(doc => {
|
|
329
|
+
const fieldValue = doc._source[field];
|
|
330
|
+
if (!fieldValue) return false;
|
|
331
|
+
return String(fieldValue).toLowerCase().includes(String(value).toLowerCase());
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (query.bool.should) {
|
|
338
|
+
// OR logic
|
|
339
|
+
const shouldMatches = [];
|
|
340
|
+
for (const shouldClause of query.bool.should) {
|
|
341
|
+
if (shouldClause.match) {
|
|
342
|
+
const [field, value] = Object.entries(shouldClause.match)[0];
|
|
343
|
+
shouldMatches.push(
|
|
344
|
+
results.filter(doc => {
|
|
345
|
+
const fieldValue = doc._source[field];
|
|
346
|
+
if (!fieldValue) return false;
|
|
347
|
+
return String(fieldValue).toLowerCase().includes(String(value).toLowerCase());
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
results = [...new Map(shouldMatches.flat().map(item => [item._id, item])).values()];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (query.query_string) {
|
|
357
|
+
// Full-text search
|
|
358
|
+
const text = query.query_string.query.toLowerCase();
|
|
359
|
+
results = results.filter(doc => {
|
|
360
|
+
for (const value of Object.values(doc._source)) {
|
|
361
|
+
if (String(value).toLowerCase().includes(text)) return true;
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Aplicar sorting
|
|
368
|
+
if (sort.length > 0) {
|
|
369
|
+
for (const sortSpec of sort) {
|
|
370
|
+
if (typeof sortSpec === 'string') {
|
|
371
|
+
results.sort((a, b) => {
|
|
372
|
+
const aVal = a._source[sortSpec];
|
|
373
|
+
const bVal = b._source[sortSpec];
|
|
374
|
+
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
375
|
+
});
|
|
376
|
+
} else if (typeof sortSpec === 'object') {
|
|
377
|
+
const [field, direction] = Object.entries(sortSpec)[0];
|
|
378
|
+
results.sort((a, b) => {
|
|
379
|
+
const aVal = a._source[field];
|
|
380
|
+
const bVal = b._source[field];
|
|
381
|
+
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
382
|
+
return direction.order === 'desc' ? -cmp : cmp;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Paginação
|
|
389
|
+
const total = results.length;
|
|
390
|
+
const hits = results.slice(from, from + size);
|
|
391
|
+
|
|
392
|
+
logger.verboso(`Busca em ${indexName}: ${total} resultados, retornando ${hits.length}`);
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
took: 5,
|
|
396
|
+
timed_out: false,
|
|
397
|
+
_shards: { total: 5, successful: 5, skipped: 0, failed: 0 },
|
|
398
|
+
hits: {
|
|
399
|
+
total: { value: total, relation: 'eq' },
|
|
400
|
+
max_score: hits.length > 0 ? 1.0 : null,
|
|
401
|
+
hits: hits.map(doc => ({
|
|
402
|
+
_index: indexName,
|
|
403
|
+
_type: '_doc',
|
|
404
|
+
_id: doc._id,
|
|
405
|
+
_score: 1.0,
|
|
406
|
+
_source: doc._source
|
|
407
|
+
}))
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Busca agregada (simples)
|
|
414
|
+
*/
|
|
415
|
+
aggregate(indexName, aggs = {}) {
|
|
416
|
+
if (!this.indices.has(indexName)) {
|
|
417
|
+
throw new Error(`Index '${indexName}' does not exist`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const documents = this.store.loadDocuments(indexName);
|
|
421
|
+
const result = { aggregations: {} };
|
|
422
|
+
|
|
423
|
+
for (const [aggName, aggSpec] of Object.entries(aggs)) {
|
|
424
|
+
if (aggSpec.terms) {
|
|
425
|
+
// Agregação por termos
|
|
426
|
+
const field = aggSpec.terms.field;
|
|
427
|
+
const buckets = {};
|
|
428
|
+
|
|
429
|
+
documents.forEach(doc => {
|
|
430
|
+
const value = doc._source[field];
|
|
431
|
+
if (value) {
|
|
432
|
+
buckets[value] = (buckets[value] || 0) + 1;
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
result.aggregations[aggName] = {
|
|
437
|
+
buckets: Object.entries(buckets)
|
|
438
|
+
.map(([key, count]) => ({ key, doc_count: count }))
|
|
439
|
+
.sort((a, b) => b.doc_count - a.doc_count)
|
|
440
|
+
.slice(0, aggSpec.terms.size || 10)
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (aggSpec.avg || aggSpec.sum) {
|
|
445
|
+
// Agregação numérica
|
|
446
|
+
const spec = aggSpec.avg || aggSpec.sum;
|
|
447
|
+
const field = spec.field;
|
|
448
|
+
const values = documents.map(doc => Number(doc._source[field])).filter(v => !isNaN(v));
|
|
449
|
+
|
|
450
|
+
if (aggSpec.avg) {
|
|
451
|
+
result.aggregations[aggName] = {
|
|
452
|
+
value: values.length > 0 ? values.reduce((a, b) => a + b) / values.length : 0
|
|
453
|
+
};
|
|
454
|
+
} else {
|
|
455
|
+
result.aggregations[aggName] = {
|
|
456
|
+
value: values.reduce((a, b) => a + b, 0)
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Atualiza um documento por query
|
|
467
|
+
*/
|
|
468
|
+
updateByQuery(indexName, updateScript, query = {}) {
|
|
469
|
+
if (!this.indices.has(indexName)) {
|
|
470
|
+
throw new Error(`Index '${indexName}' does not exist`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let documents = this.store.loadDocuments(indexName);
|
|
474
|
+
let updated = 0;
|
|
475
|
+
|
|
476
|
+
documents = documents.map(doc => {
|
|
477
|
+
let matches = true;
|
|
478
|
+
|
|
479
|
+
if (Object.keys(query).length > 0) {
|
|
480
|
+
for (const [field, value] of Object.entries(query)) {
|
|
481
|
+
if (doc._source[field] !== value) {
|
|
482
|
+
matches = false;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (matches && updateScript) {
|
|
489
|
+
// Simples: executar update script
|
|
490
|
+
doc._source = { ...doc._source, ...updateScript };
|
|
491
|
+
doc._version++;
|
|
492
|
+
doc._updatedAt = new Date().toISOString();
|
|
493
|
+
updated++;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return doc;
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
this.store.saveDocuments(indexName, documents);
|
|
500
|
+
this.indexCache.delete(indexName);
|
|
501
|
+
|
|
502
|
+
logger.info(`Atualizado ${updated} documentos em ${indexName}`);
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
took: 10,
|
|
506
|
+
timed_out: false,
|
|
507
|
+
total: updated,
|
|
508
|
+
updated: updated,
|
|
509
|
+
deleted: 0,
|
|
510
|
+
batches: 1,
|
|
511
|
+
version_conflicts: 0,
|
|
512
|
+
noops: 0,
|
|
513
|
+
retries: { bulk: 0, search: 0 },
|
|
514
|
+
throttled_millis: 0,
|
|
515
|
+
requests_per_second: -1.0,
|
|
516
|
+
throttled_until_millis: 0,
|
|
517
|
+
failures: []
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Deleta documentos por query
|
|
523
|
+
*/
|
|
524
|
+
deleteByQuery(indexName, query = {}) {
|
|
525
|
+
if (!this.indices.has(indexName)) {
|
|
526
|
+
throw new Error(`Index '${indexName}' does not exist`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let documents = this.store.loadDocuments(indexName);
|
|
530
|
+
let deleted = 0;
|
|
531
|
+
|
|
532
|
+
documents = documents.filter(doc => {
|
|
533
|
+
let matches = true;
|
|
534
|
+
|
|
535
|
+
for (const [field, value] of Object.entries(query)) {
|
|
536
|
+
if (doc._source[field] !== value) {
|
|
537
|
+
matches = false;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (matches) {
|
|
543
|
+
deleted++;
|
|
544
|
+
this.indices.get(indexName).removeDocument();
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return true;
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
this.store.saveDocuments(indexName, documents);
|
|
552
|
+
this.indexCache.delete(indexName);
|
|
553
|
+
|
|
554
|
+
logger.info(`Deletado ${deleted} documentos de ${indexName}`);
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
took: 10,
|
|
558
|
+
timed_out: false,
|
|
559
|
+
total: deleted,
|
|
560
|
+
deleted: deleted,
|
|
561
|
+
batches: 1,
|
|
562
|
+
version_conflicts: 0,
|
|
563
|
+
noops: 0,
|
|
564
|
+
retries: { bulk: 0, search: 0 },
|
|
565
|
+
throttled_millis: 0,
|
|
566
|
+
requests_per_second: -1.0,
|
|
567
|
+
throttled_until_millis: 0,
|
|
568
|
+
failures: []
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Retorna estatísticas de cluster
|
|
574
|
+
*/
|
|
575
|
+
getClusterStats() {
|
|
576
|
+
let totalDocs = 0;
|
|
577
|
+
let totalSize = 0;
|
|
578
|
+
|
|
579
|
+
for (const index of this.indices.values()) {
|
|
580
|
+
totalDocs += index.docsCount;
|
|
581
|
+
totalSize += index.storeSize;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
status: 'green',
|
|
586
|
+
indices: this.indices.size,
|
|
587
|
+
shards: this.indices.size * 2,
|
|
588
|
+
replicas: this.indices.size,
|
|
589
|
+
docs: totalDocs,
|
|
590
|
+
size_mb: (totalSize / 1024 / 1024).toFixed(2)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Reset - limpa tudo
|
|
596
|
+
*/
|
|
597
|
+
async reset() {
|
|
598
|
+
this.indices.clear();
|
|
599
|
+
this.indexCache.clear();
|
|
600
|
+
this.store.clear();
|
|
601
|
+
logger.info('🔄 OpenSearch resetado');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
module.exports = OpenSearchSimulator;
|