import { AssertionError } from 'assert'; import { intersection, union } from 'lodash'; import { AbstractRepository, EntityManager, EntityNotFoundError, EntityRepository, getCustomRepository, getManager, In, } from 'typeorm'; import { RetailerProductVariant } from '@/entities'; import { RetailerProductSource } from '@/entities/types'; import logger from '@/logger'; import { AggregateProductVariantRepository, RetailerProductRepository, } from '..'; import { assertAreValidIdentifiers } from '../shared/identifiers'; import { FindOneByIdentifiersOptions, RetailerProductVariantDataWithIdentifiers, } from './types'; import { mergeDuplicatesByVariantAndProductIds } from './utils'; /** * Repository for the RetailerProductVariant entity. Primarily responsible for * handling identity resolution. */ @EntityRepository(RetailerProductVariant) // eslint-disable-next-line max-len export default class RetailerProductVariantRepository extends AbstractRepository { async bulkFindOneByIdentifiers( input: FindOneByIdentifiersOptions[], manager: EntityManager = getManager() ): Promise<(RetailerProductVariant | null)[]> { if (input.some(({ identifiers }) => identifiers.length === 0)) { throw new AssertionError({ message: 'No identifiers specified when calling RetailerProductVariantRepository.bulkFindOneByIdentifiers', expected: true, actual: false, }); } const allIdentifiers = input.reduce((prev, { identifiers }) => { return [...prev, ...identifiers]; }, new Array()); const allRetailers = input.reduce((prev, { retailer }) => { return [...prev, retailer]; }, new Array()); const retailerProductIds = input.reduce((prev, { retailerProductId }) => { return [...prev, retailerProductId]; }, new Array()); const products = await manager .createQueryBuilder(RetailerProductVariant, 'RetailerProductVariant') // finds any where a single identifier matches .where('RetailerProductVariant.identifiers && :identifiers', { identifiers: allIdentifiers, }) // and where retailer is in the retailer list .andWhere('RetailerProductVariant.retailer IN (:...retailers)', { retailers: allRetailers, }) // and where productId is in the retailer list .andWhere({ productId: In(retailerProductIds), }) .getMany(); const retailerToProductsMap = new Map(); products.forEach((product) => { retailerToProductsMap.set(product.retailer, [ ...(retailerToProductsMap.get(product.retailer) || []), product, ]); }); return input.map(({ identifiers, retailer, retailerProductId }) => { const eligibleProducts = retailerToProductsMap .get(retailer) ?.filter((product) => { return ( retailerProductId === product.productId && product.identifiers.some((identifier) => identifiers.includes(identifier) ) ); }); if (!eligibleProducts || eligibleProducts.length === 0) { return null; } // in event that multiple are present, we could consider warning, but // this is the same behavior as findOneByIdentifiers return eligibleProducts[0]; }); } async findOneByIdentifiers( { identifiers, retailerProductId }: FindOneByIdentifiersOptions, manager: EntityManager = getManager() ): Promise { const ids = identifiers.filter((id) => id !== 'unknown'); if (ids.length === 0) { logger.warn( 'No identifiers specified when calling RetailerProductVariantRepository.findByIdentifiers' ); return undefined; } return ( manager .createQueryBuilder(RetailerProductVariant, 'RetailerProductVariant') // finds any where a single identifier matches .where('RetailerProductVariant.identifiers && :identifiers', { identifiers: ids, }) .andWhere({ productId: retailerProductId, }) .getOne() ); } async findOneByIdentifiersOrFail( options: FindOneByIdentifiersOptions, manager: EntityManager = getManager() ): Promise { const productVariant = await this.findOneByIdentifiers(options, manager); if (!productVariant) { throw new EntityNotFoundError(RetailerProductVariant, options); } return productVariant; } /** * Upsert a RetailerProductVariant based on passed in `identifiers`. */ async upsert( { data, productIdentifiers, variantIdentifiers, retailer, source, }: RetailerProductVariantDataWithIdentifiers & { source?: RetailerProductSource; }, manager: EntityManager = getManager() ): Promise<{ retailerProductVariantId: string; retailerProductId: string; aggregateProductVariantId: string; aggregateProductId: string; }> { assertAreValidIdentifiers(productIdentifiers); assertAreValidIdentifiers(variantIdentifiers); const { retailerProductId } = await getCustomRepository( RetailerProductRepository ).upsert( { data, productIdentifiers, retailer, source, }, manager ); const { aggregateProductId, aggregateProductVariantId } = await getCustomRepository(AggregateProductVariantRepository).upsert( { data, productIdentifiers, variantIdentifiers }, manager ); let existingRetailerProductVariant = await this.findOneByIdentifiers( { identifiers: variantIdentifiers, retailerProductId, retailer }, manager ); if (existingRetailerProductVariant) { existingRetailerProductVariant = Object.assign( existingRetailerProductVariant, { ...data, productId: retailerProductId, variantId: aggregateProductVariantId, source, identifiers: union( existingRetailerProductVariant.identifiers, variantIdentifiers ), imageUrls: union( existingRetailerProductVariant.imageUrls, data.imageUrls ), } ); const savedRetailerProductVariant = await manager.save( existingRetailerProductVariant ); return { retailerProductVariantId: savedRetailerProductVariant.id, retailerProductId, aggregateProductVariantId, aggregateProductId, }; } const savedRetailerProductVariant = await manager.save( RetailerProductVariant, manager.create(RetailerProductVariant, { ...data, source, identifiers: variantIdentifiers, productId: retailerProductId, variantId: aggregateProductVariantId, retailer, }) ); return { retailerProductVariantId: savedRetailerProductVariant.id, retailerProductId, aggregateProductVariantId, aggregateProductId, }; } /** * Bulk upsert RetailerProductVariants */ async bulkUpsert( { data, source, }: { data: RetailerProductVariantDataWithIdentifiers[]; source?: RetailerProductSource; }, manager: EntityManager = getManager() ): Promise<{ retailerProductVariantIds: string[]; retailerProductIds: string[]; aggregateProductVariantIds: string[]; aggregateProductIds: string[]; }> { const retailerProductVariantsInput = mergeDuplicatesByVariantAndProductIds(data); const { retailerProducts } = await getCustomRepository( RetailerProductRepository ).bulkUpsert( { source, data: retailerProductVariantsInput.map( ({ productIdentifiers, retailer, data }) => ({ productIdentifiers, retailer, data, }) ), }, manager ); const { aggregateProductVariants, aggregateProducts } = await getCustomRepository(AggregateProductVariantRepository).bulkUpsert( retailerProductVariantsInput, manager ); const input = retailerProductVariantsInput .map(({ productIdentifiers: pids, variantIdentifiers, retailer }) => { const retailerProductId = retailerProducts.find( ({ identifiers: productIdentifiers }) => intersection(pids, productIdentifiers).length > 0 )?.id; return { retailerProductId, identifiers: variantIdentifiers, retailer, }; }) .filter( (value): value is FindOneByIdentifiersOptions => !!value.retailerProductId ); const bulkRetailerProductVariants = await this.bulkFindOneByIdentifiers( input, manager ); const retailerProductVariantEntities: RetailerProductVariant[] = bulkRetailerProductVariants.filter( (product): product is RetailerProductVariant => !!product ); retailerProductVariantsInput.forEach( ({ data, variantIdentifiers, productIdentifiers, retailer }, index) => { const retailerProductVariant = bulkRetailerProductVariants[index]; const productId = retailerProducts.find( ({ identifiers }) => intersection(identifiers, productIdentifiers).length > 0 )?.id; const variantId = aggregateProductVariants.find( ({ identifiers }) => intersection(identifiers, variantIdentifiers).length > 0 )?.id; if (!productId) { logger.warn( 'Could not find productId when attempting to create new retailer product variants', { data, } ); } if (!variantId) { logger.warn( 'Could not find variantId when attempting to create new retailer product variants', { data, } ); } if (retailerProductVariant) { retailerProductVariantEntities.push( Object.assign(retailerProductVariant, { ...data, productId, variantId, source, identifiers: union( retailerProductVariant.identifiers, variantIdentifiers ), imageUrls: union( retailerProductVariant.imageUrls, data.imageUrls ), }) ); } else { retailerProductVariantEntities.push( manager.create(RetailerProductVariant, { ...data, source, productId, variantId, identifiers: variantIdentifiers, retailer, }) ); } } ); const savedRetailerProductVariants = await manager.save( RetailerProductVariant, retailerProductVariantEntities ); return { aggregateProductIds: aggregateProducts.map(({ id }) => id), aggregateProductVariantIds: aggregateProductVariants.map(({ id }) => id), retailerProductIds: retailerProducts.map(({ id }) => id), retailerProductVariantIds: savedRetailerProductVariants.map( ({ id }) => id ), }; } }