ModelQuery: add create() and fix boolean serialization
This commit is contained in:
parent
00c806aa0a
commit
595a6d4066
@ -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>;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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 ||
|
||||
|
@ -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', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user