ModelQuery: add create() and fix boolean serialization

This commit is contained in:
Alice Gaudon 2020-10-02 12:06:18 +02:00
parent 00c806aa0a
commit 595a6d4066
5 changed files with 118 additions and 69 deletions

View File

@ -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<ModelComponent<Model>>
return ModelFactory.get(this).create(data, true);
}
public static select<M extends Model>(this: ModelType<M>, ...fields: SelectFields): ModelQuery<M> {
public static select<M extends Model>(this: ModelType<M>, ...fields: QueryFields): ModelQuery<M> {
return ModelFactory.get(this).select(...fields);
}
@ -157,43 +157,25 @@ export default abstract class Model implements Extendable<ModelComponent<Model>>
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<M extends Model> extends Type<M> {
getPrimaryKeyFields(): (keyof M & string)[];
select<M extends Model>(this: ModelType<M>, ...fields: SelectFields): ModelQuery<M>;
select<M extends Model>(this: ModelType<M>, ...fields: QueryFields): ModelQuery<M>;
}

View File

@ -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<M extends Model> {
@ -45,10 +45,14 @@ export default class ModelFactory<M extends Model> {
return this.modelType.table;
}
public select(...fields: SelectFields): ModelQuery<M> {
public select(...fields: QueryFields): ModelQuery<M> {
return ModelQuery.select(this, ...fields);
}
public insert(data: Pick<M, keyof M>): ModelQuery<M> {
return ModelQuery.insert(this, data);
}
public update(data: Pick<M, keyof M>): ModelQuery<M> {
return ModelQuery.update(this, data);
}

View File

@ -7,15 +7,23 @@ import ModelFactory from "./ModelFactory";
export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M> {
public static select<M extends Model>(factory: ModelFactory<M>, ...fields: SelectFields): ModelQuery<M> {
public static select<M extends Model>(factory: ModelFactory<M>, ...fields: QueryFields): ModelQuery<M> {
fields = fields.map(v => v === '' ? new SelectFieldValue('none', 1, true) : v);
return new ModelQuery(QueryType.SELECT, factory, fields.length > 0 ? fields : ['*']);
}
public static update<M extends Model>(factory: ModelFactory<M>, data: { [K in keyof M]?: M[K] }): ModelQuery<M> {
public static insert<M extends Model>(factory: ModelFactory<M>, data: Pick<M, keyof M>): ModelQuery<M> {
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<M extends Model>(factory: ModelFactory<M>, data: Pick<M, keyof M>): ModelQuery<M> {
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<M extends Model> implements WhereFieldConsumer<M
private readonly type: QueryType;
private readonly factory: ModelFactory<M>;
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<M extends Model> implements WhereFieldConsumer<M
private _recursiveRelation?: RelationDatabaseProperties;
private _reverseRecursiveRelation?: boolean;
private constructor(type: QueryType, factory: ModelFactory<M>, fields?: SelectFields) {
private constructor(type: QueryType, factory: ModelFactory<M>, fields?: QueryFields) {
this.type = type;
this.factory = factory;
this.table = factory.table;
@ -240,6 +248,14 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
query = `(${query}) UNION ${this._union.query.toString(false)}${unionOrderBy}${unionLimit}${unionOffset}`;
}
break;
case QueryType.INSERT: {
const insertFields = this.fields.filter(f => 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<M extends Model> extends Array<M> {
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<M extends Model> {
groupWhere(setter: (query: WhereFieldConsumer<M>) => void, operator?: WhereOperator): this;
}
export type SelectFields = (string | SelectFieldValue | UpdateFieldValue)[];
export type QueryFields = (string | SelectFieldValue | FieldValue)[];
export type SortDirection = 'ASC' | 'DESC';

View File

@ -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 ||

View File

@ -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<Model>, '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<Model>,
{
'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<Model>, {
'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', () => {