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 Validator from "./Validator";
|
||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import ModelComponent from "./ModelComponent";
|
import ModelComponent from "./ModelComponent";
|
||||||
import {Type} from "../Utils";
|
import {Type} from "../Utils";
|
||||||
import ModelFactory, {PrimaryKeyValue} from "./ModelFactory";
|
import ModelFactory, {PrimaryKeyValue} from "./ModelFactory";
|
||||||
import ModelRelation from "./ModelRelation";
|
import ModelRelation from "./ModelRelation";
|
||||||
import ModelQuery, {ModelFieldData, ModelQueryResult, SelectFields} from "./ModelQuery";
|
import ModelQuery, {ModelFieldData, ModelQueryResult, QueryFields} from "./ModelQuery";
|
||||||
import {Request} from "express";
|
import {Request} from "express";
|
||||||
import Extendable from "../Extendable";
|
import Extendable from "../Extendable";
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export default abstract class Model implements Extendable<ModelComponent<Model>>
|
|||||||
return ModelFactory.get(this).create(data, true);
|
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);
|
return ModelFactory.get(this).select(...fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,43 +157,25 @@ export default abstract class Model implements Extendable<ModelComponent<Model>>
|
|||||||
this.updated_at = new Date();
|
this.updated_at = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
const properties = [];
|
|
||||||
const values: QueryVariable[] = [];
|
|
||||||
let needsFullUpdate = false;
|
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()) {
|
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);
|
const query = this._factory.update(data);
|
||||||
for (const indexField of this._factory.getPrimaryKeyFields()) {
|
for (const indexField of this._factory.getPrimaryKeyFields()) {
|
||||||
query.where(indexField, this[indexField]);
|
query.where(indexField, this[indexField]);
|
||||||
}
|
}
|
||||||
await query.execute(connection);
|
await query.execute(connection);
|
||||||
} else {
|
} else {
|
||||||
const props_holders = [];
|
const query = this._factory.insert(data);
|
||||||
for (const property of this._properties) {
|
const result = await query.execute(connection);
|
||||||
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);
|
|
||||||
|
|
||||||
if (this.hasProperty('id')) this.id = Number(result.other?.insertId);
|
if (this.hasProperty('id')) this.id = Number(result.other?.insertId);
|
||||||
this._exists = true;
|
this._exists = true;
|
||||||
@ -259,5 +241,5 @@ export interface ModelType<M extends Model> extends Type<M> {
|
|||||||
|
|
||||||
getPrimaryKeyFields(): (keyof M & string)[];
|
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 ModelComponent from "./ModelComponent";
|
||||||
import Model, {ModelType} from "./Model";
|
import Model, {ModelType} from "./Model";
|
||||||
import ModelQuery, {ModelQueryResult, SelectFields} from "./ModelQuery";
|
import ModelQuery, {ModelQueryResult, QueryFields} from "./ModelQuery";
|
||||||
import {Request} from "express";
|
import {Request} from "express";
|
||||||
|
|
||||||
export default class ModelFactory<M extends Model> {
|
export default class ModelFactory<M extends Model> {
|
||||||
@ -45,10 +45,14 @@ export default class ModelFactory<M extends Model> {
|
|||||||
return this.modelType.table;
|
return this.modelType.table;
|
||||||
}
|
}
|
||||||
|
|
||||||
public select(...fields: SelectFields): ModelQuery<M> {
|
public select(...fields: QueryFields): ModelQuery<M> {
|
||||||
return ModelQuery.select(this, ...fields);
|
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> {
|
public update(data: Pick<M, keyof M>): ModelQuery<M> {
|
||||||
return ModelQuery.update(this, data);
|
return ModelQuery.update(this, data);
|
||||||
}
|
}
|
||||||
|
@ -7,15 +7,23 @@ import ModelFactory from "./ModelFactory";
|
|||||||
|
|
||||||
|
|
||||||
export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M> {
|
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);
|
fields = fields.map(v => v === '' ? new SelectFieldValue('none', 1, true) : v);
|
||||||
return new ModelQuery(QueryType.SELECT, factory, fields.length > 0 ? fields : ['*']);
|
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 = [];
|
const fields = [];
|
||||||
for (const key of Object.keys(data)) {
|
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);
|
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 type: QueryType;
|
||||||
private readonly factory: ModelFactory<M>;
|
private readonly factory: ModelFactory<M>;
|
||||||
private readonly table: string;
|
private readonly table: string;
|
||||||
private readonly fields: SelectFields;
|
private readonly fields: QueryFields;
|
||||||
private _leftJoin?: string;
|
private _leftJoin?: string;
|
||||||
private _leftJoinAlias?: string;
|
private _leftJoinAlias?: string;
|
||||||
private _leftJoinOn: WhereFieldValue[] = [];
|
private _leftJoinOn: WhereFieldValue[] = [];
|
||||||
@ -43,7 +51,7 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
private _recursiveRelation?: RelationDatabaseProperties;
|
private _recursiveRelation?: RelationDatabaseProperties;
|
||||||
private _reverseRecursiveRelation?: boolean;
|
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.type = type;
|
||||||
this.factory = factory;
|
this.factory = factory;
|
||||||
this.table = factory.table;
|
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}`;
|
query = `(${query}) UNION ${this._union.query.toString(false)}${unionOrderBy}${unionLimit}${unionOffset}`;
|
||||||
}
|
}
|
||||||
break;
|
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:
|
case QueryType.UPDATE:
|
||||||
query = `UPDATE ${table} SET ${fields}${where}${orderBy}${limit}`;
|
query = `UPDATE ${table} SET ${fields}${where}${orderBy}${limit}`;
|
||||||
break;
|
break;
|
||||||
@ -375,6 +391,7 @@ export interface ModelQueryResult<M extends Model> extends Array<M> {
|
|||||||
|
|
||||||
export enum QueryType {
|
export enum QueryType {
|
||||||
SELECT,
|
SELECT,
|
||||||
|
INSERT,
|
||||||
UPDATE,
|
UPDATE,
|
||||||
DELETE,
|
DELETE,
|
||||||
}
|
}
|
||||||
@ -406,21 +423,7 @@ class FieldValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public toString(first: boolean = true): string {
|
public toString(first: boolean = true): string {
|
||||||
let valueStr: string;
|
return `${first ? '' : ','}${this.fieldName}${this.test}${this.fieldValue}`;
|
||||||
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}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get test(): string {
|
protected get test(): string {
|
||||||
@ -429,7 +432,8 @@ class FieldValue {
|
|||||||
|
|
||||||
public get variables(): QueryVariable[] {
|
public get variables(): QueryVariable[] {
|
||||||
if (this.value instanceof ModelQuery) return this.value.variables;
|
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 (Array.isArray(this.value)) return this.value.map(value => {
|
||||||
if (!isQueryVariable(value)) value = value.toString();
|
if (!isQueryVariable(value)) value = value.toString();
|
||||||
return value;
|
return value;
|
||||||
@ -439,6 +443,28 @@ class FieldValue {
|
|||||||
if (!isQueryVariable(value)) value = value.toString();
|
if (!isQueryVariable(value)) value = value.toString();
|
||||||
return [value as QueryVariable];
|
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 {
|
export class SelectFieldValue extends FieldValue {
|
||||||
@ -448,6 +474,8 @@ export class SelectFieldValue extends FieldValue {
|
|||||||
value = this.value.toString(true);
|
value = this.value.toString(true);
|
||||||
} else if (this.value === null || this.value === undefined) {
|
} else if (this.value === null || this.value === undefined) {
|
||||||
value = 'null';
|
value = 'null';
|
||||||
|
} else if (typeof this.value === 'boolean') {
|
||||||
|
value = String(this.value);
|
||||||
} else {
|
} else {
|
||||||
value = this.raw ?
|
value = this.raw ?
|
||||||
this.value.toString() :
|
this.value.toString() :
|
||||||
@ -457,9 +485,6 @@ export class SelectFieldValue extends FieldValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdateFieldValue extends FieldValue {
|
|
||||||
}
|
|
||||||
|
|
||||||
class WhereFieldValue extends FieldValue {
|
class WhereFieldValue extends FieldValue {
|
||||||
private readonly _test: WhereTest;
|
private readonly _test: WhereTest;
|
||||||
private readonly operator: WhereOperator;
|
private readonly operator: WhereOperator;
|
||||||
@ -475,7 +500,7 @@ class WhereFieldValue extends FieldValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected get test(): string {
|
protected get test(): string {
|
||||||
if (this.value === null) {
|
if (this.value === null || this.value === undefined) {
|
||||||
if (this._test === WhereTest.EQ) {
|
if (this._test === WhereTest.EQ) {
|
||||||
return ' IS ';
|
return ' IS ';
|
||||||
} else if (this._test === WhereTest.NE) {
|
} 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;
|
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';
|
export type SortDirection = 'ASC' | 'DESC';
|
||||||
|
|
||||||
|
@ -244,6 +244,7 @@ export default class MysqlConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type QueryVariable =
|
export type QueryVariable =
|
||||||
|
| boolean
|
||||||
| string
|
| string
|
||||||
| number
|
| number
|
||||||
| Date
|
| Date
|
||||||
@ -252,7 +253,8 @@ export type QueryVariable =
|
|||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
export function isQueryVariable(value: unknown): value is QueryVariable {
|
export function isQueryVariable(value: unknown): value is QueryVariable {
|
||||||
return typeof value === "string" ||
|
return typeof value === 'boolean' ||
|
||||||
|
typeof value === "string" ||
|
||||||
typeof value === 'number' ||
|
typeof value === 'number' ||
|
||||||
value instanceof Date ||
|
value instanceof Date ||
|
||||||
value instanceof Buffer ||
|
value instanceof Buffer ||
|
||||||
|
@ -5,8 +5,11 @@ import Model from "../src/db/Model";
|
|||||||
describe('Test ModelQuery', () => {
|
describe('Test ModelQuery', () => {
|
||||||
test('select', () => {
|
test('select', () => {
|
||||||
const query = ModelQuery.select({table: 'model'} as unknown as ModelFactory<Model>, 'f1', '"Test" as f2')
|
const query = ModelQuery.select({table: 'model'} as unknown as ModelFactory<Model>, 'f1', '"Test" as f2')
|
||||||
.where('f4', 'v4');
|
.where('f4', 'v4')
|
||||||
expect(query.toString(true)).toBe('SELECT `model`.`f1`,"Test" as f2 FROM `model` WHERE `f4`=?');
|
.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']);
|
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');
|
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', () => {
|
test('update', () => {
|
||||||
|
const date = new Date();
|
||||||
const query = ModelQuery.update({table: 'model'} as unknown as ModelFactory<Model>, {
|
const query = ModelQuery.update({table: 'model'} as unknown as ModelFactory<Model>, {
|
||||||
'f1': 'v1',
|
'boolean': true,
|
||||||
'f2': 'v2',
|
'null': null,
|
||||||
'f3': 'v3',
|
'undefined': undefined,
|
||||||
}).where('f4', 'v4');
|
'string': 'string',
|
||||||
expect(query.toString(true)).toBe('UPDATE `model` SET `model`.`f1`=?,`model`.`f2`=?,`model`.`f3`=? WHERE `f4`=?');
|
'date': date,
|
||||||
expect(query.variables).toStrictEqual(['v1', 'v2', 'v3', 'v4']);
|
'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', () => {
|
test('function select', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user