Last active
October 5, 2020 15:10
-
-
Save svonava/1564fa465359afeffd4c66c55319c4b9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// --- USAGE --- | |
import { InjectConnection } from '@nestjs/typeorm'; | |
//... | |
@Injectable() | |
export class Service { | |
constructor( | |
@InjectConnection() | |
private connection: Connection, | |
//... | |
const prime = await this.repo | |
.createQueryBuilder('someobject') | |
.where('someobject.id = :id', { id }) | |
.getOne(); | |
const loader = new RelationLoader(this.connection); | |
await loader.loadRelations(prime, { | |
field1: {}, | |
field2: { | |
nested1: { | |
nested2: {}, | |
nested3: {} | |
}, | |
}, | |
}); | |
// --- IMPLEMENTATION --- | |
// | |
// Performs a breadth-first search through the provided relationship object, | |
// running parallel queries on each depth level and building the queried object. | |
// Supports all relationship cardinalities (One/Many variantions). | |
// | |
import { Connection, ObjectLiteral } from 'typeorm'; | |
import { RelationMetadata } from 'typeorm/metadata/RelationMetadata'; | |
type LoadTask = { entities: ObjectLiteral[]; relations: object }; | |
function getLoadTask( | |
entityOrEntities: ObjectLiteral | ObjectLiteral[], | |
relations: object, | |
): LoadTask { | |
return { | |
entities: Array.isArray(entityOrEntities) | |
? entityOrEntities | |
: [entityOrEntities], | |
relations: relations, | |
}; | |
} | |
type LoadQuery = { | |
entity: ObjectLiteral; | |
relation: string; | |
relationMeta: RelationMetadata; | |
relations: object; | |
}; | |
type LoadResult = { | |
query: LoadQuery; | |
entityOrEntities: ObjectLiteral | ObjectLiteral[]; | |
}; | |
function getLoadResult( | |
query: LoadQuery, | |
entityOrEntities: ObjectLiteral | ObjectLiteral[], | |
): LoadResult { | |
return { query: query, entityOrEntities: entityOrEntities }; | |
} | |
export class RelationLoader { | |
connection: Connection; | |
metadata_cache: Map<string, RelationMetadata>; | |
constructor(connection: Connection) { | |
this.connection = connection; | |
this.metadata_cache = new Map(); | |
} | |
/* | |
* Loads related entities to arbitrary depth. | |
*/ | |
async loadRelations( | |
entityOrEntities: ObjectLiteral | ObjectLiteral[], | |
relations: object, | |
): Promise<void> { | |
// Make this a no-op, if entities are not provided. | |
if (!entityOrEntities) { | |
return Promise.resolve(null); | |
} | |
// Seed the tasks with the initial entity itself with it's relations. | |
let loadTasks = [getLoadTask(entityOrEntities, relations)]; | |
while (loadTasks.length) { | |
// 1) Prepare queries on the object frontier. | |
const queries = await this.getQueries(loadTasks); | |
// 2) Execute prepared queries. | |
const results = await this.executeQueries(queries); | |
// 3) Generate the next level of LoadTasks. | |
loadTasks = this.getLoadTasks(results); | |
} | |
} | |
async getMetadata(entity_name, relation): Promise<RelationMetadata> { | |
const key = entity_name + relation; | |
if (!this.metadata_cache.has(key)) { | |
this.metadata_cache.set( | |
key, | |
await this.connection | |
.getMetadata(entity_name) | |
.relations.find(rM => rM.propertyName === relation), | |
); | |
} | |
console.assert( | |
this.metadata_cache.get(key), | |
'Relation does not exist: ' + key, | |
); | |
return this.metadata_cache.get(key); | |
} | |
async getQuery( | |
entity: ObjectLiteral, | |
relation: string, | |
nextRelations: object, | |
): Promise<LoadQuery> { | |
return { | |
entity: entity, | |
relation: relation, | |
relationMeta: await this.getMetadata(entity.constructor.name, relation), | |
relations: nextRelations, | |
}; | |
} | |
async getQueries(loadTasks: Array<LoadTask>): Promise<Array<LoadQuery>> { | |
const queries = await Promise.all( | |
loadTasks.map(async loadTask => | |
Promise.all( | |
loadTask.entities.map(async entity => | |
Promise.all( | |
Object.keys(loadTask.relations).map(async relation => | |
this.getQuery(entity, relation, loadTask.relations[relation]), | |
), | |
), | |
), | |
), | |
), | |
); | |
return this.flatten(queries); | |
} | |
async executeQueries(queries: Array<LoadQuery>): Promise<Array<LoadResult>> { | |
return await Promise.all( | |
queries.map(async query => { | |
const loadedEntityOrEntities = await this.connection.relationLoader.load( | |
query.relationMeta, | |
query.entity, | |
); | |
return getLoadResult(query, loadedEntityOrEntities); | |
}), | |
); | |
} | |
isRelationToOne(r: RelationMetadata): boolean { | |
return ( | |
r && | |
(r.isManyToOne || | |
r.isOneToOne || | |
r.isOneToOneNotOwner || | |
r.isOneToOneOwner) | |
); | |
} | |
getLoadTasks(results: Array<LoadResult>): Array<LoadTask> { | |
return results.reduce( | |
(acc, loadResult) => { | |
// 1) Update the entity with the loaded result. | |
loadResult.query.entity[loadResult.query.relation] = | |
this.isRelationToOne(loadResult.query.relationMeta) && | |
Array.isArray(loadResult.entityOrEntities) | |
? loadResult.entityOrEntities[0] | |
: loadResult.entityOrEntities; | |
// 2) Generate the next tasks. | |
acc.push( | |
getLoadTask(loadResult.entityOrEntities, loadResult.query.relations), | |
); | |
return acc; | |
}, | |
[] as Array<LoadTask>, | |
); | |
} | |
flatten(arr) { | |
return arr.reduce((flat, toFlatten) => { | |
return flat.concat( | |
Array.isArray(toFlatten) ? this.flatten(toFlatten) : toFlatten, | |
); | |
}, []); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment