diff --git a/package.json b/package.json index 8eb3efd..c6fd2f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wms-core", - "version": "0.21.13-rc.1", + "version": "0.21.13-rc.2", "description": "Node web application framework and toolbelt.", "repository": "https://gitlab.com/ArisuOngaku/wms-core", "author": "Alice Gaudon ", diff --git a/src/db/Model.ts b/src/db/Model.ts index 94ba992..8a24deb 100644 --- a/src/db/Model.ts +++ b/src/db/Model.ts @@ -5,7 +5,7 @@ import ModelComponent from "./ModelComponent"; import {Type} from "../Utils"; import ModelFactory from "./ModelFactory"; import ModelRelation from "./ModelRelation"; -import ModelQuery, {ModelQueryResult} from "./ModelQuery"; +import ModelQuery, {ModelQueryResult, SelectFields, SelectFieldValue} from "./ModelQuery"; import {Request} from "express"; export default abstract class Model { @@ -193,7 +193,7 @@ export default abstract class Model { public async exists(): Promise { if (this._cached_exists === undefined) { - const query = this._factory.select('1'); + const query = this._factory.select(''); for (const indexField of this._factory.getPrimaryKeyFields()) { query.where(indexField, this[indexField]); } diff --git a/src/db/ModelFactory.ts b/src/db/ModelFactory.ts index 50d5358..d54d794 100644 --- a/src/db/ModelFactory.ts +++ b/src/db/ModelFactory.ts @@ -1,6 +1,6 @@ import ModelComponent from "./ModelComponent"; import Model from "./Model"; -import ModelQuery, {ModelQueryResult} from "./ModelQuery"; +import ModelQuery, {ModelQueryResult, SelectFields} from "./ModelQuery"; import {Request} from "express"; import {Type} from "../Utils"; @@ -43,7 +43,7 @@ export default class ModelFactory { return this.modelType.table; } - public select(...fields: string[]): ModelQuery { + public select(...fields: SelectFields): ModelQuery { return ModelQuery.select(this, ...fields); } diff --git a/src/db/ModelQuery.ts b/src/db/ModelQuery.ts index d02e601..ca33b07 100644 --- a/src/db/ModelQuery.ts +++ b/src/db/ModelQuery.ts @@ -7,7 +7,8 @@ import ModelFactory from "./ModelFactory"; export default class ModelQuery implements WhereFieldConsumer { - public static select(factory: ModelFactory, ...fields: string[]): ModelQuery { + public static select(factory: ModelFactory, ...fields: SelectFields): ModelQuery { + fields = fields.map(v => v === '' ? new SelectFieldValue('none', 1, true) : v); return new ModelQuery(QueryType.SELECT, factory, fields.length > 0 ? fields : ['*']); } @@ -30,8 +31,9 @@ export default class ModelQuery implements WhereFieldConsumer; private readonly table: string; - private fields: (string | SelectFieldValue | UpdateFieldValue)[]; + private readonly fields: SelectFields; private _leftJoin?: string; + private _leftJoinAlias?: string; private _leftJoinOn: WhereFieldValue[] = []; private _where: (WhereFieldValue | WhereFieldValueGroup)[] = []; private _limit?: number; @@ -42,15 +44,16 @@ export default class ModelQuery implements WhereFieldConsumer, fields?: (string | SelectFieldValue | UpdateFieldValue)[]) { + private constructor(type: QueryType, factory: ModelFactory, fields?: SelectFields) { this.type = type; this.factory = factory; this.table = factory.table; this.fields = fields || []; } - public leftJoin(table: string): this { + public leftJoin(table: string, alias?: string): this { this._leftJoin = table; + this._leftJoinAlias = alias; return this; } @@ -119,19 +122,26 @@ export default class ModelQuery implements WhereFieldConsumer !f.toString().startsWith('(') && f.toString().split('.').length === 1 ? `\`${this.table}\`.${f}` : f); - } - if (this._pivot) this.fields.push(...this._pivot); - let fields = this.fields.join(','); + // Prevent wildcard and fields from conflicting + let fieldArray = this.fields.map(f => { + let field = f.toString(); + + if (field.startsWith('(')) return f; // Skip sub-queries + + let parts = field.split('.'); + if (parts.length === 1) parts = [this.table, field]; // Add table disambiguation + + return parts.map(v => v.startsWith('`') || v === '*' ? v : `\`${v}\``).join('.'); + }); + + let fields = fieldArray.join(','); let join = ''; if (this._leftJoin) { - join = `LEFT JOIN \`${this._leftJoin}\` ON ${this._leftJoinOn[0]}`; + const alias = this._leftJoinAlias ? ` AS \`${this._leftJoinAlias}\`` : ''; + join = `LEFT JOIN \`${this._leftJoin}\`${alias} ON ${this._leftJoinOn[0]}`; for (let i = 1; i < this._leftJoinOn.length; i++) { join += this._leftJoinOn[i].toString(false); } @@ -343,7 +353,7 @@ class FieldValue { } } -class SelectFieldValue extends FieldValue { +export class SelectFieldValue extends FieldValue { public toString(first: boolean = true): string { return `(${this.value instanceof ModelQuery ? this.value : (this.raw ? this.value : '?')}) AS \`${this.field}\``; } @@ -404,3 +414,5 @@ interface WhereFieldConsumer { groupWhere(setter: (query: WhereFieldConsumer) => void, operator?: WhereOperator): this; } + +export type SelectFields = (string | SelectFieldValue | UpdateFieldValue)[]; diff --git a/src/db/ModelRelation.ts b/src/db/ModelRelation.ts index c6ea22e..1353df2 100644 --- a/src/db/ModelRelation.ts +++ b/src/db/ModelRelation.ts @@ -158,7 +158,7 @@ export class ManyThroughModelRelation extends super(model, foreignFactory, dbProperties); this.dbProperties = dbProperties; this.constraint(query => query - .leftJoin(`${this.dbProperties.pivotTable} as pivot`) + .leftJoin(this.dbProperties.pivotTable, 'pivot') .on(`pivot.${this.dbProperties.foreignPivotKey}`, `${this.foreignFactory.table}.${this.dbProperties.foreignKey}`) ); } diff --git a/src/db/Validator.ts b/src/db/Validator.ts index 6be3454..b888c08 100644 --- a/src/db/Validator.ts +++ b/src/db/Validator.ts @@ -193,7 +193,7 @@ export default class Validator { if (querySupplier) { query = querySupplier().where(foreignKey, val); } else { - query = (model instanceof Model ? model.constructor : model).select('1').where(foreignKey, val); + query = (model instanceof Model ? model.constructor : model).select('').where(foreignKey, val); } if (model instanceof Model && typeof model.id === 'number') query = query.where('id', model.id, WhereTest.NE); return (await query.execute(c)).results.length === 0; @@ -206,7 +206,7 @@ export default class Validator { public exists(modelClass: Function, foreignKey?: string): Validator { this.addStep({ - verifyStep: async (val, thingName, c) => (await (modelClass).select('1').where(foreignKey !== undefined ? foreignKey : thingName, val).execute(c)).results.length >= 1, + verifyStep: async (val, thingName, c) => (await (modelClass).select('').where(foreignKey !== undefined ? foreignKey : thingName, val).execute(c)).results.length >= 1, throw: () => new UnknownRelationValidationError((modelClass).table, foreignKey), isFormat: false, }); diff --git a/test/Model.test.ts b/test/Model.test.ts index 2c770a9..f190339 100644 --- a/test/Model.test.ts +++ b/test/Model.test.ts @@ -3,6 +3,7 @@ import Model from "../src/db/Model"; import {MIGRATIONS} from "./_migrations"; import ModelFactory from "../src/db/ModelFactory"; import {ValidationBag} from "../src/db/Validator"; +import Logger from "../src/Logger"; class FakeDummyModel extends Model { public id?: number = undefined; @@ -18,6 +19,7 @@ class FakeDummyModel extends Model { let factory: ModelFactory; beforeAll(async () => { + Logger.verbose(); MysqlConnectionManager.registerMigrations(MIGRATIONS); ModelFactory.register(FakeDummyModel); await MysqlConnectionManager.prepare(); diff --git a/test/ModelQuery.test.ts b/test/ModelQuery.test.ts index 44d667d..f3d4093 100644 --- a/test/ModelQuery.test.ts +++ b/test/ModelQuery.test.ts @@ -1,8 +1,32 @@ -import ModelQuery, {WhereOperator} from "../src/db/ModelQuery"; +import ModelQuery, {SelectFieldValue, WhereOperator} from "../src/db/ModelQuery"; import ModelFactory from "../src/db/ModelFactory"; import Model from "../src/db/Model"; describe('Test ModelQuery', () => { + test('update', () => { + const query = ModelQuery.update({table: 'model'} as unknown as ModelFactory, { + 'f1': 'v1', + 'f2': 'v2', + 'f3': 'v3', + }).where('f4', 'v4'); + expect(query.toString(true)).toBe('UPDATE `model` SET `model`.`f1`=?,`model`.`f2`=?,`model`.`f3`=? WHERE `f4`=? '); + expect(query.variables).toStrictEqual(['v1','v2','v3','v4']); + }); + + test('function select', () => { + const query = ModelQuery.select({table: 'model'} as unknown as ModelFactory, 'f1', new SelectFieldValue('_count', 'COUNT(*)', true)); + expect(query.toString(true)).toBe('SELECT `model`.`f1`,(COUNT(*)) AS `_count` FROM `model` '); + expect(query.variables).toStrictEqual([]); + }); + + test('pivot', () => { + const query = ModelQuery.select({table: 'model'} as unknown as ModelFactory, 'f1'); + query.pivot('pivot.f2', 'f3'); + + expect(query.toString(true)).toBe('SELECT `model`.`f1`,`pivot`.`f2`,`model`.`f3` FROM `model` '); + expect(query.variables).toStrictEqual([]); + }); + test('groupWhere generates proper query', () => { const query = ModelQuery.select({table: 'model'} as unknown as ModelFactory, '*'); query.where('f1', 'v1'); @@ -10,8 +34,8 @@ 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.variables.length).toBe(5); + expect(query.toString(true)).toBe('SELECT `model`.* FROM `model` WHERE `f1`=? AND (`f2`=? AND `f3`=? OR (`f4`=?)) AND `f5`=? '); + expect(query.variables).toStrictEqual(['v1','v2','v3','v4','v5']); }); test('recursive queries', () => { @@ -22,6 +46,6 @@ describe('Test ModelQuery', () => { 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); + expect(query.variables).toStrictEqual(['v1']); }); });