From 9e38b003f96af477ee1918dc5c36180e09d01316 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sat, 5 Sep 2020 14:55:19 +0200 Subject: [PATCH] Model relations: add recursive query and recursive relation --- package.json | 2 +- src/db/ModelQuery.ts | 32 ++++++-- src/db/ModelRelation.ts | 167 +++++++++++++++++----------------------- test/ModelQuery.test.ts | 13 +++- 4 files changed, 107 insertions(+), 107 deletions(-) diff --git a/package.json b/package.json index 5223c12..8eb3efd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wms-core", - "version": "0.21.12", + "version": "0.21.13-rc.1", "description": "Node web application framework and toolbelt.", "repository": "https://gitlab.com/ArisuOngaku/wms-core", "author": "Alice Gaudon ", diff --git a/src/db/ModelQuery.ts b/src/db/ModelQuery.ts index c104f66..d02e601 100644 --- a/src/db/ModelQuery.ts +++ b/src/db/ModelQuery.ts @@ -2,7 +2,7 @@ import {query, QueryResult} from "./MysqlConnectionManager"; import {Connection} from "mysql"; import Model from "./Model"; import Pagination from "../Pagination"; -import ModelRelation from "./ModelRelation"; +import ModelRelation, {RelationDatabaseProperties} from "./ModelRelation"; import ModelFactory from "./ModelFactory"; @@ -40,6 +40,7 @@ export default class ModelQuery implements WhereFieldConsumer, fields?: (string | SelectFieldValue | UpdateFieldValue)[]) { this.type = type; @@ -54,7 +55,7 @@ export default class ModelQuery implements WhereFieldConsumer `\`${v}\``).join('.'), true, test, operator)); return this; } @@ -109,6 +110,12 @@ export default class ModelQuery implements WhereFieldConsumer implements WhereFieldConsumer implements WhereFieldConsumer { protected readonly model: S; protected readonly foreignFactory: ModelFactory; + protected readonly dbProperties: RelationDatabaseProperties; protected readonly queryModifiers: QueryModifier[] = []; protected readonly filters: ModelFilter[] = []; - protected cachedModels?: R; + protected cachedModels?: O[]; - protected constructor(model: S, foreignFactory: ModelFactory) { + protected constructor(model: S, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { this.model = model; this.foreignFactory = foreignFactory; + this.dbProperties = dbProperties; } public abstract clone(): ModelRelation; @@ -32,29 +34,53 @@ export default abstract class ModelRelation): Promise; - - protected abstract applyRegularConstraints(query: ModelQuery): void; + protected applyRegularConstraints(query: ModelQuery): void { + query.where(this.dbProperties.foreignKey, this.getModelID()); + } public async get(): Promise { if (this.cachedModels === undefined) { const query = this.makeQuery(); this.applyRegularConstraints(query); - this.cachedModels = await this.compute(query); + this.cachedModels = await query.get(); } - return this.cachedModels; + + let models = this.cachedModels; + for (const filter of this.filters) { + const newModels = []; + for (const model of models) { + if (await filter(model)) { + newModels.push(model); + } + } + models = newModels; + } + return this.collectionToOutput(models); } public getOrFail(): R { if (this.cachedModels === undefined) throw new Error('Models were not fetched'); - return this.cachedModels; + return this.collectionToOutput(this.cachedModels); } - public abstract async eagerLoad(relations: ModelRelation[]): Promise>; + protected abstract collectionToOutput(models: O[]): R; - public abstract async populate(models: ModelQueryResult): Promise; + public async eagerLoad(relations: ModelRelation[]): Promise> { + const ids = relations.map(r => r.getModelID()).filter(id => id !== null && id !== undefined); + if (ids.length === 0) return []; + + const query = this.makeQuery(); + query.where(this.dbProperties.foreignKey, ids, WhereTest.IN); + return await query.get(); + } + + public async populate(models: ModelQueryResult): Promise { + this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID()); + } public async count(): Promise { const models = await this.get(); @@ -70,52 +96,16 @@ export default abstract class ModelRelation extends ModelRelation { - protected readonly dbProperties: RelationDatabaseProperties; - - constructor(model: S, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { - super(model, foreignFactory); - this.dbProperties = dbProperties; + public constructor(model: S, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { + super(model, foreignFactory, dbProperties); } public clone(): OneModelRelation { return new OneModelRelation(this.model, this.foreignFactory, this.dbProperties); } - public getModelID() { - return this.model[this.dbProperties.localKey]; - } - - protected async compute(query: ModelQuery): Promise { - return await query.first(); - } - - protected applyRegularConstraints(query: ModelQuery): void { - query.where(this.dbProperties.foreignKey, this.getModelID()); - } - - public async get(): Promise { - const model = await super.get(); - if (model) { - for (const filter of this.filters) { - if (!(await filter(model))) { - return null; - } - } - } - return model; - } - - public async eagerLoad(relations: ModelRelation[]): Promise> { - const ids = relations.map(r => r.getModelID()).filter(id => id !== null && id !== undefined); - if (ids.length === 0) return []; - - const query = this.makeQuery(); - query.where(this.dbProperties.foreignKey, ids, WhereTest.IN); - return await query.get(); - } - - public async populate(models: ModelQueryResult): Promise { - this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID())[0] || null; + protected collectionToOutput(models: O[]): O | null { + return models[0] || null; } public async set(model: O): Promise { @@ -128,12 +118,10 @@ export class OneModelRelation extends ModelRel } export class ManyModelRelation extends ModelRelation { - protected readonly dbProperties: RelationDatabaseProperties; protected readonly paginatedCache: { [perPage: number]: { [pageNumber: number]: ModelQueryResult } } = {}; constructor(model: S, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { - super(model, foreignFactory); - this.dbProperties = dbProperties; + super(model, foreignFactory, dbProperties); } public clone(): ManyModelRelation { @@ -144,29 +132,7 @@ export class ManyModelRelation extends ModelRe return new OneModelRelation(this.model, this.foreignFactory, this.dbProperties); } - public getModelID(): any { - return this.model[this.dbProperties.localKey]; - } - - protected async compute(query: ModelQuery): Promise { - return await query.get(); - } - - protected applyRegularConstraints(query: ModelQuery): void { - query.where(this.dbProperties.foreignKey, this.getModelID()); - } - - public async get(): Promise { - let models = await super.get(); - for (const filter of this.filters) { - const newModels = []; - for (const model of models) { - if (await filter(model)) { - newModels.push(model); - } - } - models = newModels; - } + protected collectionToOutput(models: O[]): O[] { return models; } @@ -183,19 +149,6 @@ export class ManyModelRelation extends ModelRe return cache[page]; } - - public async eagerLoad(relations: ModelRelation[]): Promise> { - const ids = relations.map(r => r.getModelID()).filter(id => id !== null && id !== undefined); - if (ids.length === 0) return []; - - const query = this.makeQuery(); - query.where(this.dbProperties.foreignKey, ids, WhereTest.IN); - return await query.get(); - } - - public async populate(models: ModelQueryResult): Promise { - this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID()); - } } export class ManyThroughModelRelation extends ManyModelRelation { @@ -218,14 +171,6 @@ export class ManyThroughModelRelation extends throw new Error('Cannot reduce many through relation to one model.'); } - public getModelID(): any { - return this.model[this.dbProperties.localKey]; - } - - protected async compute(query: ModelQuery): Promise { - return await query.get(); - } - protected applyRegularConstraints(query: ModelQuery): void { query.where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelID()); } @@ -248,6 +193,32 @@ export class ManyThroughModelRelation extends } } +export class RecursiveModelRelation extends ManyModelRelation { + public constructor(model: M, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { + super(model, foreignFactory, dbProperties); + this.constraint(query => query.recursive(this.dbProperties)); + } + + public clone(): RecursiveModelRelation { + return new RecursiveModelRelation(this.model, this.foreignFactory, this.dbProperties); + } + + public async populate(models: ModelQueryResult): Promise { + await super.populate(models); + if (this.cachedModels) { + let count; + do { + count = this.cachedModels.length; + this.cachedModels.push(...models.filter(model => + !this.cachedModels!.find(cached => cached.equals(model)) && + this.cachedModels!.find(cached => cached[this.dbProperties.localKey] === model[this.dbProperties.foreignKey]) + )); + } while (count !== this.cachedModels.length); + } + } + +} + export type QueryModifier = (query: ModelQuery) => ModelQuery; export type ModelFilter = (model: O) => boolean | Promise; diff --git a/test/ModelQuery.test.ts b/test/ModelQuery.test.ts index 8f0bab9..44d667d 100644 --- a/test/ModelQuery.test.ts +++ b/test/ModelQuery.test.ts @@ -10,7 +10,18 @@ describe('Test ModelQuery', () => { .groupWhere(q => q.where('f4', 'v4'), WhereOperator.OR)) .where('f5', 'v5'); - expect(query.toString(true)).toBe('SELECT * FROM model WHERE `f1`=? AND (`f2`=? AND `f3`=? OR (`f4`=?)) AND `f5`=? '); + expect(query.toString(true)).toBe('SELECT * FROM `model` WHERE `f1`=? AND (`f2`=? AND `f3`=? OR (`f4`=?)) AND `f5`=? '); expect(query.variables.length).toBe(5); }); + + test('recursive queries', () => { + const query = ModelQuery.select({table: 'model'} as unknown as ModelFactory, '*'); + query.where('f1', 'v1'); + query.leftJoin('test').on('model.j1', 'test.j2'); + query.recursive({localKey: 'local', foreignKey: 'foreign'}); + query.sortBy('f2', 'ASC').limit(8); + + expect(query.toString(true)).toBe("WITH RECURSIVE cte AS (SELECT `model`.* FROM `model` WHERE `f1`=? UNION SELECT o.* FROM `model` AS o, cte AS c WHERE o.`foreign`=c.`local`) SELECT * FROM cte LEFT JOIN `test` ON `model`.`j1`=`test`.`j2` ORDER BY `f2` ASC LIMIT 8"); + expect(query.variables.length).toBe(1); + }); });