Model relations: add recursive query and recursive relation
This commit is contained in:
parent
7b739ff056
commit
9e38b003f9
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wms-core",
|
"name": "wms-core",
|
||||||
"version": "0.21.12",
|
"version": "0.21.13-rc.1",
|
||||||
"description": "Node web application framework and toolbelt.",
|
"description": "Node web application framework and toolbelt.",
|
||||||
"repository": "https://gitlab.com/ArisuOngaku/wms-core",
|
"repository": "https://gitlab.com/ArisuOngaku/wms-core",
|
||||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||||
|
@ -2,7 +2,7 @@ import {query, QueryResult} from "./MysqlConnectionManager";
|
|||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import Model from "./Model";
|
import Model from "./Model";
|
||||||
import Pagination from "../Pagination";
|
import Pagination from "../Pagination";
|
||||||
import ModelRelation from "./ModelRelation";
|
import ModelRelation, {RelationDatabaseProperties} from "./ModelRelation";
|
||||||
import ModelFactory from "./ModelFactory";
|
import ModelFactory from "./ModelFactory";
|
||||||
|
|
||||||
|
|
||||||
@ -40,6 +40,7 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
private _sortDirection?: 'ASC' | 'DESC';
|
private _sortDirection?: 'ASC' | 'DESC';
|
||||||
private readonly relations: string[] = [];
|
private readonly relations: string[] = [];
|
||||||
private _pivot?: string[];
|
private _pivot?: string[];
|
||||||
|
private _recursiveRelation?: RelationDatabaseProperties;
|
||||||
|
|
||||||
private constructor(type: QueryType, factory: ModelFactory<M>, fields?: (string | SelectFieldValue | UpdateFieldValue)[]) {
|
private constructor(type: QueryType, factory: ModelFactory<M>, fields?: (string | SelectFieldValue | UpdateFieldValue)[]) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
@ -54,7 +55,7 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
}
|
}
|
||||||
|
|
||||||
public on(field1: string, field2: string, test: WhereTest = WhereTest.EQ, operator: WhereOperator = WhereOperator.AND): this {
|
public on(field1: string, field2: string, test: WhereTest = WhereTest.EQ, operator: WhereOperator = WhereOperator.AND): this {
|
||||||
this._leftJoinOn.push(new WhereFieldValue(field1, field2, true, test, operator));
|
this._leftJoinOn.push(new WhereFieldValue(field1, field2.split('.').map(v => `\`${v}\``).join('.'), true, test, operator));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +110,12 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public recursive(relation: RelationDatabaseProperties): this {
|
||||||
|
if (this.type !== QueryType.SELECT) throw new Error('Recursive queries are only implemented with SELECT.');
|
||||||
|
this._recursiveRelation = relation;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public toString(final: boolean = false): string {
|
public toString(final: boolean = false): string {
|
||||||
let query = '';
|
let query = '';
|
||||||
|
|
||||||
@ -124,7 +131,7 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
|
|
||||||
let join = '';
|
let join = '';
|
||||||
if (this._leftJoin) {
|
if (this._leftJoin) {
|
||||||
join = `LEFT JOIN ${this._leftJoin} ON ${this._leftJoinOn[0]}`;
|
join = `LEFT JOIN \`${this._leftJoin}\` ON ${this._leftJoinOn[0]}`;
|
||||||
for (let i = 1; i < this._leftJoinOn.length; i++) {
|
for (let i = 1; i < this._leftJoinOn.length; i++) {
|
||||||
join += this._leftJoinOn[i].toString(false);
|
join += this._leftJoinOn[i].toString(false);
|
||||||
}
|
}
|
||||||
@ -148,18 +155,29 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
|
|
||||||
let orderBy = '';
|
let orderBy = '';
|
||||||
if (typeof this._sortBy === 'string') {
|
if (typeof this._sortBy === 'string') {
|
||||||
orderBy = `ORDER BY ${this._sortBy} ${this._sortDirection}`;
|
orderBy = `ORDER BY \`${this._sortBy}\` ${this._sortDirection}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const table = `\`${this.table}\``;
|
||||||
|
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case QueryType.SELECT:
|
case QueryType.SELECT:
|
||||||
query = `SELECT ${fields} FROM ${this.table} ${join} ${where} ${orderBy} ${limit}`;
|
if (this._recursiveRelation) {
|
||||||
|
const cteFields = fields.replace(RegExp(`${table}`, 'g'), 'o');
|
||||||
|
query = `WITH RECURSIVE cte AS (`
|
||||||
|
+ `SELECT ${fields} FROM ${table} ${where}`
|
||||||
|
+ ` UNION `
|
||||||
|
+ `SELECT ${cteFields} FROM ${table} AS o, cte AS c WHERE o.\`${this._recursiveRelation.foreignKey}\`=c.\`${this._recursiveRelation.localKey}\``
|
||||||
|
+ `) SELECT * FROM cte ${join} ${orderBy} ${limit}`;
|
||||||
|
} else {
|
||||||
|
query = `SELECT ${fields} FROM ${table} ${join} ${where} ${orderBy} ${limit}`;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case QueryType.UPDATE:
|
case QueryType.UPDATE:
|
||||||
query = `UPDATE ${this.table} SET ${fields} ${where} ${orderBy} ${limit}`;
|
query = `UPDATE ${table} SET ${fields} ${where} ${orderBy} ${limit}`;
|
||||||
break;
|
break;
|
||||||
case QueryType.DELETE:
|
case QueryType.DELETE:
|
||||||
query = `DELETE FROM ${this.table} ${where} ${orderBy} ${limit}`;
|
query = `DELETE FROM ${table} ${where} ${orderBy} ${limit}`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,15 @@ import ModelFactory from "./ModelFactory";
|
|||||||
export default abstract class ModelRelation<S extends Model, O extends Model, R extends O | O[] | null> {
|
export default abstract class ModelRelation<S extends Model, O extends Model, R extends O | O[] | null> {
|
||||||
protected readonly model: S;
|
protected readonly model: S;
|
||||||
protected readonly foreignFactory: ModelFactory<O>;
|
protected readonly foreignFactory: ModelFactory<O>;
|
||||||
|
protected readonly dbProperties: RelationDatabaseProperties;
|
||||||
protected readonly queryModifiers: QueryModifier<O>[] = [];
|
protected readonly queryModifiers: QueryModifier<O>[] = [];
|
||||||
protected readonly filters: ModelFilter<O>[] = [];
|
protected readonly filters: ModelFilter<O>[] = [];
|
||||||
protected cachedModels?: R;
|
protected cachedModels?: O[];
|
||||||
|
|
||||||
protected constructor(model: S, foreignFactory: ModelFactory<O>) {
|
protected constructor(model: S, foreignFactory: ModelFactory<O>, dbProperties: RelationDatabaseProperties) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.foreignFactory = foreignFactory;
|
this.foreignFactory = foreignFactory;
|
||||||
|
this.dbProperties = dbProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract clone(): ModelRelation<S, O, R>;
|
public abstract clone(): ModelRelation<S, O, R>;
|
||||||
@ -32,29 +34,53 @@ export default abstract class ModelRelation<S extends Model, O extends Model, R
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract getModelID(): any;
|
public getModelID(): any {
|
||||||
|
return this.model[this.dbProperties.localKey];
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract async compute(query: ModelQuery<O>): Promise<R>;
|
protected applyRegularConstraints(query: ModelQuery<O>): void {
|
||||||
|
query.where(this.dbProperties.foreignKey, this.getModelID());
|
||||||
protected abstract applyRegularConstraints(query: ModelQuery<O>): void;
|
}
|
||||||
|
|
||||||
public async get(): Promise<R> {
|
public async get(): Promise<R> {
|
||||||
if (this.cachedModels === undefined) {
|
if (this.cachedModels === undefined) {
|
||||||
const query = this.makeQuery();
|
const query = this.makeQuery();
|
||||||
this.applyRegularConstraints(query);
|
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 {
|
public getOrFail(): R {
|
||||||
if (this.cachedModels === undefined) throw new Error('Models were not fetched');
|
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<S, O, R>[]): Promise<ModelQueryResult<O>>;
|
protected abstract collectionToOutput(models: O[]): R;
|
||||||
|
|
||||||
public abstract async populate(models: ModelQueryResult<O>): Promise<void>;
|
public async eagerLoad(relations: ModelRelation<S, O, R>[]): Promise<ModelQueryResult<O>> {
|
||||||
|
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<O>): Promise<void> {
|
||||||
|
this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID());
|
||||||
|
}
|
||||||
|
|
||||||
public async count(): Promise<number> {
|
public async count(): Promise<number> {
|
||||||
const models = await this.get();
|
const models = await this.get();
|
||||||
@ -70,52 +96,16 @@ export default abstract class ModelRelation<S extends Model, O extends Model, R
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class OneModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O | null> {
|
export class OneModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O | null> {
|
||||||
protected readonly dbProperties: RelationDatabaseProperties;
|
public constructor(model: S, foreignFactory: ModelFactory<O>, dbProperties: RelationDatabaseProperties) {
|
||||||
|
super(model, foreignFactory, dbProperties);
|
||||||
constructor(model: S, foreignFactory: ModelFactory<O>, dbProperties: RelationDatabaseProperties) {
|
|
||||||
super(model, foreignFactory);
|
|
||||||
this.dbProperties = dbProperties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public clone(): OneModelRelation<S, O> {
|
public clone(): OneModelRelation<S, O> {
|
||||||
return new OneModelRelation(this.model, this.foreignFactory, this.dbProperties);
|
return new OneModelRelation(this.model, this.foreignFactory, this.dbProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getModelID() {
|
protected collectionToOutput(models: O[]): O | null {
|
||||||
return this.model[this.dbProperties.localKey];
|
return models[0] || null;
|
||||||
}
|
|
||||||
|
|
||||||
protected async compute(query: ModelQuery<O>): Promise<O | null> {
|
|
||||||
return await query.first();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected applyRegularConstraints(query: ModelQuery<O>): void {
|
|
||||||
query.where(this.dbProperties.foreignKey, this.getModelID());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get(): Promise<O | null> {
|
|
||||||
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<S, O, O | null>[]): Promise<ModelQueryResult<O>> {
|
|
||||||
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<O>): Promise<void> {
|
|
||||||
this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID())[0] || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async set(model: O): Promise<void> {
|
public async set(model: O): Promise<void> {
|
||||||
@ -128,12 +118,10 @@ export class OneModelRelation<S extends Model, O extends Model> extends ModelRel
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ManyModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O[]> {
|
export class ManyModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O[]> {
|
||||||
protected readonly dbProperties: RelationDatabaseProperties;
|
|
||||||
protected readonly paginatedCache: { [perPage: number]: { [pageNumber: number]: ModelQueryResult<O> } } = {};
|
protected readonly paginatedCache: { [perPage: number]: { [pageNumber: number]: ModelQueryResult<O> } } = {};
|
||||||
|
|
||||||
constructor(model: S, foreignFactory: ModelFactory<O>, dbProperties: RelationDatabaseProperties) {
|
constructor(model: S, foreignFactory: ModelFactory<O>, dbProperties: RelationDatabaseProperties) {
|
||||||
super(model, foreignFactory);
|
super(model, foreignFactory, dbProperties);
|
||||||
this.dbProperties = dbProperties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public clone(): ManyModelRelation<S, O> {
|
public clone(): ManyModelRelation<S, O> {
|
||||||
@ -144,29 +132,7 @@ export class ManyModelRelation<S extends Model, O extends Model> extends ModelRe
|
|||||||
return new OneModelRelation<S, O>(this.model, this.foreignFactory, this.dbProperties);
|
return new OneModelRelation<S, O>(this.model, this.foreignFactory, this.dbProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getModelID(): any {
|
protected collectionToOutput(models: O[]): O[] {
|
||||||
return this.model[this.dbProperties.localKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async compute(query: ModelQuery<O>): Promise<O[]> {
|
|
||||||
return await query.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected applyRegularConstraints(query: ModelQuery<O>): void {
|
|
||||||
query.where(this.dbProperties.foreignKey, this.getModelID());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get(): Promise<O[]> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,19 +149,6 @@ export class ManyModelRelation<S extends Model, O extends Model> extends ModelRe
|
|||||||
|
|
||||||
return cache[page];
|
return cache[page];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async eagerLoad(relations: ModelRelation<S, O, O[]>[]): Promise<ModelQueryResult<O>> {
|
|
||||||
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<O>): Promise<void> {
|
|
||||||
this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ManyThroughModelRelation<S extends Model, O extends Model> extends ManyModelRelation<S, O> {
|
export class ManyThroughModelRelation<S extends Model, O extends Model> extends ManyModelRelation<S, O> {
|
||||||
@ -218,14 +171,6 @@ export class ManyThroughModelRelation<S extends Model, O extends Model> extends
|
|||||||
throw new Error('Cannot reduce many through relation to one model.');
|
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<O>): Promise<O[]> {
|
|
||||||
return await query.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected applyRegularConstraints(query: ModelQuery<O>): void {
|
protected applyRegularConstraints(query: ModelQuery<O>): void {
|
||||||
query.where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelID());
|
query.where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelID());
|
||||||
}
|
}
|
||||||
@ -248,6 +193,32 @@ export class ManyThroughModelRelation<S extends Model, O extends Model> extends
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RecursiveModelRelation<M extends Model> extends ManyModelRelation<M, M> {
|
||||||
|
public constructor(model: M, foreignFactory: ModelFactory<M>, dbProperties: RelationDatabaseProperties) {
|
||||||
|
super(model, foreignFactory, dbProperties);
|
||||||
|
this.constraint(query => query.recursive(this.dbProperties));
|
||||||
|
}
|
||||||
|
|
||||||
|
public clone(): RecursiveModelRelation<M> {
|
||||||
|
return new RecursiveModelRelation(this.model, this.foreignFactory, this.dbProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async populate(models: ModelQueryResult<M>): Promise<void> {
|
||||||
|
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<M extends Model> = (query: ModelQuery<M>) => ModelQuery<M>;
|
export type QueryModifier<M extends Model> = (query: ModelQuery<M>) => ModelQuery<M>;
|
||||||
|
|
||||||
export type ModelFilter<O extends Model> = (model: O) => boolean | Promise<boolean>;
|
export type ModelFilter<O extends Model> = (model: O) => boolean | Promise<boolean>;
|
||||||
|
@ -10,7 +10,18 @@ describe('Test ModelQuery', () => {
|
|||||||
.groupWhere(q => q.where('f4', 'v4'), WhereOperator.OR))
|
.groupWhere(q => q.where('f4', 'v4'), WhereOperator.OR))
|
||||||
.where('f5', 'v5');
|
.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);
|
expect(query.variables.length).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('recursive queries', () => {
|
||||||
|
const query = ModelQuery.select({table: 'model'} as unknown as ModelFactory<Model>, '*');
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user