index-builder.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { Connection, ConnectionOptions, createConnection, SelectQueryBuilder } from 'typeorm';
  2. import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils';
  3. import { ID, Type } from '../../../../../common/lib/shared-types';
  4. import { unique } from '../../../../../common/lib/unique';
  5. import { RequestContext } from '../../../api/common/request-context';
  6. import { FacetValue } from '../../../entity/facet-value/facet-value.entity';
  7. import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
  8. import { SearchIndexItem } from '../search-index-item.entity';
  9. import { CompletedMessage, ConnectedMessage, Message, MessageType, ReturnRawBatchMessage, SaveVariantsPayload, VariantsSavedMessage } from './ipc';
  10. export const BATCH_SIZE = 500;
  11. export const variantRelations = [
  12. 'product',
  13. 'product.featuredAsset',
  14. 'product.facetValues',
  15. 'product.facetValues.facet',
  16. 'featuredAsset',
  17. 'facetValues',
  18. 'facetValues.facet',
  19. 'collections',
  20. 'taxCategory',
  21. ];
  22. export function getSearchIndexQueryBuilder(connection: Connection) {
  23. const qb = connection.getRepository(ProductVariant).createQueryBuilder('variants');
  24. FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, {
  25. relations: variantRelations,
  26. });
  27. FindOptionsUtils.joinEagerRelations(qb, qb.alias, connection.getMetadata(ProductVariant));
  28. return qb;
  29. }
  30. /**
  31. * This class is responsible for all updates to the search index.
  32. */
  33. export class IndexBuilder {
  34. private connection: Connection;
  35. private indexQueryBuilder: SelectQueryBuilder<ProductVariant>;
  36. private onMessageHandlers = new Set<(message: string) => void>();
  37. /**
  38. * When running in the main process, it should be constructed with the existing connection.
  39. * Otherwise, the connection will be created in the .connect() method in response to an
  40. * IPC message.
  41. */
  42. constructor(connection?: Connection) {
  43. if (connection) {
  44. this.connection = connection;
  45. this.indexQueryBuilder = getSearchIndexQueryBuilder(this.connection);
  46. }
  47. }
  48. processMessage(message: Message): Promise<Message | undefined> {
  49. switch (message.type) {
  50. case MessageType.CONNECTION_OPTIONS: {
  51. return this.connect(message.value);
  52. }
  53. case MessageType.GET_RAW_BATCH: {
  54. return this.getRawBatch(message.value.batchNumber);
  55. }
  56. case MessageType.GET_RAW_BATCH_BY_IDS: {
  57. return this.getRawBatchByIds(message.value.ids);
  58. }
  59. case MessageType.SAVE_VARIANTS: {
  60. return this.saveVariants(message.value);
  61. }
  62. default:
  63. return Promise.resolve(undefined);
  64. }
  65. }
  66. async processMessageAndEmitResult(message: Message) {
  67. const result = await this.processMessage(message);
  68. if (result) {
  69. result.channelId = message.channelId;
  70. this.onMessageHandlers.forEach(handler => {
  71. handler(JSON.stringify(result));
  72. });
  73. }
  74. }
  75. addMessageListener<T extends Message>(handler: (message: string) => void) {
  76. this.onMessageHandlers.add(handler);
  77. }
  78. removeMessageListener<T extends Message>(handler: (message: string) => void) {
  79. this.onMessageHandlers.delete(handler);
  80. }
  81. private async connect(dbConnectionOptions: ConnectionOptions): Promise<ConnectedMessage> {
  82. const {coreEntitiesMap} = await import('../../../entity/entities');
  83. const coreEntities = Object.values(coreEntitiesMap) as Array<Type<any>>;
  84. this.connection = await createConnection({...dbConnectionOptions, entities: [SearchIndexItem, ...coreEntities]});
  85. this.indexQueryBuilder = getSearchIndexQueryBuilder(this.connection);
  86. return new ConnectedMessage(this.connection.isConnected);
  87. }
  88. private async getRawBatchByIds(ids: ID[]): Promise<ReturnRawBatchMessage> {
  89. const variants = await this.connection.getRepository(ProductVariant).findByIds(ids, {
  90. relations: variantRelations,
  91. });
  92. return new ReturnRawBatchMessage({variants});
  93. }
  94. private async getRawBatch(batchNumber: string | number): Promise<ReturnRawBatchMessage> {
  95. const i = Number.parseInt(batchNumber.toString(), 10);
  96. const variants = await this.indexQueryBuilder
  97. .where('variants__product.deletedAt IS NULL')
  98. .take(BATCH_SIZE)
  99. .skip(i * BATCH_SIZE)
  100. .getMany();
  101. return new ReturnRawBatchMessage({variants});
  102. }
  103. private async saveVariants(payload: SaveVariantsPayload): Promise<VariantsSavedMessage | CompletedMessage> {
  104. const {variants, ctx, batch, total} = payload;
  105. const requestContext = new RequestContext(ctx);
  106. const items = variants.map((v: ProductVariant) =>
  107. new SearchIndexItem({
  108. sku: v.sku,
  109. enabled: v.enabled,
  110. slug: v.product.slug,
  111. price: v.price,
  112. priceWithTax: v.priceWithTax,
  113. languageCode: requestContext.languageCode,
  114. productVariantId: v.id,
  115. productId: v.product.id,
  116. productName: v.product.name,
  117. description: v.product.description,
  118. productVariantName: v.name,
  119. productPreview: v.product.featuredAsset ? v.product.featuredAsset.preview : '',
  120. productVariantPreview: v.featuredAsset ? v.featuredAsset.preview : '',
  121. facetIds: this.getFacetIds(v),
  122. facetValueIds: this.getFacetValueIds(v),
  123. collectionIds: v.collections.map(c => c.id.toString()),
  124. }),
  125. );
  126. await this.connection.getRepository(SearchIndexItem).save(items);
  127. if (batch === total - 1) {
  128. return new CompletedMessage(true);
  129. } else {
  130. return new VariantsSavedMessage({batchNumber: batch});
  131. }
  132. }
  133. private getFacetIds(variant: ProductVariant): string[] {
  134. const facetIds = (fv: FacetValue) => fv.facet.id.toString();
  135. const variantFacetIds = variant.facetValues.map(facetIds);
  136. const productFacetIds = variant.product.facetValues.map(facetIds);
  137. return unique([...variantFacetIds, ...productFacetIds]);
  138. }
  139. private getFacetValueIds(variant: ProductVariant): string[] {
  140. const facetValueIds = (fv: FacetValue) => fv.id.toString();
  141. const variantFacetValueIds = variant.facetValues.map(facetValueIds);
  142. const productFacetValueIds = variant.product.facetValues.map(facetValueIds);
  143. return unique([...variantFacetValueIds, ...productFacetValueIds]);
  144. }
  145. }