import assert, { AssertionError } from 'assert'; import { union, intersection } from 'lodash'; import { AbstractRepository, EntityManager, EntityNotFoundError, EntityRepository, getManager, } from 'typeorm'; import logger from '@/logger'; import { AggregateProduct } from '@/entities'; import { assertAreValidIdentifiers } from '../shared/identifiers'; import { AggregateProductData, AggregateProductDataWithIdentifiers, isValidAggregateProduct, } from './types'; import { mergeDuplicatesByIdentifiers } from './utils'; /** * Repository for the AggregateProduct entity. Primarily responsible for * handling identity resolution. */ @EntityRepository(AggregateProduct) // eslint-disable-next-line max-len export default class AggregateProductRepository extends AbstractRepository { async bulkFindOneByIdentifiers( input: { identifiers: string[] }[], manager: EntityManager = getManager() ): Promise<(AggregateProduct | null)[]> { if (input.some(({ identifiers }) => identifiers.length === 0)) { throw new AssertionError({ message: 'No identifiers specified when calling AggregateProductRepository.bulkFindOneByIdentifiers', expected: true, actual: false, }); } const allIdentifiers = input.reduce((prev, { identifiers }) => { return [...prev, ...identifiers]; }, new Array()); const products = await manager .createQueryBuilder(AggregateProduct, 'AggregateProduct') // finds any where a single identifier matches .where('AggregateProduct.identifiers && :identifiers', { identifiers: allIdentifiers, }) .getMany(); return input.map(({ identifiers }) => { const eligibleProducts = products.filter((product) => { return product.identifiers.some((identifier) => identifiers.includes(identifier) ); }); if (!eligibleProducts || eligibleProducts.length === 0) { return null; } if (eligibleProducts.length > 1) { logger.warn(`Found multiple aggregate products with identifiers`, { identifiers, results: eligibleProducts.map(({ id }) => id), }); } const matchingProduct = eligibleProducts[0]; const matchingIdentifiers = intersection( matchingProduct.identifiers, identifiers ); logger.info( `Found matching identifiers for AggregateProduct: ${matchingIdentifiers}` ); return matchingProduct; }); } async findOneByIdentifiers( { identifiers }: { identifiers: string[] }, manager: EntityManager = getManager() ): Promise { if (identifiers.length === 0) { logger.warn( 'No identifiers specified when calling AggregateProductRepository.findByIdentifiers' ); return undefined; } const results = await manager .createQueryBuilder(AggregateProduct, 'AggregateProduct') // finds any where a single identifier matches .where('AggregateProduct.identifiers && :identifiers', { identifiers, }) .limit(2) .getMany(); if (results.length > 1) { logger.warn(`Found multiple aggregate products with identifiers`, { identifiers, results: results.map(({ id }) => id), }); } return results[0]; } async findOneByIdentifiersOrFail( { identifiers }: { identifiers: string[] }, manager: EntityManager = getManager() ): Promise { const product = await this.findOneByIdentifiers({ identifiers }, manager); if (!product) { throw new EntityNotFoundError(AggregateProduct, { identifiers }); } return product; } /** * Upsert a AggregateProduct based on passed in `identifiers`. */ async upsert( { data, identifiers, }: { data: Partial; identifiers: string[]; }, manager: EntityManager = getManager() ): Promise<{ aggregateProductId: string; }> { assert(isValidAggregateProduct(data), 'Missing aggregate product data'); assertAreValidIdentifiers(identifiers); let existingProduct = await this.findOneByIdentifiers( { identifiers }, manager ); if (existingProduct) { existingProduct = Object.assign(existingProduct, { ...data, identifiers: union(existingProduct.identifiers, identifiers), imageUrls: union(existingProduct.imageUrls, data.imageUrls), }); const savedAggregateProduct = await manager.save(existingProduct); return { aggregateProductId: savedAggregateProduct.id, }; } const savedAggregateProduct = await manager.save( AggregateProduct, manager.create(AggregateProduct, { ...data, identifiers, }) ); return { aggregateProductId: savedAggregateProduct.id, }; } /** * Bulk upsert AggregateProducts */ async bulkUpsert( aggregateProducts: AggregateProductDataWithIdentifiers[], manager: EntityManager = getManager() ): Promise<{ aggregateProducts: { id: string; identifiers: string[] }[]; }> { const aggregateProductEntities: AggregateProduct[] = []; const aggregateProductsInput = mergeDuplicatesByIdentifiers(aggregateProducts); const input = aggregateProductsInput.map(({ identifiers }) => ({ identifiers, })); const products = await this.bulkFindOneByIdentifiers(input, manager); aggregateProductsInput.forEach(({ data, identifiers }, index) => { const product = products[index]; if (product) { aggregateProductEntities.push( Object.assign(product, { ...data, identifiers: union(product.identifiers, identifiers), imageUrls: union(product.imageUrls, data.imageUrls), }) ); } else { aggregateProductEntities.push( manager.create(AggregateProduct, { ...data, id: data.pid, identifiers, }) ); } }); const savedAggregateProducts = await manager.save( AggregateProduct, aggregateProductEntities ); return { aggregateProducts: savedAggregateProducts.map(({ id, identifiers }) => ({ id, identifiers, })), }; } }