Last active
February 27, 2024 08:19
-
-
Save alexy4744/50edc40d2ed6eb706f2fcf3027dbe806 to your computer and use it in GitHub Desktop.
MikroORM Closure Table Repository
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
import { Constructor } from "@mikro-orm/core"; | |
import { EntityRepository, Knex } from "@mikro-orm/postgresql"; | |
type Node = string; | |
export interface ClosureTableRepository<Entity> extends EntityRepository<Entity> { | |
containsDescendant(parent: Node, descendant: Node): Promise<boolean>; | |
findDescendants(parent: Node): Knex.QueryBuilder; | |
insertLeafNode(leaf: Node, parent: Node): Promise<void>; | |
insertRootNode(root: Node): Promise<void>; | |
moveSubtree(source: Node, destination: Node): Promise<void>; | |
removeDescendants(parent: Node): Promise<void>; | |
removeSubtree(node: Node): Promise<void>; | |
} | |
export interface ClosureTableRepositoryOptions { | |
ancestorColumn: string; | |
depthColumn: string; | |
descendantColumn: string; | |
} | |
export const ClosureTableRepository = <Entity>( | |
options: ClosureTableRepositoryOptions | |
): Constructor<ClosureTableRepository<Entity>> => { | |
// Use em.execute() to allow the ORM to log the query | |
class ClosureTableMixinRepository | |
extends EntityRepository<Entity> | |
implements ClosureTableRepository<Entity> { | |
private readonly ancestorColumn = this.getColumnName(options.ancestorColumn); | |
private readonly depthColumn = this.getColumnName(options.depthColumn); | |
private readonly descendantColumn = this.getColumnName(options.descendantColumn); | |
private readonly tableName = this.getTableName(this.entityName.toString()); | |
private get knex(): Knex { | |
return this.em.getKnex(); | |
} | |
async containsDescendant(parent: Node, descendant: Node): Promise<boolean> { | |
const hasDescendant = await this.createQueryBuilder() | |
.select("*") | |
.where({ | |
[this.ancestorColumn]: parent, | |
[this.descendantColumn]: descendant | |
}) | |
.limit(1) | |
.getSingleResult(); | |
return !!hasDescendant; | |
} | |
findDescendants(parent: Node): Knex.QueryBuilder { | |
return this.knex(this.tableName) | |
.select(this.descendantColumn) | |
.where(this.ancestorColumn, parent) | |
.andWhereNot(this.descendantColumn, parent); | |
} | |
async insertLeafNode(leaf: Node, parent: Node): Promise<void> { | |
await this.em.execute( | |
this.knex(this.tableName).insert( | |
this.knex | |
.select( | |
this.ancestorColumn, | |
this.knex.raw("?", leaf), | |
this.knex.raw(`${this.depthColumn} + 1`) | |
) | |
.from(this.tableName) | |
.where(this.descendantColumn, parent) | |
.unionAll( | |
this.knex.select( | |
this.knex.raw("?", leaf), | |
this.knex.raw("?", leaf), | |
this.knex.raw("?", 0) | |
) | |
) | |
) | |
); | |
} | |
async insertRootNode(root: Node): Promise<void> { | |
await this.em.execute( | |
this.knex(this.tableName).insert({ | |
[this.ancestorColumn]: root, | |
[this.depthColumn]: 0, | |
[this.descendantColumn]: root | |
}) | |
); | |
} | |
async moveSubtree(source: Node, destination: Node): Promise<void> { | |
await this.em.execute( | |
this.knex(this.tableName) | |
.delete() | |
.whereIn( | |
this.descendantColumn, | |
this.knex | |
.select(this.descendantColumn) | |
.from(this.tableName) | |
.where(this.ancestorColumn, source) | |
) | |
.whereIn( | |
this.ancestorColumn, | |
this.knex | |
.select(this.ancestorColumn) | |
.from(this.tableName) | |
.where(this.descendantColumn, source) | |
.andWhereNot(this.ancestorColumn, this.knex.ref(this.descendantColumn)) | |
) | |
); | |
await this.em.execute( | |
this.knex(this.tableName).insert( | |
this.knex | |
.select( | |
this.knex.ref(`supertree.${this.ancestorColumn}`), | |
this.knex.ref(`subtree.${this.descendantColumn}`), | |
// can't use .ref() here because knex gets confused with the + operator | |
this.knex.raw(`supertree.${this.depthColumn} + subtree.${this.depthColumn} + 1`) | |
) | |
.from({ supertree: this.tableName }) | |
.crossJoin(this.knex.ref(`${this.tableName} as subtree`)) | |
.where(`supertree.${this.descendantColumn}`, destination) | |
.andWhere(`subtree.${this.ancestorColumn}`, source) | |
) | |
); | |
} | |
async removeDescendants(parent: Node): Promise<void> { | |
await this.em.execute( | |
this.knex(this.tableName) | |
.delete() | |
.whereIn( | |
this.descendantColumn, | |
this.knex | |
.select(this.descendantColumn) | |
.from(this.tableName) | |
.where(this.ancestorColumn, parent) | |
.andWhereNot(this.descendantColumn, parent) | |
) | |
); | |
} | |
async removeSubtree(node: Node): Promise<void> { | |
await this.em.execute( | |
this.knex(this.tableName) | |
.delete() | |
.whereIn( | |
this.descendantColumn, | |
this.knex | |
.select(this.descendantColumn) | |
.from(this.tableName) | |
.where(this.ancestorColumn, node) | |
) | |
); | |
} | |
private getColumnName(columnName: string): string { | |
return this.em.config.getNamingStrategy().propertyToColumnName(columnName); | |
} | |
private getTableName(entityName: string): string { | |
return this.em.config.getNamingStrategy().classToTableName(entityName); | |
} | |
} | |
return ClosureTableMixinRepository; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment