From 595a6d4066cbdbee41994ff768bf6ac4da56b0ea Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Fri, 2 Oct 2020 12:06:18 +0200 Subject: [PATCH] ModelQuery: add create() and fix boolean serialization --- src/db/Model.ts | 46 ++++++------------- src/db/ModelFactory.ts | 8 +++- src/db/ModelQuery.ts | 77 +++++++++++++++++++++----------- src/db/MysqlConnectionManager.ts | 4 +- test/ModelQuery.test.ts | 52 +++++++++++++++++---- 5 files changed, 118 insertions(+), 69 deletions(-) diff --git a/src/db/Model.ts b/src/db/Model.ts index 590fd4d..399d407 100644 --- a/src/db/Model.ts +++ b/src/db/Model.ts @@ -1,11 +1,11 @@ -import MysqlConnectionManager, {isQueryVariable, query, QueryVariable} from "./MysqlConnectionManager"; +import MysqlConnectionManager from "./MysqlConnectionManager"; import Validator from "./Validator"; import {Connection} from "mysql"; import ModelComponent from "./ModelComponent"; import {Type} from "../Utils"; import ModelFactory, {PrimaryKeyValue} from "./ModelFactory"; import ModelRelation from "./ModelRelation"; -import ModelQuery, {ModelFieldData, ModelQueryResult, SelectFields} from "./ModelQuery"; +import ModelQuery, {ModelFieldData, ModelQueryResult, QueryFields} from "./ModelQuery"; import {Request} from "express"; import Extendable from "../Extendable"; @@ -25,7 +25,7 @@ export default abstract class Model implements Extendable> return ModelFactory.get(this).create(data, true); } - public static select(this: ModelType, ...fields: SelectFields): ModelQuery { + public static select(this: ModelType, ...fields: QueryFields): ModelQuery { return ModelFactory.get(this).select(...fields); } @@ -157,43 +157,25 @@ export default abstract class Model implements Extendable> this.updated_at = new Date(); } - const properties = []; - const values: QueryVariable[] = []; let needsFullUpdate = false; + const data: { [K in keyof this]?: this[K] } = {}; + for (const property of this._properties) { + const value = this[property]; + + if (value === undefined) needsFullUpdate = true; + else data[property] = value; + } + if (this.exists()) { - const data: { [K in keyof this]?: this[K] } = {}; - for (const property of this._properties) { - const value = this[property]; - - if (value === undefined) needsFullUpdate = true; - else data[property] = value; - } - const query = this._factory.update(data); for (const indexField of this._factory.getPrimaryKeyFields()) { query.where(indexField, this[indexField]); } await query.execute(connection); } else { - const props_holders = []; - for (const property of this._properties) { - let value: ModelFieldData = this[property]; - - if (value === undefined) { - needsFullUpdate = true; - } else { - if (!isQueryVariable(value)) { - value = value.toString(); - } - properties.push(property); - props_holders.push('?'); - values.push(value as QueryVariable); - } - } - - const fieldNames = properties.map(f => `\`${f}\``).join(', '); - const result = await query(`INSERT INTO ${this.table} (${fieldNames}) VALUES(${props_holders.join(', ')})`, values, connection); + const query = this._factory.insert(data); + const result = await query.execute(connection); if (this.hasProperty('id')) this.id = Number(result.other?.insertId); this._exists = true; @@ -259,5 +241,5 @@ export interface ModelType extends Type { getPrimaryKeyFields(): (keyof M & string)[]; - select(this: ModelType, ...fields: SelectFields): ModelQuery; + select(this: ModelType, ...fields: QueryFields): ModelQuery; } diff --git a/src/db/ModelFactory.ts b/src/db/ModelFactory.ts index 53551aa..e2e1ae1 100644 --- a/src/db/ModelFactory.ts +++ b/src/db/ModelFactory.ts @@ -1,6 +1,6 @@ import ModelComponent from "./ModelComponent"; import Model, {ModelType} from "./Model"; -import ModelQuery, {ModelQueryResult, SelectFields} from "./ModelQuery"; +import ModelQuery, {ModelQueryResult, QueryFields} from "./ModelQuery"; import {Request} from "express"; export default class ModelFactory { @@ -45,10 +45,14 @@ export default class ModelFactory { return this.modelType.table; } - public select(...fields: SelectFields): ModelQuery { + public select(...fields: QueryFields): ModelQuery { return ModelQuery.select(this, ...fields); } + public insert(data: Pick): ModelQuery { + return ModelQuery.insert(this, data); + } + public update(data: Pick): ModelQuery { return ModelQuery.update(this, data); } diff --git a/src/db/ModelQuery.ts b/src/db/ModelQuery.ts index bca76f8..472744d 100644 --- a/src/db/ModelQuery.ts +++ b/src/db/ModelQuery.ts @@ -7,15 +7,23 @@ import ModelFactory from "./ModelFactory"; export default class ModelQuery implements WhereFieldConsumer { - public static select(factory: ModelFactory, ...fields: SelectFields): ModelQuery { + public static select(factory: ModelFactory, ...fields: QueryFields): ModelQuery { fields = fields.map(v => v === '' ? new SelectFieldValue('none', 1, true) : v); return new ModelQuery(QueryType.SELECT, factory, fields.length > 0 ? fields : ['*']); } - public static update(factory: ModelFactory, data: { [K in keyof M]?: M[K] }): ModelQuery { + public static insert(factory: ModelFactory, data: Pick): ModelQuery { const fields = []; for (const key of Object.keys(data)) { - fields.push(new UpdateFieldValue(inputToFieldOrValue(key, factory.table), data[key], false)); + fields.push(new FieldValue(key, data[key], false)); + } + return new ModelQuery(QueryType.INSERT, factory, fields); + } + + public static update(factory: ModelFactory, data: Pick): ModelQuery { + const fields = []; + for (const key of Object.keys(data)) { + fields.push(new FieldValue(inputToFieldOrValue(key, factory.table), data[key], false)); } return new ModelQuery(QueryType.UPDATE, factory, fields); } @@ -27,7 +35,7 @@ export default class ModelQuery implements WhereFieldConsumer; private readonly table: string; - private readonly fields: SelectFields; + private readonly fields: QueryFields; private _leftJoin?: string; private _leftJoinAlias?: string; private _leftJoinOn: WhereFieldValue[] = []; @@ -43,7 +51,7 @@ export default class ModelQuery implements WhereFieldConsumer, fields?: SelectFields) { + private constructor(type: QueryType, factory: ModelFactory, fields?: QueryFields) { this.type = type; this.factory = factory; this.table = factory.table; @@ -240,6 +248,14 @@ export default class ModelQuery implements WhereFieldConsumer f instanceof FieldValue) + .map(f => f as FieldValue); + const insertFieldNames = insertFields.map(f => f.fieldName).join(','); + const insertFieldValues = insertFields.map(f => f.fieldValue).join(','); + query = `INSERT INTO ${table} (${insertFieldNames}) VALUES(${insertFieldValues})`; + break; + } case QueryType.UPDATE: query = `UPDATE ${table} SET ${fields}${where}${orderBy}${limit}`; break; @@ -375,6 +391,7 @@ export interface ModelQueryResult extends Array { export enum QueryType { SELECT, + INSERT, UPDATE, DELETE, } @@ -406,21 +423,7 @@ class FieldValue { } public toString(first: boolean = true): string { - let valueStr: string; - if (this.value instanceof ModelQuery) { - valueStr = this.value.toString(false); - } else if (this.value === null || this.value === undefined) { - valueStr = 'null'; - } else if (this.raw) { - valueStr = this.value.toString(); - } else { - valueStr = Array.isArray(this.value) ? - `(${'?'.repeat(this.value.length).split('').join(',')})` : - '?'; - } - - const field = inputToFieldOrValue(this.field); - return `${first ? '' : ','}${field}${this.test}${valueStr}`; + return `${first ? '' : ','}${this.fieldName}${this.test}${this.fieldValue}`; } protected get test(): string { @@ -429,7 +432,8 @@ class FieldValue { public get variables(): QueryVariable[] { if (this.value instanceof ModelQuery) return this.value.variables; - if (this.raw || this.value === null || this.value === undefined) return []; + if (this.raw || this.value === null || this.value === undefined || + typeof this.value === 'boolean') return []; if (Array.isArray(this.value)) return this.value.map(value => { if (!isQueryVariable(value)) value = value.toString(); return value; @@ -439,6 +443,28 @@ class FieldValue { if (!isQueryVariable(value)) value = value.toString(); return [value as QueryVariable]; } + + public get fieldName(): string { + return inputToFieldOrValue(this.field); + } + + public get fieldValue(): ModelFieldData { + let value: string; + if (this.value instanceof ModelQuery) { + value = this.value.toString(false); + } else if (this.value === null || this.value === undefined) { + value = 'null'; + } else if (typeof this.value === 'boolean') { + value = String(this.value); + } else if (this.raw) { + value = this.value.toString(); + } else { + value = Array.isArray(this.value) ? + `(${'?'.repeat(this.value.length).split('').join(',')})` : + '?'; + } + return value; + } } export class SelectFieldValue extends FieldValue { @@ -448,6 +474,8 @@ export class SelectFieldValue extends FieldValue { value = this.value.toString(true); } else if (this.value === null || this.value === undefined) { value = 'null'; + } else if (typeof this.value === 'boolean') { + value = String(this.value); } else { value = this.raw ? this.value.toString() : @@ -457,9 +485,6 @@ export class SelectFieldValue extends FieldValue { } } -class UpdateFieldValue extends FieldValue { -} - class WhereFieldValue extends FieldValue { private readonly _test: WhereTest; private readonly operator: WhereOperator; @@ -475,7 +500,7 @@ class WhereFieldValue extends FieldValue { } protected get test(): string { - if (this.value === null) { + if (this.value === null || this.value === undefined) { if (this._test === WhereTest.EQ) { return ' IS '; } else if (this._test === WhereTest.NE) { @@ -513,7 +538,7 @@ export interface WhereFieldConsumer { groupWhere(setter: (query: WhereFieldConsumer) => void, operator?: WhereOperator): this; } -export type SelectFields = (string | SelectFieldValue | UpdateFieldValue)[]; +export type QueryFields = (string | SelectFieldValue | FieldValue)[]; export type SortDirection = 'ASC' | 'DESC'; diff --git a/src/db/MysqlConnectionManager.ts b/src/db/MysqlConnectionManager.ts index ca39167..87093d1 100644 --- a/src/db/MysqlConnectionManager.ts +++ b/src/db/MysqlConnectionManager.ts @@ -244,6 +244,7 @@ export default class MysqlConnectionManager { } export type QueryVariable = + | boolean | string | number | Date @@ -252,7 +253,8 @@ export type QueryVariable = | undefined; export function isQueryVariable(value: unknown): value is QueryVariable { - return typeof value === "string" || + return typeof value === 'boolean' || + typeof value === "string" || typeof value === 'number' || value instanceof Date || value instanceof Buffer || diff --git a/test/ModelQuery.test.ts b/test/ModelQuery.test.ts index 4b2f199..92bc7df 100644 --- a/test/ModelQuery.test.ts +++ b/test/ModelQuery.test.ts @@ -5,8 +5,11 @@ import Model from "../src/db/Model"; describe('Test ModelQuery', () => { test('select', () => { const query = ModelQuery.select({table: 'model'} as unknown as ModelFactory, 'f1', '"Test" as f2') - .where('f4', 'v4'); - expect(query.toString(true)).toBe('SELECT `model`.`f1`,"Test" as f2 FROM `model` WHERE `f4`=?'); + .where('f4', 'v4') + .where('f5', true) + .where('f6', null) + .where('f7', undefined); + expect(query.toString(true)).toBe('SELECT `model`.`f1`,"Test" as f2 FROM `model` WHERE `f4`=? AND `f5`=true AND `f6` IS null AND `f7` IS null'); expect(query.variables).toStrictEqual(['v4']); }); @@ -20,14 +23,47 @@ describe('Test ModelQuery', () => { expect(queryRaw.toString(true)).toBe('SELECT `model`.* FROM `model` ORDER BY coalesce(model.f1, model.f2) ASC'); }); + test('create (insert into)', () => { + const date = new Date(); + const query = ModelQuery.insert( + {table: 'model'} as unknown as ModelFactory, + { + 'boolean': true, + 'null': null, + 'undefined': undefined, + 'string': 'string', + 'date': date, + 'sensitive': 'sensitive', // Reserved word + }, + ); + expect(query.toString(true)).toBe('INSERT INTO `model` (`boolean`,`null`,`undefined`,`string`,`date`,`sensitive`) VALUES(true,null,null,?,?,?)'); + expect(query.variables).toStrictEqual([ + 'string', + date, + 'sensitive', + ]); + }); + test('update', () => { + const date = new Date(); 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']); + 'boolean': true, + 'null': null, + 'undefined': undefined, + 'string': 'string', + 'date': date, + 'sensitive': 'sensitive', // Reserved word + }).where('f4', 'v4') + .where('f5', true) + .where('f6', null) + .where('f7', undefined); + expect(query.toString(true)).toBe('UPDATE `model` SET `model`.`boolean`=true,`model`.`null`=null,`model`.`undefined`=null,`model`.`string`=?,`model`.`date`=?,`model`.`sensitive`=? WHERE `f4`=? AND `f5`=true AND `f6` IS null AND `f7` IS null'); + expect(query.variables).toStrictEqual([ + 'string', + date, + 'sensitive', + 'v4', + ]); }); test('function select', () => {