import DataLoader from 'dataloader'; import { EntityTarget, FindManyOptions, getRepository, OrderByCondition, } from 'typeorm'; import { AggregateProduct, AggregateProductVariant, AggregateProductVariantSavesList, Page, Product, RetailerProduct, RetailerProductVariant, User, } from '..'; import { AbstractEntity } from '../abstract'; export default class TypeormDataLoader { users = TypeormDataLoader.factory('User', User, 'id'); pages = TypeormDataLoader.factory('Page', Page, 'id'); products = TypeormDataLoader.factory('Product', Product, 'id'); productsByFirebaseId = TypeormDataLoader.factory( 'Product', Product, 'firebaseId' ); pagesByProductFirebaseIdPriceAsc = TypeormDataLoader.deprecatedManyToOneFactory( 'Page', Page, 'productFirebaseId', { orderBy: { price: 'ASC' }, where: { discoverable: true }, } ); discoverablePagesByProduct = TypeormDataLoader.deprecatedManyToOneFactory( 'Page', Page, 'productFirebaseId', { where: { discoverable: true, }, } ); aggregateProductVariantsByAggregateProductId = TypeormDataLoader.manyToOneFactory( 'AggregateProductVariant', AggregateProductVariant, 'productId' ); retailerProductsByAggregateProductId = TypeormDataLoader.manyToOneFactory( 'RetailerProduct', RetailerProduct, 'aggregateProductId' ); retailerProductVariantsByRetailerProductId = TypeormDataLoader.manyToOneFactory( 'RetailerProductVariant', RetailerProductVariant, 'productId' ); retailerProductById = TypeormDataLoader.factory( 'RetailerProductById', RetailerProduct, 'id' ); aggregateProductById = TypeormDataLoader.factory( 'AggregateProductById', AggregateProduct, 'id' ); aggregateProductsByCategory = TypeormDataLoader.manyToOneFactory( 'AggregateProduct', AggregateProduct, 'category' ); variantsForSavesList = TypeormDataLoader.manyToManyFactory({ from: { entity: AggregateProductVariant, relation: 'savesLists' }, to: { entity: AggregateProductVariantSavesList }, }); savesListForvariants = TypeormDataLoader.manyToManyFactory({ from: { entity: AggregateProductVariantSavesList, relation: 'variants' }, to: { entity: AggregateProductVariant }, }); collaboratorsForSavesList = TypeormDataLoader.manyToManyFactory({ from: { entity: User, relation: 'savesLists' }, to: { entity: AggregateProductVariantSavesList }, }); retailerProductVariantsByAggregateProductVariantId = TypeormDataLoader.manyToOneFactory( 'RetailerProductVariant', RetailerProductVariant, 'variantId' ); /** * Creates new standard typeorm loaders. This method will only return data * if the value of the matching key `identifier` is not null or undefined. */ static factory< V extends Partial>, Identifier extends keyof V >( name: string, entity: EntityTarget, identifier: Identifier, options?: { take?: number; orderBy?: OrderByCondition; where?: FindManyOptions['where']; } ): DataLoader, V> { const { orderBy, where, take } = options ?? {}; const dataLoader = new DataLoader< NonNullable, NonNullable >(async (ids) => { let query = getRepository(entity) .createQueryBuilder() .where(`"${String(identifier)}" IN(:...ids)`, { ids: ids }); if (orderBy) { query = query.orderBy(orderBy); } // the take for any of them is `take`, so we load `take` * ids.length if (take !== undefined) { query = query.take(take * ids.length); } if (where !== undefined) { query.where(where); } const entities = await query.getMany(); const results = new Map( entities .filter( (result): result is V => !!result && result[identifier] !== null && result[identifier] !== undefined ) .map((result) => { return [result[identifier], result]; }) ); return ids.map( (originalId) => results.get(originalId) ?? new Error( `There is no ${name} with ${String(identifier)} ${originalId}` ) ); }); return dataLoader; } /** * Creates new standard many-to-one typeorm loaders based on a foreign key. */ static manyToOneFactory< Entity extends AbstractEntity & Record, Identifier extends keyof Entity >( _name: string, entity: EntityTarget, identifier: Identifier, options?: { orderBy?: OrderByCondition; take?: number; where?: FindManyOptions['where']; } ): DataLoader { const { orderBy, take, where } = options ?? {}; const dataLoader = new DataLoader(async (ids) => { let query = getRepository(entity) .createQueryBuilder() .where(`"${String(identifier)}" IN(:...ids)`, { ids: ids }) .andWhere(`"${String(identifier)}" IS NOT NULL`); if (orderBy) { query = query.orderBy(orderBy); } if (take !== undefined) { query = query.take(take); } if (where !== undefined) { query = query.andWhere(where); } const entities = await query.getMany(); const results = new Map( Object.entries( entities .filter((result): result is Entity => !!result) .reduce((prev, result): Record => { const key = result[identifier]; if (typeof key === 'string' || typeof key === 'number') { return { ...prev, [key]: prev[key] ? [...prev[key], result] : [result], }; } return prev; }, {} as Record) ) ); return ids.map((originalId) => results.get(originalId) ?? []); }); return dataLoader; } /** * Creates new standard many-to-one typeorm loaders based on a foreign key. * @deprecated */ static deprecatedManyToOneFactory< V extends Partial>, Identifier extends keyof V >( _name: string, entity: EntityTarget, identifier: Identifier, options?: { orderBy?: OrderByCondition; take?: number; where?: FindManyOptions['where']; } ): DataLoader, V[]> { const { orderBy, take, where } = options ?? {}; const dataLoader = new DataLoader, V[]>( async (ids) => { let query = getRepository(entity) .createQueryBuilder() .where(`"${String(identifier)}" IN(:...ids)`, { ids: ids }); if (orderBy) { query = query.orderBy(orderBy); } if (take !== undefined) { query = query.take(take); } if (where !== undefined) { query = query.andWhere(where); } const entities = await query.getMany(); const results = entities .filter( (result): result is V => !!result && result[identifier] !== null && result[identifier] !== undefined ) .reduce((prev, result): Record => { const id = result[identifier]; return typeof id === 'string' ? { ...prev, [id]: prev[id] ? [...prev[id], result] : [result], } : prev; }, {} as Record); return ids.map((originalId) => results[originalId] ?? []); } ); return dataLoader; } /** * Creates new standard many-to-many typeorm loaders based on join table. */ static manyToManyFactory< Entity extends AbstractEntity, RelatedEntity extends AbstractEntity >({ from, to, }: { from: { entity: EntityTarget; /** @default 'id'' */ identifier?: keyof Entity; relation: keyof Entity; }; to: { entity: EntityTarget; /** @default 'id'' */ identifier?: keyof RelatedEntity; }; }): DataLoader { const name = '__metaManyToManyQueryName'; const fromId = from.identifier || 'id'; const toId = to.identifier || 'id'; const dataLoader = new DataLoader(async (ids) => { const queryResult = await getRepository(from.entity) .createQueryBuilder(name) .addSelect(`"${name}"."${String(fromId)}" AS "__metaManyToManyMainId"`) .addSelect( `__metaManyToManyJoined.${String(toId)} AS "__metaManyToManyJoinedId"` ) .leftJoin(`${name}.${String(from.relation)}`, '__metaManyToManyJoined') .where(`"__metaManyToManyJoined"."${String(toId)}" IN(:...ids)`, { ids: ids, }) .getRawAndEntities<{ /* eslint-disable @typescript-eslint/naming-convention */ __metaManyToManyMainId: string; __metaManyToManyJoinedId: string; /* eslint-enable @typescript-eslint/naming-convention */ }>(); const results = new Map( Object.entries( queryResult.raw.reduce((prev, curr): Record => { const entity = queryResult.entities.find( (entity) => entity[fromId] === curr.__metaManyToManyMainId ); if (!entity) { return prev; } return { ...prev, [curr.__metaManyToManyJoinedId]: prev[ curr.__metaManyToManyJoinedId ] ? [...prev[curr.__metaManyToManyJoinedId], entity] : [entity], }; }, {} as Record) ) ); return ids.map((originalId) => results.get(originalId) ?? []); }); return dataLoader; } }