import { AssertionError } from 'assert'; import { intersection, union } from 'lodash'; import { AbstractRepository, EntityManager, EntityNotFoundError, EntityRepository, getCustomRepository, getManager, In, } from 'typeorm'; import logger from '@/logger'; import { AggregateProductVariant } from '@/entities'; import { AggregateProductRepository } from '..'; import { assertAreValidIdentifiers } from '../shared/identifiers'; import { AggregateProductVariantDataWithIdentifiers, FindOneByIdentifiersOptions, } from './types'; import { mergeDuplicatesByIdentifiers } from './utils'; /** * Repository for the AggregateProductVariant entity. Primarily responsible for * handling identity resolution. */ @EntityRepository(AggregateProductVariant) // eslint-disable-next-line max-len export default class AggregateProductVariantRepository extends AbstractRepository { async bulkFindOneByIdentifiers( input: FindOneByIdentifiersOptions[], manager: EntityManager = getManager() ): Promise<(AggregateProductVariant | null)[]> { if (input.some(({ identifiers }) => identifiers.length === 0)) { throw new AssertionError({ message: 'No identifiers specified when calling AggregateProductVariantRepository.bulkFindOneByIdentifiers', expected: true, actual: false, }); } const allIdentifiers = Array.from( new Set( input.reduce((prev, { identifiers }) => { return [...prev, ...identifiers]; }, new Array()) ) ); const aggregateProductIds = Array.from( new Set( input.reduce((prev, { aggregateProductId }) => { return [...prev, aggregateProductId]; }, new Array()) ) ); const products = await manager .createQueryBuilder(AggregateProductVariant, 'AggregateProductVariant') // finds any where a single identifier matches .where('AggregateProductVariant.identifiers && :identifiers', { identifiers: allIdentifiers, }) .andWhere({ productId: In(aggregateProductIds), }) .getMany(); return input.map(({ identifiers, aggregateProductId }) => { const eligibleProducts = products.filter((product) => { return product.identifiers.some( (identifier) => identifiers.includes(identifier) && product.productId === aggregateProductId ); }); if (!eligibleProducts || eligibleProducts.length === 0) { return null; } if (eligibleProducts.length > 1) { logger.warn( `Found multiple aggregate product variants with identifiers`, { identifiers, results: eligibleProducts.map(({ id }) => id), } ); } // in event that multiple are present, we could consider warning, but // this is the same behavior as findOneByIdentifiers return eligibleProducts[0]; }); } async findOneByIdentifiers( { identifiers, aggregateProductId, }: { aggregateProductId: string; identifiers: string[] }, manager: EntityManager = getManager() ): Promise { const ids = identifiers.filter((id) => id !== 'unknown'); if (ids.length === 0) { logger.warn( 'No identifiers specified when calling AggregateProductVariantRepository.findByIdentifiers' ); return undefined; } const results = await manager .createQueryBuilder(AggregateProductVariant, 'AggregateProductVariant') // finds any where a single identifier matches .where('AggregateProductVariant.identifiers && :identifiers', { identifiers: ids, }) .andWhere({ productId: aggregateProductId, }) .limit(2) .getMany(); if (results.length > 1) { logger.warn( `Found multiple aggregate product variants with identifiers`, { identifiers, results: results.map(({ id }) => id), } ); } return results[0]; } async findOneByIdentifiersOrFail( { aggregateProductId, identifiers }: FindOneByIdentifiersOptions, manager: EntityManager = getManager() ): Promise { const productVariant = await this.findOneByIdentifiers( { aggregateProductId, identifiers }, manager ); if (!productVariant) { throw new EntityNotFoundError(AggregateProductVariant, { identifiers }); } return productVariant; } /** * Upsert AggregateProductVariant based on passed in `identifiers`. */ async upsert( { data, productIdentifiers, variantIdentifiers, }: AggregateProductVariantDataWithIdentifiers, manager: EntityManager = getManager() ): Promise<{ aggregateProductVariantId: string; aggregateProductId: string; }> { assertAreValidIdentifiers(productIdentifiers); const { aggregateProductId } = await getCustomRepository( AggregateProductRepository ).upsert({ data, identifiers: productIdentifiers }, manager); let existingAggregateProductVariant = await this.findOneByIdentifiers( { identifiers: variantIdentifiers, aggregateProductId }, manager ); if (existingAggregateProductVariant) { existingAggregateProductVariant = Object.assign( existingAggregateProductVariant, { ...data, productId: aggregateProductId, identifiers: union( existingAggregateProductVariant.identifiers, variantIdentifiers ), imageUrls: union( existingAggregateProductVariant.imageUrls, data.imageUrls ), } ); const savedAggregateProductVariant = await manager.save( existingAggregateProductVariant ); return { aggregateProductId, aggregateProductVariantId: savedAggregateProductVariant.id, }; } const savedAggregateProductVariant = await manager.save( AggregateProductVariant, manager.create(AggregateProductVariant, { ...data, productId: aggregateProductId, identifiers: variantIdentifiers, }) ); return { aggregateProductId, aggregateProductVariantId: savedAggregateProductVariant.id, }; } async bulkUpsert( data: AggregateProductVariantDataWithIdentifiers[], manager: EntityManager = getManager() ): Promise<{ aggregateProductVariants: { identifiers: string[]; id: string }[]; aggregateProducts: { identifiers: string[]; id: string }[]; }> { const mergedAggregateProductVariantsData = mergeDuplicatesByIdentifiers(data); const aggregateProductVariantEntities: AggregateProductVariant[] = []; const { aggregateProducts } = await getCustomRepository( AggregateProductRepository ).bulkUpsert( mergedAggregateProductVariantsData.map( ({ data, productIdentifiers }) => ({ identifiers: productIdentifiers, data, }) ), manager ); const input = mergedAggregateProductVariantsData .map(({ variantIdentifiers: identifiers, productIdentifiers: pids }) => { const aggregateProductId = aggregateProducts.find( ({ identifiers: productIdentifiers }) => intersection(pids, productIdentifiers).length > 0 )?.id; return { aggregateProductId, identifiers, }; }) .filter( (value): value is FindOneByIdentifiersOptions => !!value.aggregateProductId ); const variants = await this.bulkFindOneByIdentifiers(input, manager); mergedAggregateProductVariantsData.forEach( ({ data, variantIdentifiers, productIdentifiers }, index) => { const variant = variants[index]; const productId = aggregateProducts.find( ({ identifiers }) => intersection(identifiers, productIdentifiers).length > 0 )?.id; if (!productId) { logger.warn( 'Could not find productId when attempting to create new aggregate product variants', { data, } ); return; } if (variant) { aggregateProductVariantEntities.push( Object.assign(variant, { ...data, productId, identifiers: union(variant.identifiers, variantIdentifiers), imageUrls: union(variant.imageUrls, data.imageUrls), }) ); } else { aggregateProductVariantEntities.push( manager.create(AggregateProductVariant, { ...data, id: data.vid, productId, identifiers: variantIdentifiers, }) ); } } ); const savedVariants = await manager.save( AggregateProductVariant, aggregateProductVariantEntities ); return { aggregateProductVariants: savedVariants.map(({ id, identifiers }) => ({ id, identifiers, })), aggregateProducts: aggregateProducts.map(({ id, identifiers }) => ({ id, identifiers, })), }; } }