@deenruv/elasticsearch-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +122 -0
  3. package/lib/index.d.ts +3 -0
  4. package/lib/index.js +20 -0
  5. package/lib/index.js.map +1 -0
  6. package/lib/src/api/api-extensions.d.ts +3 -0
  7. package/lib/src/api/api-extensions.js +148 -0
  8. package/lib/src/api/api-extensions.js.map +1 -0
  9. package/lib/src/api/custom-mappings.resolver.d.ts +12 -0
  10. package/lib/src/api/custom-mappings.resolver.js +47 -0
  11. package/lib/src/api/custom-mappings.resolver.js.map +1 -0
  12. package/lib/src/api/custom-script-fields.resolver.d.ts +12 -0
  13. package/lib/src/api/custom-script-fields.resolver.js +50 -0
  14. package/lib/src/api/custom-script-fields.resolver.js.map +1 -0
  15. package/lib/src/api/elasticsearch-resolver.d.ts +34 -0
  16. package/lib/src/api/elasticsearch-resolver.js +150 -0
  17. package/lib/src/api/elasticsearch-resolver.js.map +1 -0
  18. package/lib/src/build-elastic-body.d.ts +8 -0
  19. package/lib/src/build-elastic-body.js +173 -0
  20. package/lib/src/build-elastic-body.js.map +1 -0
  21. package/lib/src/constants.d.ts +3 -0
  22. package/lib/src/constants.js +7 -0
  23. package/lib/src/constants.js.map +1 -0
  24. package/lib/src/elasticsearch.health.d.ts +9 -0
  25. package/lib/src/elasticsearch.health.js +52 -0
  26. package/lib/src/elasticsearch.health.js.map +1 -0
  27. package/lib/src/elasticsearch.service.d.ts +56 -0
  28. package/lib/src/elasticsearch.service.js +469 -0
  29. package/lib/src/elasticsearch.service.js.map +1 -0
  30. package/lib/src/indexing/elasticsearch-index.service.d.ts +24 -0
  31. package/lib/src/indexing/elasticsearch-index.service.js +163 -0
  32. package/lib/src/indexing/elasticsearch-index.service.js.map +1 -0
  33. package/lib/src/indexing/indexer.controller.d.ts +98 -0
  34. package/lib/src/indexing/indexer.controller.js +790 -0
  35. package/lib/src/indexing/indexer.controller.js.map +1 -0
  36. package/lib/src/indexing/indexing-utils.d.ts +8 -0
  37. package/lib/src/indexing/indexing-utils.js +109 -0
  38. package/lib/src/indexing/indexing-utils.js.map +1 -0
  39. package/lib/src/options.d.ts +695 -0
  40. package/lib/src/options.js +59 -0
  41. package/lib/src/options.js.map +1 -0
  42. package/lib/src/plugin.d.ts +192 -0
  43. package/lib/src/plugin.js +371 -0
  44. package/lib/src/plugin.js.map +1 -0
  45. package/lib/src/types.d.ts +262 -0
  46. package/lib/src/types.js +17 -0
  47. package/lib/src/types.js.map +1 -0
  48. package/package.json +45 -0
@@ -0,0 +1,790 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var ElasticsearchIndexerController_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.ElasticsearchIndexerController = exports.defaultVariantRelations = exports.defaultProductRelations = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const core_1 = require("@nestjs/core");
19
+ const unique_1 = require("@deenruv/common/lib/unique");
20
+ const core_2 = require("@deenruv/core");
21
+ const typeorm_1 = require("typeorm");
22
+ const constants_1 = require("../constants");
23
+ const indexing_utils_1 = require("./indexing-utils");
24
+ exports.defaultProductRelations = [
25
+ "featuredAsset",
26
+ "facetValues",
27
+ "facetValues.facet",
28
+ "channels",
29
+ "channels.defaultTaxZone",
30
+ ];
31
+ exports.defaultVariantRelations = [
32
+ "featuredAsset",
33
+ "facetValues",
34
+ "facetValues.facet",
35
+ "collections",
36
+ "taxCategory",
37
+ "channels",
38
+ "channels.defaultTaxZone",
39
+ ];
40
+ let ElasticsearchIndexerController = ElasticsearchIndexerController_1 = class ElasticsearchIndexerController {
41
+ constructor(connection, options, productPriceApplicator, configService, productVariantService, requestContextCache, moduleRef) {
42
+ this.connection = connection;
43
+ this.options = options;
44
+ this.productPriceApplicator = productPriceApplicator;
45
+ this.configService = configService;
46
+ this.productVariantService = productVariantService;
47
+ this.requestContextCache = requestContextCache;
48
+ this.moduleRef = moduleRef;
49
+ this.asyncQueue = new core_2.AsyncQueue("elasticsearch-indexer", 5);
50
+ }
51
+ onModuleInit() {
52
+ this.client = (0, indexing_utils_1.getClient)(this.options);
53
+ this.productRelations = this.getReindexRelations(exports.defaultProductRelations, this.options.hydrateProductRelations);
54
+ this.variantRelations = this.getReindexRelations(exports.defaultVariantRelations, this.options.hydrateProductVariantRelations);
55
+ this.injector = new core_2.Injector(this.moduleRef);
56
+ }
57
+ onModuleDestroy() {
58
+ return this.client.close();
59
+ }
60
+ /**
61
+ * Updates the search index only for the affected product.
62
+ */
63
+ async updateProduct({ ctx: rawContext, productId, }) {
64
+ const ctx = core_2.MutableRequestContext.deserialize(rawContext);
65
+ await this.updateProductsInternal(ctx, [productId]);
66
+ return true;
67
+ }
68
+ /**
69
+ * Updates the search index only for the affected product.
70
+ */
71
+ async deleteProduct({ ctx: rawContext, productId, }) {
72
+ await this.deleteProductOperations(core_2.RequestContext.deserialize(rawContext), productId);
73
+ return true;
74
+ }
75
+ /**
76
+ * Updates the search index only for the affected product.
77
+ */
78
+ async assignProductToChannel({ ctx: rawContext, productId, channelId, }) {
79
+ const ctx = core_2.MutableRequestContext.deserialize(rawContext);
80
+ await this.updateProductsInternal(ctx, [productId]);
81
+ return true;
82
+ }
83
+ /**
84
+ * Updates the search index only for the affected product.
85
+ */
86
+ async removeProductFromChannel({ ctx: rawContext, productId, channelId, }) {
87
+ const ctx = core_2.MutableRequestContext.deserialize(rawContext);
88
+ await this.updateProductsInternal(ctx, [productId]);
89
+ return true;
90
+ }
91
+ async assignVariantToChannel({ ctx: rawContext, productVariantId, channelId, }) {
92
+ const productIds = await this.getProductIdsByVariantIds([productVariantId]);
93
+ const ctx = core_2.MutableRequestContext.deserialize(rawContext);
94
+ await this.updateProductsInternal(ctx, productIds);
95
+ return true;
96
+ }
97
+ async removeVariantFromChannel({ ctx: rawContext, productVariantId, channelId, }) {
98
+ const productIds = await this.getProductIdsByVariantIds([productVariantId]);
99
+ const ctx = core_2.MutableRequestContext.deserialize(rawContext);
100
+ await this.updateProductsInternal(ctx, productIds);
101
+ return true;
102
+ }
103
+ /**
104
+ * Updates the search index only for the affected entities.
105
+ */
106
+ async updateVariants({ ctx: rawContext, variantIds, }) {
107
+ const ctx = core_2.MutableRequestContext.deserialize(rawContext);
108
+ return this.asyncQueue.push(async () => {
109
+ const productIds = await this.getProductIdsByVariantIds(variantIds);
110
+ await this.updateProductsInternal(ctx, productIds);
111
+ return true;
112
+ });
113
+ }
114
+ async deleteVariants({ ctx: rawContext, variantIds, }) {
115
+ const ctx = core_2.MutableRequestContext.deserialize(rawContext);
116
+ const productIds = await this.getProductIdsByVariantIds(variantIds);
117
+ for (const productId of productIds) {
118
+ await this.updateProductsInternal(ctx, [productId]);
119
+ }
120
+ return true;
121
+ }
122
+ updateVariantsById({ ctx: rawContext, ids, }) {
123
+ const ctx = core_2.MutableRequestContext.deserialize(rawContext);
124
+ return (0, core_2.asyncObservable)(async (observer) => {
125
+ return this.asyncQueue.push(async () => {
126
+ const timeStart = Date.now();
127
+ const productIds = await this.getProductIdsByVariantIds(ids);
128
+ if (productIds.length) {
129
+ let finishedProductsCount = 0;
130
+ for (const productId of productIds) {
131
+ await this.updateProductsInternal(ctx, [productId]);
132
+ finishedProductsCount++;
133
+ observer.next({
134
+ total: productIds.length,
135
+ completed: Math.min(finishedProductsCount, productIds.length),
136
+ duration: +new Date() - timeStart,
137
+ });
138
+ }
139
+ }
140
+ core_2.Logger.verbose("Completed updating variants", constants_1.loggerCtx);
141
+ return {
142
+ total: productIds.length,
143
+ completed: productIds.length,
144
+ duration: +new Date() - timeStart,
145
+ };
146
+ });
147
+ });
148
+ }
149
+ reindex({ ctx: rawContext, }) {
150
+ return (0, core_2.asyncObservable)(async (observer) => {
151
+ return this.asyncQueue.push(async () => {
152
+ const timeStart = Date.now();
153
+ const ctx = core_2.MutableRequestContext.deserialize(rawContext);
154
+ const reindexTempName = new Date().getTime();
155
+ const variantIndexName = `${this.options.indexPrefix}${constants_1.VARIANT_INDEX_NAME}`;
156
+ const variantIndexNameForReindex = `${constants_1.VARIANT_INDEX_NAME}-reindex-${reindexTempName}`;
157
+ const reindexVariantAliasName = `${this.options.indexPrefix}${variantIndexNameForReindex}`;
158
+ try {
159
+ await (0, indexing_utils_1.createIndices)(this.client, this.options.indexPrefix, this.options.indexSettings, this.options.indexMappingProperties, true, `-reindex-${reindexTempName}`);
160
+ }
161
+ catch (e) {
162
+ core_2.Logger.error("Could not recreate indices.", constants_1.loggerCtx);
163
+ core_2.Logger.error(JSON.stringify(e), constants_1.loggerCtx);
164
+ throw e;
165
+ }
166
+ const totalProductIds = await this.connection.rawConnection
167
+ .getRepository(core_2.Product)
168
+ .createQueryBuilder("product")
169
+ .where("product.deletedAt IS NULL")
170
+ .getCount();
171
+ core_2.Logger.verbose(`Will reindex ${totalProductIds} products`, constants_1.loggerCtx);
172
+ let productIds = [];
173
+ let skip = 0;
174
+ let finishedProductsCount = 0;
175
+ do {
176
+ productIds = await this.connection.rawConnection
177
+ .getRepository(core_2.Product)
178
+ .createQueryBuilder("product")
179
+ .select("product.id")
180
+ .where("product.deletedAt IS NULL")
181
+ .skip(skip)
182
+ .take(this.options.reindexProductsChunkSize)
183
+ .getMany();
184
+ for (const { id: productId } of productIds) {
185
+ await this.updateProductsOperationsOnly(ctx, productId, variantIndexNameForReindex);
186
+ finishedProductsCount++;
187
+ observer.next({
188
+ total: totalProductIds,
189
+ completed: Math.min(finishedProductsCount, totalProductIds),
190
+ duration: +new Date() - timeStart,
191
+ });
192
+ }
193
+ skip += this.options.reindexProductsChunkSize;
194
+ core_2.Logger.verbose(`Done ${finishedProductsCount} / ${totalProductIds} products`);
195
+ } while (productIds.length >= this.options.reindexProductsChunkSize);
196
+ // Switch the index to the new reindexed one
197
+ await this.switchAlias(reindexVariantAliasName, variantIndexName);
198
+ core_2.Logger.verbose("Completed reindexing!", constants_1.loggerCtx);
199
+ return {
200
+ total: totalProductIds,
201
+ completed: totalProductIds,
202
+ duration: +new Date() - timeStart,
203
+ };
204
+ });
205
+ });
206
+ }
207
+ async executeBulkOperationsByChunks(chunkSize, operations, index = constants_1.VARIANT_INDEX_NAME) {
208
+ core_2.Logger.verbose(`Will execute ${operations.length} bulk update operations with index ${index}`, constants_1.loggerCtx);
209
+ let i;
210
+ let j;
211
+ let processedOperation = 0;
212
+ for (i = 0, j = operations.length; i < j; i += chunkSize) {
213
+ const operationsChunks = operations.slice(i, i + chunkSize);
214
+ await this.executeBulkOperations(operationsChunks, index);
215
+ processedOperation += operationsChunks.length;
216
+ core_2.Logger.verbose(`Executing operation chunks ${processedOperation}/${operations.length}`, constants_1.loggerCtx);
217
+ }
218
+ }
219
+ async updateAsset(data) {
220
+ const result = await this.updateAssetFocalPointForIndex(constants_1.VARIANT_INDEX_NAME, data.asset);
221
+ await this.client.indices.refresh({
222
+ index: [this.options.indexPrefix + constants_1.VARIANT_INDEX_NAME],
223
+ });
224
+ return result;
225
+ }
226
+ async deleteAsset(data) {
227
+ const result = await this.deleteAssetForIndex(constants_1.VARIANT_INDEX_NAME, data.asset);
228
+ await this.client.indices.refresh({
229
+ index: [this.options.indexPrefix + constants_1.VARIANT_INDEX_NAME],
230
+ });
231
+ return result;
232
+ }
233
+ async updateAssetFocalPointForIndex(indexName, asset) {
234
+ const focalPoint = asset.focalPoint || null;
235
+ const params = { focalPoint };
236
+ return this.updateAssetForIndex(indexName, asset, {
237
+ source: "ctx._source.productPreviewFocalPoint = params.focalPoint",
238
+ params,
239
+ }, {
240
+ source: "ctx._source.productVariantPreviewFocalPoint = params.focalPoint",
241
+ params,
242
+ });
243
+ }
244
+ async deleteAssetForIndex(indexName, asset) {
245
+ return this.updateAssetForIndex(indexName, asset, { source: "ctx._source.productAssetId = null" }, { source: "ctx._source.productVariantAssetId = null" });
246
+ }
247
+ async updateAssetForIndex(indexName, asset, updateProductScript, updateVariantScript) {
248
+ const result1 = await this.client.update_by_query({
249
+ index: this.options.indexPrefix + indexName,
250
+ body: {
251
+ script: updateProductScript,
252
+ query: {
253
+ term: {
254
+ productAssetId: asset.id,
255
+ },
256
+ },
257
+ },
258
+ });
259
+ for (const failure of result1.body.failures) {
260
+ core_2.Logger.error(`${failure.cause.type}: ${failure.cause.reason}`, constants_1.loggerCtx);
261
+ }
262
+ const result2 = await this.client.update_by_query({
263
+ index: this.options.indexPrefix + indexName,
264
+ body: {
265
+ script: updateVariantScript,
266
+ query: {
267
+ term: {
268
+ productVariantAssetId: asset.id,
269
+ },
270
+ },
271
+ },
272
+ });
273
+ for (const failure of result1.body.failures) {
274
+ core_2.Logger.error(`${failure.cause.type}: ${failure.cause.reason}`, constants_1.loggerCtx);
275
+ }
276
+ return result1.body.failures.length === 0 && result2.body.failures === 0;
277
+ }
278
+ async updateProductsInternal(ctx, productIds) {
279
+ await this.updateProductsOperations(ctx, productIds);
280
+ }
281
+ async switchAlias(reindexVariantAliasName, variantIndexName) {
282
+ try {
283
+ const reindexVariantAliasExist = await this.client.indices.existsAlias({
284
+ name: reindexVariantAliasName,
285
+ });
286
+ if (reindexVariantAliasExist) {
287
+ const reindexVariantIndexName = await (0, indexing_utils_1.getIndexNameByAlias)(this.client, reindexVariantAliasName);
288
+ const originalVariantAliasExist = await this.client.indices.existsAlias({
289
+ name: variantIndexName,
290
+ });
291
+ const originalVariantIndexExist = await this.client.indices.exists({
292
+ index: variantIndexName,
293
+ });
294
+ const originalVariantIndexName = await (0, indexing_utils_1.getIndexNameByAlias)(this.client, variantIndexName);
295
+ const actions = [
296
+ {
297
+ remove: {
298
+ index: reindexVariantIndexName,
299
+ alias: reindexVariantAliasName,
300
+ },
301
+ },
302
+ {
303
+ add: {
304
+ index: reindexVariantIndexName,
305
+ alias: variantIndexName,
306
+ },
307
+ },
308
+ ];
309
+ if (originalVariantAliasExist.body) {
310
+ actions.push({
311
+ remove: {
312
+ index: originalVariantIndexName,
313
+ alias: variantIndexName,
314
+ },
315
+ });
316
+ }
317
+ else if (originalVariantIndexExist.body) {
318
+ await this.client.indices.delete({
319
+ index: [variantIndexName],
320
+ });
321
+ }
322
+ await this.client.indices.updateAliases({
323
+ body: {
324
+ actions,
325
+ },
326
+ });
327
+ if (originalVariantAliasExist.body) {
328
+ await this.client.indices.delete({
329
+ index: [originalVariantIndexName],
330
+ });
331
+ }
332
+ }
333
+ }
334
+ catch (e) {
335
+ core_2.Logger.error("Could not switch indexes");
336
+ }
337
+ finally {
338
+ const reindexVariantAliasExist = await this.client.indices.existsAlias({
339
+ name: reindexVariantAliasName,
340
+ });
341
+ if (reindexVariantAliasExist.body) {
342
+ const reindexVariantAliasResult = await this.client.indices.getAlias({
343
+ name: reindexVariantAliasName,
344
+ });
345
+ const reindexVariantIndexName = Object.keys(reindexVariantAliasResult.body)[0];
346
+ await this.client.indices.delete({
347
+ index: [reindexVariantIndexName],
348
+ });
349
+ }
350
+ }
351
+ }
352
+ async updateProductsOperationsOnly(ctx, productId, index = constants_1.VARIANT_INDEX_NAME) {
353
+ let operations = [];
354
+ let product;
355
+ try {
356
+ product = await this.connection
357
+ .getRepository(ctx, core_2.Product)
358
+ .find({
359
+ where: { id: productId, deletedAt: (0, typeorm_1.IsNull)() },
360
+ relations: this.productRelations,
361
+ })
362
+ .then((result) => { var _a; return (_a = result[0]) !== null && _a !== void 0 ? _a : undefined; });
363
+ }
364
+ catch (e) {
365
+ core_2.Logger.error(e.message, constants_1.loggerCtx, e.stack);
366
+ throw e;
367
+ }
368
+ if (!product) {
369
+ return;
370
+ }
371
+ let updatedProductVariants = [];
372
+ try {
373
+ updatedProductVariants = await this.connection.rawConnection
374
+ .getRepository(core_2.ProductVariant)
375
+ .find({
376
+ relations: this.variantRelations,
377
+ where: {
378
+ productId,
379
+ deletedAt: (0, typeorm_1.IsNull)(),
380
+ },
381
+ order: {
382
+ id: "ASC",
383
+ },
384
+ });
385
+ }
386
+ catch (e) {
387
+ core_2.Logger.error(e.message, constants_1.loggerCtx, e.stack);
388
+ }
389
+ updatedProductVariants.forEach((variant) => (variant.product = product));
390
+ if (!product.enabled) {
391
+ updatedProductVariants.forEach((v) => (v.enabled = false));
392
+ }
393
+ core_2.Logger.debug(`Updating Product (${productId})`, constants_1.loggerCtx);
394
+ const languageVariants = [];
395
+ languageVariants.push(...product.translations.map((t) => t.languageCode));
396
+ for (const variant of updatedProductVariants)
397
+ languageVariants.push(...variant.translations.map((t) => t.languageCode));
398
+ const uniqueLanguageVariants = (0, unique_1.unique)(languageVariants);
399
+ for (const channel of product.channels) {
400
+ ctx.setChannel(channel);
401
+ const variantsInChannel = updatedProductVariants.filter((v) => v.channels.map((c) => c.id).includes(ctx.channelId));
402
+ for (const variant of variantsInChannel)
403
+ await this.productPriceApplicator.applyChannelPriceAndTax(variant, ctx);
404
+ for (const languageCode of uniqueLanguageVariants) {
405
+ if (variantsInChannel.length) {
406
+ for (const variant of variantsInChannel) {
407
+ operations.push({
408
+ index: constants_1.VARIANT_INDEX_NAME,
409
+ operation: {
410
+ update: {
411
+ _id: ElasticsearchIndexerController_1.getId(variant.id, ctx.channelId, languageCode),
412
+ },
413
+ },
414
+ }, {
415
+ index: constants_1.VARIANT_INDEX_NAME,
416
+ operation: {
417
+ doc: await this.createVariantIndexItem(variant, variantsInChannel, ctx, languageCode),
418
+ doc_as_upsert: true,
419
+ },
420
+ });
421
+ if (operations.length >= this.options.reindexBulkOperationSizeLimit) {
422
+ // Because we can have a huge amount of variant for 1 product, we also chunk update operations
423
+ await this.executeBulkOperationsByChunks(this.options.reindexBulkOperationSizeLimit, operations, index);
424
+ operations = [];
425
+ }
426
+ }
427
+ }
428
+ else {
429
+ operations.push({
430
+ index: constants_1.VARIANT_INDEX_NAME,
431
+ operation: {
432
+ update: {
433
+ _id: ElasticsearchIndexerController_1.getId(-product.id, ctx.channelId, languageCode),
434
+ },
435
+ },
436
+ }, {
437
+ index: constants_1.VARIANT_INDEX_NAME,
438
+ operation: {
439
+ doc: await this.createSyntheticProductIndexItem(product, ctx, languageCode),
440
+ doc_as_upsert: true,
441
+ },
442
+ });
443
+ }
444
+ if (operations.length >= this.options.reindexBulkOperationSizeLimit) {
445
+ // Because we can have a huge amount of variant for 1 product, we also chunk update operations
446
+ await this.executeBulkOperationsByChunks(this.options.reindexBulkOperationSizeLimit, operations, index);
447
+ operations = [];
448
+ }
449
+ }
450
+ }
451
+ // Because we can have a huge amount of variant for 1 product, we also chunk update operations
452
+ await this.executeBulkOperationsByChunks(this.options.reindexBulkOperationSizeLimit, operations, index);
453
+ return;
454
+ }
455
+ async updateProductsOperations(ctx, productIds) {
456
+ core_2.Logger.debug(`Updating ${productIds.length} Products`, constants_1.loggerCtx);
457
+ for (const productId of productIds) {
458
+ await this.deleteProductOperations(ctx, productId);
459
+ await this.updateProductsOperationsOnly(ctx, productId);
460
+ }
461
+ return;
462
+ }
463
+ /**
464
+ * Takes the default relations, and combines them with any extra relations specified in the
465
+ * `hydrateProductRelations` and `hydrateProductVariantRelations`. This method also ensures
466
+ * that the relation values are unique and that paths are fully expanded.
467
+ *
468
+ * This means that if a `hydrateProductRelations` value of `['assets.asset']` is specified,
469
+ * this method will also add `['assets']` to the relations array, otherwise TypeORM would
470
+ * throw an error trying to join a 2nd-level deep relation without the first level also
471
+ * being joined.
472
+ */
473
+ getReindexRelations(defaultRelations, hydratedRelations) {
474
+ const uniqueRelations = (0, unique_1.unique)([...defaultRelations, ...hydratedRelations]);
475
+ for (const relation of hydratedRelations) {
476
+ let path = relation.split(".");
477
+ if (path[0] === "customFields") {
478
+ if (path.length > 2) {
479
+ throw new core_2.InternalServerError([
480
+ "hydrateProductRelations / hydrateProductVariantRelations does not currently support nested custom field relations",
481
+ `Received: "${relation}"`,
482
+ ].join("\n"));
483
+ }
484
+ path = [path.join(".")];
485
+ }
486
+ const pathToPart = [];
487
+ for (const part of path) {
488
+ pathToPart.push(part);
489
+ const joinedPath = pathToPart.join(".");
490
+ if (!uniqueRelations.includes(joinedPath)) {
491
+ uniqueRelations.push(joinedPath);
492
+ }
493
+ }
494
+ }
495
+ return uniqueRelations;
496
+ }
497
+ async deleteProductOperations(ctx, productId, index = constants_1.VARIANT_INDEX_NAME) {
498
+ const channels = await this.requestContextCache.get(ctx, "elastic-index-all-channels", () => this.connection.rawConnection
499
+ .getRepository(core_2.Channel)
500
+ .createQueryBuilder("channel")
501
+ .select("channel.id")
502
+ .getMany());
503
+ const product = await this.connection
504
+ .getRepository(ctx, core_2.Product)
505
+ .createQueryBuilder("product")
506
+ .select([
507
+ "product.id",
508
+ "productVariant.id",
509
+ "productTranslations.languageCode",
510
+ "productVariantTranslations.languageCode",
511
+ ])
512
+ .leftJoin("product.translations", "productTranslations")
513
+ .leftJoin("product.variants", "productVariant")
514
+ .leftJoin("productVariant.translations", "productVariantTranslations")
515
+ .leftJoin("product.channels", "channel")
516
+ .where("product.id = :productId", { productId })
517
+ .andWhere("channel.id = :channelId", { channelId: ctx.channelId })
518
+ .getOne();
519
+ if (!product)
520
+ return;
521
+ core_2.Logger.debug(`Deleting 1 Product (id: ${productId})`, constants_1.loggerCtx);
522
+ let operations = [];
523
+ const languageVariants = [];
524
+ languageVariants.push(...product.translations.map((t) => t.languageCode));
525
+ for (const variant of product.variants)
526
+ languageVariants.push(...variant.translations.map((t) => t.languageCode));
527
+ const uniqueLanguageVariants = (0, unique_1.unique)(languageVariants);
528
+ for (const { id: channelId } of channels) {
529
+ for (const languageCode of uniqueLanguageVariants) {
530
+ operations.push({
531
+ index: constants_1.VARIANT_INDEX_NAME,
532
+ operation: {
533
+ delete: {
534
+ _id: ElasticsearchIndexerController_1.getId(-product.id, channelId, languageCode),
535
+ },
536
+ },
537
+ });
538
+ if (operations.length >= this.options.reindexBulkOperationSizeLimit) {
539
+ // Because we can have a huge amount of variant for 1 product, we also chunk update operations
540
+ await this.executeBulkOperationsByChunks(this.options.reindexBulkOperationSizeLimit, operations, index);
541
+ operations = [];
542
+ }
543
+ }
544
+ }
545
+ // Because we can have a huge amount of variant for 1 product, we also chunk update operations
546
+ await this.executeBulkOperationsByChunks(this.options.reindexBulkOperationSizeLimit, operations, index);
547
+ await this.deleteVariantsInternalOperations(product.variants, channels.map((c) => c.id), uniqueLanguageVariants, index);
548
+ return;
549
+ }
550
+ async deleteVariantsInternalOperations(variants, channelIds, languageVariants, index = constants_1.VARIANT_INDEX_NAME) {
551
+ core_2.Logger.debug(`Deleting ${variants.length} ProductVariants`, constants_1.loggerCtx);
552
+ let operations = [];
553
+ for (const variant of variants) {
554
+ for (const channelId of channelIds) {
555
+ for (const languageCode of languageVariants) {
556
+ operations.push({
557
+ index: constants_1.VARIANT_INDEX_NAME,
558
+ operation: {
559
+ delete: {
560
+ _id: ElasticsearchIndexerController_1.getId(variant.id, channelId, languageCode),
561
+ },
562
+ },
563
+ });
564
+ if (operations.length >= this.options.reindexBulkOperationSizeLimit) {
565
+ // Because we can have a huge amount of variant for 1 product, we also chunk update operations
566
+ await this.executeBulkOperationsByChunks(this.options.reindexBulkOperationSizeLimit, operations, index);
567
+ operations = [];
568
+ }
569
+ }
570
+ }
571
+ }
572
+ // Because we can have a huge amount of variant for 1 product, we also chunk update operations
573
+ await this.executeBulkOperationsByChunks(this.options.reindexBulkOperationSizeLimit, operations, index);
574
+ return;
575
+ }
576
+ async getProductIdsByVariantIds(variantIds) {
577
+ const variants = await this.connection.getRepository(core_2.ProductVariant).find({
578
+ where: { id: (0, typeorm_1.In)(variantIds) },
579
+ relations: ["product"],
580
+ loadEagerRelations: false,
581
+ });
582
+ return (0, unique_1.unique)(variants.map((v) => v.product.id));
583
+ }
584
+ async executeBulkOperations(operations, indexName = constants_1.VARIANT_INDEX_NAME) {
585
+ const variantOperations = [];
586
+ for (const operation of operations) {
587
+ variantOperations.push(operation.operation);
588
+ }
589
+ return Promise.all([
590
+ this.runBulkOperationsOnIndex(indexName, variantOperations),
591
+ ]);
592
+ }
593
+ async runBulkOperationsOnIndex(indexName, operations) {
594
+ var _a;
595
+ if (operations.length === 0) {
596
+ return;
597
+ }
598
+ try {
599
+ const fullIndexName = this.options.indexPrefix + indexName;
600
+ const { body } = await this.client.bulk({
601
+ refresh: true,
602
+ index: fullIndexName,
603
+ body: operations,
604
+ });
605
+ if (body.errors) {
606
+ core_2.Logger.error(`Some errors occurred running bulk operations on ${fullIndexName}! Set logger to "debug" to print all errors.`, constants_1.loggerCtx);
607
+ body.items.forEach((item) => {
608
+ if (item.index) {
609
+ core_2.Logger.debug(JSON.stringify(item.index.error, null, 2), constants_1.loggerCtx);
610
+ }
611
+ if (item.update) {
612
+ core_2.Logger.debug(JSON.stringify(item.update.error, null, 2), constants_1.loggerCtx);
613
+ }
614
+ if (item.delete) {
615
+ core_2.Logger.debug(JSON.stringify(item.delete.error, null, 2), constants_1.loggerCtx);
616
+ }
617
+ });
618
+ }
619
+ else {
620
+ core_2.Logger.debug(`Executed ${body.items.length} bulk operations on index [${fullIndexName}]`, constants_1.loggerCtx);
621
+ }
622
+ return body;
623
+ }
624
+ catch (e) {
625
+ core_2.Logger.error(`Error when attempting to run bulk operations [${JSON.stringify(e)}]`, constants_1.loggerCtx);
626
+ core_2.Logger.error("Error details: " + JSON.stringify((_a = e.body) === null || _a === void 0 ? void 0 : _a.error, null, 2), constants_1.loggerCtx);
627
+ }
628
+ }
629
+ async createVariantIndexItem(v, variants, ctx, languageCode) {
630
+ try {
631
+ const productAsset = v.product.featuredAsset;
632
+ const variantAsset = v.featuredAsset;
633
+ const productTranslation = this.getTranslation(v.product, languageCode);
634
+ const variantTranslation = this.getTranslation(v, languageCode);
635
+ const collectionTranslations = v.collections.map((c) => this.getTranslation(c, languageCode));
636
+ const productCollectionTranslations = variants.reduce((translations, variant) => [
637
+ ...translations,
638
+ ...variant.collections.map((c) => this.getTranslation(c, languageCode)),
639
+ ], []);
640
+ const prices = variants.map((variant) => variant.price);
641
+ const pricesWithTax = variants.map((variant) => variant.priceWithTax);
642
+ const item = {
643
+ channelId: ctx.channelId,
644
+ languageCode,
645
+ productVariantId: v.id,
646
+ sku: v.sku,
647
+ slug: productTranslation.slug,
648
+ productId: v.product.id,
649
+ productName: productTranslation.name,
650
+ productAssetId: productAsset ? productAsset.id : undefined,
651
+ productPreview: productAsset ? productAsset.preview : "",
652
+ productPreviewFocalPoint: productAsset
653
+ ? productAsset.focalPoint || undefined
654
+ : undefined,
655
+ productVariantName: variantTranslation.name,
656
+ productVariantAssetId: variantAsset ? variantAsset.id : undefined,
657
+ productVariantPreview: variantAsset ? variantAsset.preview : "",
658
+ productVariantPreviewFocalPoint: variantAsset
659
+ ? variantAsset.focalPoint || undefined
660
+ : undefined,
661
+ price: v.price,
662
+ priceWithTax: v.priceWithTax,
663
+ currencyCode: v.currencyCode,
664
+ description: productTranslation.description,
665
+ facetIds: this.getFacetIds([v]),
666
+ channelIds: v.channels.map((c) => c.id),
667
+ facetValueIds: this.getFacetValueIds([v]),
668
+ collectionIds: v.collections.map((c) => c.id.toString()),
669
+ collectionSlugs: collectionTranslations.map((c) => c.slug),
670
+ enabled: v.enabled && v.product.enabled,
671
+ productEnabled: variants.some((variant) => variant.enabled) && v.product.enabled,
672
+ productPriceMin: Math.min(...prices),
673
+ productPriceMax: Math.max(...prices),
674
+ productPriceWithTaxMin: Math.min(...pricesWithTax),
675
+ productPriceWithTaxMax: Math.max(...pricesWithTax),
676
+ productFacetIds: this.getFacetIds(variants),
677
+ productFacetValueIds: this.getFacetValueIds(variants),
678
+ productCollectionIds: (0, unique_1.unique)(variants.reduce((ids, variant) => [...ids, ...variant.collections.map((c) => c.id)], [])),
679
+ productCollectionSlugs: (0, unique_1.unique)(productCollectionTranslations.map((c) => c.slug)),
680
+ productChannelIds: v.product.channels.map((c) => c.id),
681
+ inStock: 0 < (await this.productVariantService.getSaleableStockLevel(ctx, v)),
682
+ productInStock: await this.getProductInStockValue(ctx, variants),
683
+ };
684
+ const variantCustomMappings = Object.entries(this.options.customProductVariantMappings);
685
+ for (const [name, def] of variantCustomMappings) {
686
+ item[`variant-${name}`] = await def.valueFn(v, languageCode, this.injector, ctx);
687
+ }
688
+ const productCustomMappings = Object.entries(this.options.customProductMappings);
689
+ for (const [name, def] of productCustomMappings) {
690
+ item[`product-${name}`] = await def.valueFn(v.product, variants, languageCode, this.injector, ctx);
691
+ }
692
+ return item;
693
+ }
694
+ catch (err) {
695
+ core_2.Logger.error(err.toString());
696
+ throw Error("Error while reindexing!");
697
+ }
698
+ }
699
+ async getProductInStockValue(ctx, variants) {
700
+ return this.requestContextCache.get(ctx, `elastic-index-product-in-stock-${variants.map((v) => v.id).join(",")}`, async () => {
701
+ const stockLevels = await Promise.all(variants.map((variant) => this.productVariantService.getSaleableStockLevel(ctx, variant)));
702
+ return stockLevels.some((stockLevel) => 0 < stockLevel);
703
+ });
704
+ }
705
+ /**
706
+ * If a Product has no variants, we create a synthetic variant for the purposes
707
+ * of making that product visible via the search query.
708
+ */
709
+ async createSyntheticProductIndexItem(product, ctx, languageCode) {
710
+ var _a, _b, _c, _d, _e, _f, _g, _h;
711
+ const productTranslation = this.getTranslation(product, languageCode);
712
+ const productAsset = product.featuredAsset;
713
+ const item = {
714
+ channelId: ctx.channelId,
715
+ languageCode,
716
+ productVariantId: 0,
717
+ sku: "",
718
+ slug: productTranslation.slug,
719
+ productId: product.id,
720
+ productName: productTranslation.name,
721
+ productAssetId: productAsset ? productAsset.id : undefined,
722
+ productPreview: productAsset ? productAsset.preview : "",
723
+ productPreviewFocalPoint: productAsset
724
+ ? productAsset.focalPoint || undefined
725
+ : undefined,
726
+ productVariantName: productTranslation.name,
727
+ productVariantAssetId: undefined,
728
+ productVariantPreview: "",
729
+ productVariantPreviewFocalPoint: undefined,
730
+ price: 0,
731
+ priceWithTax: 0,
732
+ currencyCode: ctx.currencyCode,
733
+ description: productTranslation.description,
734
+ facetIds: (_b = (_a = product.facetValues) === null || _a === void 0 ? void 0 : _a.map((fv) => fv.facet.id.toString())) !== null && _b !== void 0 ? _b : [],
735
+ channelIds: [ctx.channelId],
736
+ facetValueIds: (_d = (_c = product.facetValues) === null || _c === void 0 ? void 0 : _c.map((fv) => fv.id.toString())) !== null && _d !== void 0 ? _d : [],
737
+ collectionIds: [],
738
+ collectionSlugs: [],
739
+ enabled: false,
740
+ productEnabled: false,
741
+ productPriceMin: 0,
742
+ productPriceMax: 0,
743
+ productPriceWithTaxMin: 0,
744
+ productPriceWithTaxMax: 0,
745
+ productFacetIds: (_f = (_e = product.facetValues) === null || _e === void 0 ? void 0 : _e.map((fv) => fv.facet.id.toString())) !== null && _f !== void 0 ? _f : [],
746
+ productFacetValueIds: (_h = (_g = product.facetValues) === null || _g === void 0 ? void 0 : _g.map((fv) => fv.id.toString())) !== null && _h !== void 0 ? _h : [],
747
+ productCollectionIds: [],
748
+ productCollectionSlugs: [],
749
+ productChannelIds: product.channels.map((c) => c.id),
750
+ inStock: false,
751
+ productInStock: false,
752
+ };
753
+ const productCustomMappings = Object.entries(this.options.customProductMappings);
754
+ for (const [name, def] of productCustomMappings) {
755
+ item[`product-${name}`] = await def.valueFn(product, [], languageCode, this.injector, ctx);
756
+ }
757
+ return item;
758
+ }
759
+ getTranslation(translatable, languageCode) {
760
+ return (translatable.translations.find((t) => t.languageCode === languageCode) ||
761
+ translatable.translations.find((t) => t.languageCode === this.configService.defaultLanguageCode) ||
762
+ translatable.translations[0]);
763
+ }
764
+ getFacetIds(variants) {
765
+ const facetIds = (fv) => fv.facet.id.toString();
766
+ const variantFacetIds = variants.reduce((ids, v) => [...ids, ...v.facetValues.map(facetIds)], []);
767
+ const productFacetIds = variants[0].product.facetValues.map(facetIds);
768
+ return (0, unique_1.unique)([...variantFacetIds, ...productFacetIds]);
769
+ }
770
+ getFacetValueIds(variants) {
771
+ const facetValueIds = (fv) => fv.id.toString();
772
+ const variantFacetValueIds = variants.reduce((ids, v) => [...ids, ...v.facetValues.map(facetValueIds)], []);
773
+ const productFacetValueIds = variants[0].product.facetValues.map(facetValueIds);
774
+ return (0, unique_1.unique)([...variantFacetValueIds, ...productFacetValueIds]);
775
+ }
776
+ static getId(entityId, channelId, languageCode) {
777
+ return `${channelId.toString()}_${entityId.toString()}_${languageCode}`;
778
+ }
779
+ };
780
+ exports.ElasticsearchIndexerController = ElasticsearchIndexerController;
781
+ exports.ElasticsearchIndexerController = ElasticsearchIndexerController = ElasticsearchIndexerController_1 = __decorate([
782
+ (0, common_1.Injectable)(),
783
+ __param(1, (0, common_1.Inject)(constants_1.ELASTIC_SEARCH_OPTIONS)),
784
+ __metadata("design:paramtypes", [core_2.TransactionalConnection, Object, core_2.ProductPriceApplicator,
785
+ core_2.ConfigService,
786
+ core_2.ProductVariantService,
787
+ core_2.RequestContextCacheService,
788
+ core_1.ModuleRef])
789
+ ], ElasticsearchIndexerController);
790
+ //# sourceMappingURL=indexer.controller.js.map