Skip to content

Instantly share code, notes, and snippets.

@svonava
Last active October 5, 2020 15:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save svonava/1564fa465359afeffd4c66c55319c4b9 to your computer and use it in GitHub Desktop.
Save svonava/1564fa465359afeffd4c66c55319c4b9 to your computer and use it in GitHub Desktop.
// --- 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