import type { TgdContext } from "#/types/TgdContext"; import DataLoader from "dataloader"; import { UseMiddleware } from "type-graphql"; import Container from "typedi"; import type { Connection } from "typeorm"; import type { ColumnMetadata } from "typeorm/metadata/ColumnMetadata"; import type { RelationMetadata } from "typeorm/metadata/RelationMetadata"; export function ImplicitLoaderImpl<V>(): PropertyDecorator { return (target: Object, propertyKey: string | symbol) => { UseMiddleware(async ({ root, context }, next) => { const tgdContext = context._tgdContext as TgdContext; if (tgdContext.typeormGetConnection == null) { throw Error("typeormGetConnection is not set"); } const relation = tgdContext .typeormGetConnection() .getMetadata(target.constructor) .findRelationWithPropertyPath(propertyKey.toString()); if (relation == null) { return await next(); } if (relation.inverseRelation == null) { throw Error(`inverseRelation is required: ${String(propertyKey)}`); } const dataloaderCls = relation.isOneToOneOwner || relation.isManyToOne ? ToOneOwnerDataloader : relation.isOneToOneNotOwner ? ToOneNotOwnerDataloader : relation.isOneToMany ? OneToManyDataloader : relation.isManyToMany ? ManyToManyDataloader : null; if (dataloaderCls == null) { return await next(); } return await handler<V>(root, tgdContext, relation, dataloaderCls); })(target, propertyKey); }; } async function handler<V>( root: any, { requestId, typeormGetConnection }: TgdContext, relation: RelationMetadata, dataloaderCls: | (new (r: RelationMetadata, c: Connection) => DataLoader<string, V | null>) | (new (r: RelationMetadata, c: Connection) => DataLoader<string, V[]>) ) { if (typeormGetConnection == null) { throw Error("Connection is not available"); } const serviceId = `tgd-typeorm#${relation.entityMetadata.tableName}#${relation.propertyName}`; const container = Container.of(requestId); if (!container.has(serviceId)) { container.set( serviceId, new dataloaderCls(relation, typeormGetConnection()) ); } const dataloader = container.get< DataLoader<string, V> | DataLoader<string, V[]> >(serviceId); const columns = relation.entityMetadata.primaryColumns; const pk = columns.map((c) => c.getEntityValue(root)); return await dataloader.load(JSON.stringify(pk)); } class ToOneOwnerDataloader<V> extends DataLoader<string, V | null> { constructor(relation: RelationMetadata, connection: Connection) { super(async (pks) => { const relationName = relation.inverseRelation!.propertyName; const columns = relation.entityMetadata.primaryColumns; const entities = await findEntities<V>( relation, connection, pks, relationName, columns ); const referencedColumnNames = columns.map((c) => c.propertyPath); const entitiesByRelationKey = await getEntitiesByRelationKey( entities, relationName, referencedColumnNames ); return pks.map((pk) => entitiesByRelationKey[pk]?.[0] ?? null); }); } } class ToOneNotOwnerDataloader<V> extends DataLoader<string, V | null> { constructor(relation: RelationMetadata, connection: Connection) { super(async (pks) => { const inverseRelation = relation.inverseRelation!; const relationName = relation.propertyName; const columns = inverseRelation.joinColumns; const entities = await findEntities<V>( relation, connection, pks, relationName, columns ); const referencedColumnNames = columns.map( (c) => c.referencedColumn!.propertyPath ); const entitiesByRelationKey = await getEntitiesByRelationKey<V>( entities, inverseRelation.propertyName, referencedColumnNames ); return pks.map((pk) => entitiesByRelationKey[pk]?.[0] ?? null); }); } } class OneToManyDataloader<V> extends DataLoader<string, V[]> { constructor(relation: RelationMetadata, connection: Connection) { super(async (pks) => { const inverseRelation = relation.inverseRelation!; const columns = inverseRelation.joinColumns; const entities = await findEntities<V>( relation, connection, pks, relation.propertyName, columns ); const referencedColumnNames = columns.map( (c) => c.referencedColumn!.propertyPath ); const entitiesByRelationKey = await getEntitiesByRelationKey( entities, inverseRelation.propertyName, referencedColumnNames ); return pks.map((pk) => entitiesByRelationKey[pk] ?? []); }); } } class ManyToManyDataloader<V> extends DataLoader<string, V[]> { constructor(relation: RelationMetadata, connection: Connection) { super(async (pks) => { const inversePropName = relation.inverseRelation!.propertyName; const { ownerColumns, inverseColumns } = relation.junctionEntityMetadata!; const [relationName, columns] = relation.isManyToManyOwner ? [`${inversePropName}_${relation.propertyPath}`, ownerColumns] : [`${relation.propertyName}_${inversePropName}`, inverseColumns]; const entities = await findEntities<V>( relation, connection, pks, relationName, columns ); const referencedColumnNames = columns.map( (c) => c.referencedColumn!.propertyPath ); const entitiesByRelationKey = await getEntitiesByRelationKey( entities, inversePropName, referencedColumnNames ); return pks.map((pk) => entitiesByRelationKey[pk] ?? []); }); } } async function findEntities<V>( relation: RelationMetadata, connection: Connection, stringifiedPrimaryKeys: readonly string[], relationName: string, columnMetas: ColumnMetadata[] ): Promise<V[]> { const { Brackets } = await import("typeorm"); const qb = connection.createQueryBuilder<V>( relation.type, relation.propertyName ); if (relation.isOneToOneOwner || relation.isManyToOne) { qb.innerJoinAndSelect( `${relation.propertyName}.${relationName}`, relationName ); } else if ( relation.isOneToOneNotOwner || relation.isOneToMany || relation.isManyToMany ) { const inversePropName = relation.inverseRelation!.propertyName; qb.innerJoinAndSelect( `${relation.propertyName}.${inversePropName}`, inversePropName ); } else { throw Error("never"); } const primaryKeys = stringifiedPrimaryKeys.map((pk) => JSON.parse(pk)); const columns = columnMetas.map((c) => `${relationName}.${c.propertyPath}`); const keys = columnMetas.map((c) => `${relationName}_${c.propertyAliasName}`); if (columnMetas.length === 1) { qb.where(`${columns[0]} IN (:...${keys[0]})`, { [keys[0]]: primaryKeys.map((pk) => pk[0]), }); } else { // handle composite keys primaryKeys.forEach((pk, i) => { qb.orWhere( new Brackets((exp) => { columns.forEach((column, j) => { const key = `${i}_${keys[j]}`; exp.andWhere(`${column} = :${key}`, { [key]: pk[j] }); }); }) ); }); } return qb.getMany(); } async function getEntitiesByRelationKey<V>( entities: V[], inversePropName: string, referencedColumnNames: string[] ): Promise<{ [relationKey: string]: V[] }> { const entitiesByRelationKey: { [relationKey: string]: V[] } = {}; for (const entity of entities) { const referencedEntities = [await (entity as any)[inversePropName]].flat(); referencedEntities.forEach((re) => { const key = JSON.stringify(referencedColumnNames.map((c) => re[c])); entitiesByRelationKey[key] ??= []; entitiesByRelationKey[key].push(entity); }); } return entitiesByRelationKey; }