Revamp model system
- Add model relations - Get rid of SQL_CALC_FOUND_ROWS (deprecated) - Eager loading
This commit is contained in:
parent
c8157b7bb0
commit
ec5b2b9aa0
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wms-core",
|
"name": "wms-core",
|
||||||
"version": "0.9.3",
|
"version": "0.10.0",
|
||||||
"description": "Node web framework",
|
"description": "Node web framework",
|
||||||
"repository": "git@gitlab.com:ArisuOngaku/wms-core.git",
|
"repository": "git@gitlab.com:ArisuOngaku/wms-core.git",
|
||||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||||
|
@ -34,6 +34,4 @@ export function cryptoRandomDictionary(size: number, dictionary: string): string
|
|||||||
return output.join('');
|
return output.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Type<T> extends Function {
|
export type Type<T> = { new(...args: any[]): T };
|
||||||
new(...args: any[]): T
|
|
||||||
}
|
|
@ -14,7 +14,7 @@ export default abstract class AuthGuard<P extends AuthProof> {
|
|||||||
|
|
||||||
public async getUserForSession(session: Express.Session): Promise<User | null> {
|
public async getUserForSession(session: Express.Session): Promise<User | null> {
|
||||||
if (!await this.isAuthenticated(session)) return null;
|
if (!await this.isAuthenticated(session)) return null;
|
||||||
return await User.getById<User>(`${session.auth_id}`);
|
return await User.getById<User>(session.auth_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async authenticateOrRegister(session: Express.Session, proof: P, registerCallback?: (connection: Connection, userID: number) => Promise<(() => Promise<void>)[]>): Promise<void> {
|
public async authenticateOrRegister(session: Express.Session, proof: P, registerCallback?: (connection: Connection, userID: number) => Promise<(() => Promise<void>)[]>): Promise<void> {
|
||||||
|
@ -59,7 +59,7 @@ export default abstract class MagicLinkAuthController extends Controller {
|
|||||||
const email = req.body.email;
|
const email = req.body.email;
|
||||||
if (!email) throw new BadRequestError('Email not specified.', 'Please try again.', req.originalUrl);
|
if (!email) throw new BadRequestError('Email not specified.', 'Please try again.', req.originalUrl);
|
||||||
|
|
||||||
let userEmail = await UserEmail.fromEmail(email);
|
let userEmail = await UserEmail.select().where('email', email).first();
|
||||||
let isRegistration = false;
|
let isRegistration = false;
|
||||||
|
|
||||||
if (!userEmail) {
|
if (!userEmail) {
|
||||||
|
@ -85,7 +85,7 @@ export default abstract class MagicLinkController extends Controller {
|
|||||||
|
|
||||||
let success = true;
|
let success = true;
|
||||||
let err;
|
let err;
|
||||||
const magicLink = await MagicLink.getById<MagicLink>(`${id}`);
|
const magicLink = await MagicLink.getById<MagicLink>(id);
|
||||||
if (!magicLink) {
|
if (!magicLink) {
|
||||||
res.status(404);
|
res.status(404);
|
||||||
err = `Couldn't find this magic link. Perhaps it has already expired.`;
|
err = `Couldn't find this magic link. Perhaps it has already expired.`;
|
||||||
|
@ -5,7 +5,8 @@ import AuthProof from "../AuthProof";
|
|||||||
import Validator from "../../db/Validator";
|
import Validator from "../../db/Validator";
|
||||||
import User from "./User";
|
import User from "./User";
|
||||||
import argon2 from "argon2";
|
import argon2 from "argon2";
|
||||||
import {WhereTest} from "../../db/Query";
|
import {WhereTest} from "../../db/ModelQuery";
|
||||||
|
import UserEmail from "./UserEmail";
|
||||||
|
|
||||||
export default class MagicLink extends Model implements AuthProof {
|
export default class MagicLink extends Model implements AuthProof {
|
||||||
public static async bySessionID(sessionID: string, actionType?: string | string[]): Promise<MagicLink | null> {
|
public static async bySessionID(sessionID: string, actionType?: string | string[]): Promise<MagicLink | null> {
|
||||||
@ -17,8 +18,7 @@ export default class MagicLink extends Model implements AuthProof {
|
|||||||
query = query.where('action_type', actionType, WhereTest.IN);
|
query = query.where('action_type', actionType, WhereTest.IN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const links = await this.models<MagicLink>(query.first());
|
return await query.first();
|
||||||
return links.length > 0 ? links[0] : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static validityPeriod(): number {
|
public static validityPeriod(): number {
|
||||||
@ -42,14 +42,14 @@ export default class MagicLink extends Model implements AuthProof {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected defineProperties(): void {
|
protected init(): void {
|
||||||
this.defineProperty<string>('session_id', new Validator().defined().length(32).unique(this));
|
this.addProperty<string>('session_id', new Validator().defined().length(32).unique(this));
|
||||||
this.defineProperty<string>('email', new Validator().defined().regexp(EMAIL_REGEX));
|
this.addProperty<string>('email', new Validator().defined().regexp(EMAIL_REGEX));
|
||||||
this.defineProperty<string>('token', new Validator().defined().length(96));
|
this.addProperty<string>('token', new Validator().defined().length(96));
|
||||||
this.defineProperty<string>('action_type', new Validator().defined().maxLength(64));
|
this.addProperty<string>('action_type', new Validator().defined().maxLength(64));
|
||||||
this.defineProperty<string>('original_url', new Validator().defined().maxLength(1745));
|
this.addProperty<string>('original_url', new Validator().defined().maxLength(1745));
|
||||||
this.defineProperty<Date>('generated_at', new Validator());
|
this.addProperty<Date>('generated_at', new Validator());
|
||||||
this.defineProperty<boolean>('authorized', new Validator().defined());
|
this.addProperty<boolean>('authorized', new Validator().defined());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isOwnedBy(userId: number): Promise<boolean> {
|
public async isOwnedBy(userId: number): Promise<boolean> {
|
||||||
@ -58,7 +58,10 @@ export default class MagicLink extends Model implements AuthProof {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getUser(): Promise<User | null> {
|
public async getUser(): Promise<User | null> {
|
||||||
return await User.fromEmail(await this.getEmail());
|
const email = await UserEmail.select()
|
||||||
|
.where('email', await this.getEmail())
|
||||||
|
.first();
|
||||||
|
return email ? email.user.get() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async revoke(): Promise<void> {
|
public async revoke(): Promise<void> {
|
||||||
|
@ -1,54 +1,36 @@
|
|||||||
import UserEmail from "./UserEmail";
|
|
||||||
import Model from "../../db/Model";
|
import Model from "../../db/Model";
|
||||||
import Validator from "../../db/Validator";
|
import Validator from "../../db/Validator";
|
||||||
import MysqlConnectionManager from "../../db/MysqlConnectionManager";
|
import MysqlConnectionManager from "../../db/MysqlConnectionManager";
|
||||||
import AddApprovedFieldToUsersTable from "../migrations/AddApprovedFieldToUsersTable";
|
import AddApprovedFieldToUsersTable from "../migrations/AddApprovedFieldToUsersTable";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
import {ManyModelRelation} from "../../db/ModelRelation";
|
||||||
|
import UserEmail from "./UserEmail";
|
||||||
|
|
||||||
export default class User extends Model {
|
export default class User extends Model {
|
||||||
public static async fromEmail(email: string): Promise<User | null> {
|
|
||||||
const users = await this.models<User>(this.select().where('id', UserEmail.select('user_id').where('email', email).first()).first());
|
|
||||||
return users.length > 0 ? users[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async countAccountsToApprove(): Promise<number> {
|
|
||||||
if (!this.isApprovalMode()) return 0;
|
|
||||||
return (await this.select('COUNT(*) as c').where('approved', false).execute())
|
|
||||||
.results[0]['c'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getUsersToApprove(): Promise<User[]> {
|
|
||||||
if (!this.isApprovalMode()) return [];
|
|
||||||
return await this.models<User>(this.select('users.*', 'ue.email as main_email')
|
|
||||||
.where('approved', false)
|
|
||||||
.leftJoin('user_emails as ue').on('ue.user_id', 'users.id')
|
|
||||||
.where('ue.main', '1'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static isApprovalMode(): boolean {
|
public static isApprovalMode(): boolean {
|
||||||
return config.get<boolean>('approval_mode') && MysqlConnectionManager.hasMigration(AddApprovedFieldToUsersTable);
|
return config.get<boolean>('approval_mode') && MysqlConnectionManager.hasMigration(AddApprovedFieldToUsersTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getAdminAccounts(): Promise<User[]> {
|
|
||||||
return await this.models<User>(this.select('users.*', '')
|
|
||||||
.where('is_admin', true)
|
|
||||||
.leftJoin('user_emails as ue').on('ue.user_id', 'users.id')
|
|
||||||
.where('ue.main', true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public name?: string;
|
public name?: string;
|
||||||
public approved: boolean = false;
|
public approved: boolean = false;
|
||||||
public is_admin: boolean = false;
|
public is_admin: boolean = false;
|
||||||
public created_at?: Date;
|
public created_at?: Date;
|
||||||
public updated_at?: Date;
|
public updated_at?: Date;
|
||||||
|
|
||||||
protected defineProperties(): void {
|
public readonly emails = new ManyModelRelation<this, UserEmail>(this, UserEmail, {
|
||||||
this.defineProperty<string>('name', new Validator().acceptUndefined().between(3, 64));
|
localKey: 'id',
|
||||||
if (User.isApprovalMode()) this.defineProperty<boolean>('approved', new Validator().defined());
|
foreignKey: 'user_id'
|
||||||
this.defineProperty<boolean>('is_admin', new Validator().defined());
|
});
|
||||||
|
|
||||||
this.defineProperty<Date>('created_at');
|
public readonly mainEmail = this.emails.clone().constraint(q => q.where('main', true));
|
||||||
this.defineProperty<Date>('updated_at');
|
|
||||||
|
protected init(): void {
|
||||||
|
this.addProperty<string>('name', new Validator().acceptUndefined().between(3, 64));
|
||||||
|
if (User.isApprovalMode()) this.addProperty<boolean>('approved', new Validator().defined());
|
||||||
|
this.addProperty<boolean>('is_admin', new Validator().defined());
|
||||||
|
|
||||||
|
this.addProperty<Date>('created_at');
|
||||||
|
this.addProperty<Date>('updated_at');
|
||||||
}
|
}
|
||||||
|
|
||||||
public isApproved(): boolean {
|
public isApproved(): boolean {
|
||||||
|
@ -1,65 +1,43 @@
|
|||||||
import User from "./User";
|
import User from "./User";
|
||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import Model, {EMAIL_REGEX, ModelCache} from "../../db/Model";
|
import Model, {EMAIL_REGEX} from "../../db/Model";
|
||||||
import Validator from "../../db/Validator";
|
import Validator from "../../db/Validator";
|
||||||
import {query} from "../../db/MysqlConnectionManager";
|
import {query} from "../../db/MysqlConnectionManager";
|
||||||
|
import {OneModelRelation} from "../../db/ModelRelation";
|
||||||
|
|
||||||
export default class UserEmail extends Model {
|
export default class UserEmail extends Model {
|
||||||
public static async fromEmail(email: any): Promise<UserEmail | null> {
|
|
||||||
const emails = await this.models<UserEmail>(this.select().where('email', email).first());
|
|
||||||
return emails.length > 0 ? emails[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getMainFromUser(userID: number): Promise<UserEmail | null> {
|
|
||||||
const emails = await this.models<UserEmail>(this.select().where('user_id', userID).where('main', 1).first());
|
|
||||||
return emails.length > 0 ? emails[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async fromUser(userID: number): Promise<UserEmail[] | null> {
|
|
||||||
return await this.models<UserEmail>(this.select().where('user_id', userID));
|
|
||||||
}
|
|
||||||
|
|
||||||
public user_id?: number;
|
public user_id?: number;
|
||||||
public readonly email!: string;
|
public readonly email!: string;
|
||||||
private main!: boolean;
|
private main!: boolean;
|
||||||
public created_at?: Date;
|
public created_at?: Date;
|
||||||
|
|
||||||
|
public readonly user = new OneModelRelation<this, User>(this, User, {
|
||||||
|
localKey: 'user_id',
|
||||||
|
foreignKey: 'id'
|
||||||
|
});
|
||||||
|
|
||||||
private wasSetToMain: boolean = false;
|
private wasSetToMain: boolean = false;
|
||||||
|
|
||||||
constructor(data: any) {
|
constructor(data: any) {
|
||||||
super(data);
|
super(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected defineProperties(): void {
|
protected init(): void {
|
||||||
this.defineProperty<number>('user_id', new Validator().acceptUndefined().exists(User, 'id'));
|
this.addProperty<number>('user_id', new Validator().acceptUndefined().exists(User, 'id'));
|
||||||
this.defineProperty<string>('email', new Validator().defined().regexp(EMAIL_REGEX).unique(this));
|
this.addProperty<string>('email', new Validator().defined().regexp(EMAIL_REGEX).unique(this));
|
||||||
this.defineProperty<boolean>('main', new Validator().defined());
|
this.addProperty<boolean>('main', new Validator().defined());
|
||||||
this.defineProperty<Date>('created_at', new Validator());
|
this.addProperty<Date>('created_at', new Validator());
|
||||||
}
|
}
|
||||||
|
|
||||||
async beforeSave(exists: boolean, connection: Connection) {
|
async beforeSave(exists: boolean, connection: Connection) {
|
||||||
if (this.wasSetToMain) {
|
if (this.wasSetToMain) {
|
||||||
await query(`UPDATE ${this.table} SET main=false WHERE user_id=${this.user_id}`, null, connection);
|
await query(`UPDATE ${this.table} SET main=false WHERE user_id=${this.user_id}`, null, connection);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async afterSave(): Promise<void> {
|
|
||||||
if (this.wasSetToMain) {
|
|
||||||
this.wasSetToMain = false;
|
this.wasSetToMain = false;
|
||||||
const emails = ModelCache.all(this.table);
|
|
||||||
if (emails) {
|
|
||||||
for (const id in emails) {
|
|
||||||
const otherEmail = emails[id];
|
|
||||||
if (otherEmail.id !== this.id && otherEmail.user_id === this.user_id) {
|
|
||||||
otherEmail.main = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isMain(): boolean {
|
public isMain(): boolean {
|
||||||
return !!this.main;
|
return this.main;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setMain() {
|
public setMain() {
|
||||||
|
173
src/db/Model.ts
173
src/db/Model.ts
@ -1,72 +1,67 @@
|
|||||||
import MysqlConnectionManager, {query} from "./MysqlConnectionManager";
|
import MysqlConnectionManager, {query} from "./MysqlConnectionManager";
|
||||||
import Validator from "./Validator";
|
import Validator from "./Validator";
|
||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import Query from "./Query";
|
import ModelQuery, {ModelQueryResult} from "./ModelQuery";
|
||||||
import {Request} from "express";
|
import {Request} from "express";
|
||||||
import Pagination from "../Pagination";
|
import {Type} from "../Utils";
|
||||||
|
|
||||||
|
export interface ModelClass<M extends Model> extends Type<M> {
|
||||||
|
getFactory<M extends Model>(factory?: ModelFactory<M>): ModelFactory<M>;
|
||||||
|
|
||||||
|
table: string;
|
||||||
|
|
||||||
|
getPrimaryKey(modelData: any): string;
|
||||||
|
|
||||||
|
getPrimaryKeyFields(): string[];
|
||||||
|
|
||||||
|
select<M extends Model>(...fields: string[]): ModelQuery<M>;
|
||||||
|
|
||||||
|
update<M extends Model>(data: { [key: string]: any }): ModelQuery<M>;
|
||||||
|
|
||||||
|
delete<M extends Model>(): ModelQuery<M>;
|
||||||
|
}
|
||||||
|
|
||||||
export default abstract class Model {
|
export default abstract class Model {
|
||||||
public static async getById<T extends Model>(id: string): Promise<T | null> {
|
public static get table(): string {
|
||||||
const cachedModel = ModelCache.get(this.table, id);
|
return this.name
|
||||||
if (cachedModel?.constructor === this) {
|
.replace(/(?:^|\.?)([A-Z])/g, (x, y) => '_' + y.toLowerCase())
|
||||||
return <T>cachedModel;
|
.replace(/^_/, '')
|
||||||
}
|
+ 's';
|
||||||
|
|
||||||
const models = await this.models<T>(this.select().where('id', id).first());
|
|
||||||
return models.length > 0 ? models[0] : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async paginate<T extends Model>(request: Request, perPage: number = 20, query?: Query): Promise<T[]> {
|
public static async getById<T extends Model>(this: ModelClass<T>, id: number): Promise<T | null> {
|
||||||
|
return this.select<T>().where('id', id).first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async paginate<T extends Model>(this: ModelClass<T>, request: Request, perPage: number = 20, query?: ModelQuery<T>): Promise<ModelQueryResult<T>> {
|
||||||
let page = request.params.page ? parseInt(request.params.page) : 1;
|
let page = request.params.page ? parseInt(request.params.page) : 1;
|
||||||
if (!query) query = this.select();
|
if (!query) query = this.select();
|
||||||
query = query.limit(perPage, (page - 1) * perPage).withTotalRowCount();
|
|
||||||
if (request.params.sortBy) {
|
if (request.params.sortBy) {
|
||||||
const dir = request.params.sortDirection;
|
const dir = request.params.sortDirection;
|
||||||
query = query.sortBy(request.params.sortBy, dir === 'ASC' || dir === 'DESC' ? dir : undefined);
|
query = query.sortBy(request.params.sortBy, dir === 'ASC' || dir === 'DESC' ? dir : undefined);
|
||||||
} else {
|
} else {
|
||||||
query = query.sortBy('id');
|
query = query.sortBy('id');
|
||||||
}
|
}
|
||||||
const models = await this.models<T>(query);
|
return await query.paginate(page, perPage);
|
||||||
// @ts-ignore
|
|
||||||
models.pagination = new Pagination(models, page, perPage, models.totalCount);
|
|
||||||
return models;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static select(...fields: string[]): Query {
|
public static select<T extends Model>(this: ModelClass<T>, ...fields: string[]): ModelQuery<T> {
|
||||||
return Query.select(this.table, ...fields);
|
return ModelQuery.select(this, ...fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static update(data: { [key: string]: any }): Query {
|
public static update<T extends Model>(this: ModelClass<T>, data: { [key: string]: any }): ModelQuery<T> {
|
||||||
return Query.update(this.table, data);
|
return ModelQuery.update(this, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static delete(): Query {
|
public static delete<T extends Model>(this: ModelClass<T>): ModelQuery<T> {
|
||||||
return Query.delete(this.table);
|
return ModelQuery.delete(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static async models<T extends Model>(query: Query): Promise<T[]> {
|
public static getPrimaryKey(modelData: any): string {
|
||||||
const results = await query.execute();
|
|
||||||
const models: T[] = [];
|
|
||||||
const factory = this.getFactory<T>();
|
|
||||||
for (const result of results.results) {
|
|
||||||
const cachedModel = ModelCache.get(this.table, this.getPrimaryKey(result));
|
|
||||||
if (cachedModel && cachedModel.constructor === this) {
|
|
||||||
cachedModel.updateWithData(result);
|
|
||||||
models.push(<T>cachedModel);
|
|
||||||
} else {
|
|
||||||
models.push(factory(result));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
models.totalCount = results.foundRows;
|
|
||||||
return models;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static getPrimaryKey(modelData: any): string {
|
|
||||||
return this.getPrimaryKeyFields().map(f => `${modelData[f]}`).join(',');
|
return this.getPrimaryKeyFields().map(f => `${modelData[f]}`).join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static getPrimaryKeyFields(): string[] {
|
public static getPrimaryKeyFields(): string[] {
|
||||||
return ['id'];
|
return ['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +77,7 @@ export default abstract class Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getFactory<T extends Model>(factory?: ModelFactory<T>): ModelFactory<T> {
|
public static getFactory<T extends Model>(this: ModelClass<T>, factory?: ModelFactory<T>): ModelFactory<T> {
|
||||||
if (factory === undefined) {
|
if (factory === undefined) {
|
||||||
factory = (<any>this).FACTORY;
|
factory = (<any>this).FACTORY;
|
||||||
if (factory === undefined) factory = data => new (<any>this)(data);
|
if (factory === undefined) factory = data => new (<any>this)(data);
|
||||||
@ -91,8 +86,8 @@ export default abstract class Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected readonly modelClass: ModelClass<this> = <ModelClass<this>>this.constructor;
|
||||||
protected readonly properties: ModelProperty<any>[] = [];
|
protected readonly properties: ModelProperty<any>[] = [];
|
||||||
private readonly relations: { [p: string]: (Model | null) } = {};
|
|
||||||
public id?: number;
|
public id?: number;
|
||||||
private readonly automaticIdProperty: boolean;
|
private readonly automaticIdProperty: boolean;
|
||||||
|
|
||||||
@ -101,23 +96,23 @@ export default abstract class Model {
|
|||||||
public constructor(data: any, automaticIdProperty: boolean = true) {
|
public constructor(data: any, automaticIdProperty: boolean = true) {
|
||||||
this.automaticIdProperty = automaticIdProperty;
|
this.automaticIdProperty = automaticIdProperty;
|
||||||
if (automaticIdProperty) {
|
if (automaticIdProperty) {
|
||||||
this.defineProperty<number>('id', new Validator());
|
this.addProperty<number>('id', new Validator());
|
||||||
}
|
}
|
||||||
this.defineProperties();
|
this.init();
|
||||||
this.updateWithData(data);
|
this.updateWithData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPrimaryKey(): string {
|
public getPrimaryKey(): string {
|
||||||
return (<any>this.constructor).getPrimaryKey(this);
|
return this.modelClass.getPrimaryKey(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPrimaryKeyFields(): string {
|
public getPrimaryKeyFields(): string[] {
|
||||||
return (<any>this.constructor).getPrimaryKeyFields();
|
return this.modelClass.getPrimaryKeyFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract defineProperties(): void;
|
protected abstract init(): void;
|
||||||
|
|
||||||
protected defineProperty<T>(name: string, validator?: Validator<T> | RegExp) {
|
protected addProperty<T>(name: string, validator?: Validator<T> | RegExp) {
|
||||||
if (validator === undefined) validator = new Validator();
|
if (validator === undefined) validator = new Validator();
|
||||||
if (validator instanceof RegExp) {
|
if (validator instanceof RegExp) {
|
||||||
const regexp = validator;
|
const regexp = validator;
|
||||||
@ -162,11 +157,7 @@ export default abstract class Model {
|
|||||||
|
|
||||||
const callback = async () => {
|
const callback = async () => {
|
||||||
if (needs_full_update) {
|
if (needs_full_update) {
|
||||||
this.updateWithData((await (<any>this.constructor).select().where('id', this.id!).first().execute()).results[0]);
|
this.updateWithData((await this.modelClass.select().where('id', this.id!).limit(1).execute()).results[0]);
|
||||||
}
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
this.cache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.afterSave();
|
await this.afterSave();
|
||||||
@ -198,7 +189,7 @@ export default abstract class Model {
|
|||||||
needs_full_update = true;
|
needs_full_update = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let query = Query.update(this.table, data);
|
let query = this.modelClass.update(data);
|
||||||
for (const indexField of this.getPrimaryKeyFields()) {
|
for (const indexField of this.getPrimaryKeyFields()) {
|
||||||
query = query.where(indexField, this[indexField]);
|
query = query.where(indexField, this[indexField]);
|
||||||
}
|
}
|
||||||
@ -222,54 +213,34 @@ export default abstract class Model {
|
|||||||
return needs_full_update;
|
return needs_full_update;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get table(): string {
|
|
||||||
return this.name
|
|
||||||
.replace(/(?:^|\.?)([A-Z])/g, (x, y) => '_' + y.toLowerCase())
|
|
||||||
.replace(/^_/, '')
|
|
||||||
+ 's';
|
|
||||||
}
|
|
||||||
|
|
||||||
public get table(): string {
|
public get table(): string {
|
||||||
// @ts-ignore
|
return this.modelClass.table;
|
||||||
return this.constructor.table;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async exists(): Promise<boolean> {
|
public async exists(): Promise<boolean> {
|
||||||
if (!this.id) return false;
|
if (!this.id) return false;
|
||||||
|
|
||||||
let query = Query.select(this.table, '1');
|
let query = this.modelClass.select('1');
|
||||||
for (const indexField of this.getPrimaryKeyFields()) {
|
for (const indexField of this.getPrimaryKeyFields()) {
|
||||||
query = query.where(indexField, this[indexField]);
|
query = query.where(indexField, this[indexField]);
|
||||||
}
|
}
|
||||||
query = query.first();
|
return typeof (await query.first()) !== 'undefined';
|
||||||
const result = await query.execute();
|
|
||||||
return result.results.length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete(): Promise<void> {
|
public async delete(): Promise<void> {
|
||||||
if (!(await this.exists())) throw new Error('This model instance doesn\'t exist in DB.');
|
if (!(await this.exists())) throw new Error('This model instance doesn\'t exist in DB.');
|
||||||
|
|
||||||
let query = Query.delete(this.table);
|
let query = this.modelClass.delete();
|
||||||
for (const indexField of this.getPrimaryKeyFields()) {
|
for (const indexField of this.getPrimaryKeyFields()) {
|
||||||
query = query.where(indexField, this[indexField]);
|
query = query.where(indexField, this[indexField]);
|
||||||
}
|
}
|
||||||
await query.execute();
|
await query.execute();
|
||||||
ModelCache.forget(this);
|
|
||||||
if (this.automaticIdProperty) this.id = undefined;
|
if (this.automaticIdProperty) this.id = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async validate(onlyFormat: boolean = false, connection?: Connection): Promise<void[]> {
|
public async validate(onlyFormat: boolean = false, connection?: Connection): Promise<void[]> {
|
||||||
return await Promise.all(this.properties.map(prop => prop.validate(onlyFormat, connection)));
|
return await Promise.all(this.properties.map(prop => prop.validate(onlyFormat, connection)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private cache() {
|
|
||||||
ModelCache.cache(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected relation<T extends Model>(name: string): T | null {
|
|
||||||
if (this.relations[name] === undefined) throw new Error('Model not loaded');
|
|
||||||
return <T | null>this.relations[name];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelFactory<T extends Model> {
|
export interface ModelFactory<T extends Model> {
|
||||||
@ -299,44 +270,4 @@ class ModelProperty<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ModelCache {
|
|
||||||
private static readonly caches: {
|
|
||||||
[key: string]: {
|
|
||||||
[key: string]: Model
|
|
||||||
}
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
public static cache(instance: Model) {
|
|
||||||
const primaryKey = instance.getPrimaryKey();
|
|
||||||
if (primaryKey === undefined) throw new Error('Cannot cache an instance with an undefined primaryKey.');
|
|
||||||
|
|
||||||
let tableCache = this.caches[instance.table];
|
|
||||||
if (!tableCache) tableCache = this.caches[instance.table] = {};
|
|
||||||
|
|
||||||
if (!tableCache[primaryKey]) tableCache[primaryKey] = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static forget(instance: Model) {
|
|
||||||
const primaryKey = instance.getPrimaryKey();
|
|
||||||
if (primaryKey === undefined) throw new Error('Cannot forget an instance with an undefined primaryKey.');
|
|
||||||
|
|
||||||
let tableCache = this.caches[instance.table];
|
|
||||||
if (!tableCache) return;
|
|
||||||
|
|
||||||
if (tableCache[primaryKey]) delete tableCache[primaryKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static all(table: string): {
|
|
||||||
[key: string]: Model
|
|
||||||
} | undefined {
|
|
||||||
return this.caches[table];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get(table: string, primaryKey: string): Model | undefined {
|
|
||||||
const tableCache = this.all(table);
|
|
||||||
if (!tableCache) return undefined;
|
|
||||||
return tableCache[primaryKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
|
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
|
@ -1,28 +1,32 @@
|
|||||||
import {query, QueryResult} from "./MysqlConnectionManager";
|
import {query, QueryResult} from "./MysqlConnectionManager";
|
||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
|
import Model, {ModelClass} from "./Model";
|
||||||
|
import Pagination from "../Pagination";
|
||||||
|
import ModelRelation from "./ModelRelation";
|
||||||
|
|
||||||
export default class Query {
|
export default class ModelQuery<M extends Model> {
|
||||||
public static select(table: string, ...fields: string[]): Query {
|
public static select<M extends Model>(modelClass: ModelClass<M>, ...fields: string[]): ModelQuery<M> {
|
||||||
return new Query(QueryType.SELECT, table, fields.length > 0 ? fields : ['*']);
|
return new ModelQuery(QueryType.SELECT, modelClass, fields.length > 0 ? fields : ['*']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static update(table: string, data: {
|
public static update<M extends Model>(modelClass: ModelClass<M>, data: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}): Query {
|
}): ModelQuery<M> {
|
||||||
const fields = [];
|
const fields = [];
|
||||||
for (let key in data) {
|
for (let key in data) {
|
||||||
if (data.hasOwnProperty(key)) {
|
if (data.hasOwnProperty(key)) {
|
||||||
fields.push(new UpdateFieldValue(key, data[key], false));
|
fields.push(new UpdateFieldValue(key, data[key], false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Query(QueryType.UPDATE, table, fields);
|
return new ModelQuery(QueryType.UPDATE, modelClass, fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static delete(table: string): Query {
|
public static delete<M extends Model>(modelClass: ModelClass<M>): ModelQuery<M> {
|
||||||
return new Query(QueryType.DELETE, table);
|
return new ModelQuery(QueryType.DELETE, modelClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly type: QueryType;
|
private readonly type: QueryType;
|
||||||
|
private readonly modelClass: ModelClass<M>;
|
||||||
private readonly table: string;
|
private readonly table: string;
|
||||||
private readonly fields: (string | SelectFieldValue | UpdateFieldValue)[];
|
private readonly fields: (string | SelectFieldValue | UpdateFieldValue)[];
|
||||||
private _leftJoin?: string;
|
private _leftJoin?: string;
|
||||||
@ -32,11 +36,13 @@ export default class Query {
|
|||||||
private _offset?: number;
|
private _offset?: number;
|
||||||
private _sortBy?: string;
|
private _sortBy?: string;
|
||||||
private _sortDirection?: 'ASC' | 'DESC';
|
private _sortDirection?: 'ASC' | 'DESC';
|
||||||
private _foundRows: boolean = false;
|
private readonly relations: string[] = [];
|
||||||
|
private _pivot?: string[];
|
||||||
|
|
||||||
private constructor(type: QueryType, table: string, fields?: (string | SelectFieldValue | UpdateFieldValue)[]) {
|
private constructor(type: QueryType, modelClass: ModelClass<M>, fields?: (string | SelectFieldValue | UpdateFieldValue)[]) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.table = table;
|
this.modelClass = modelClass;
|
||||||
|
this.table = modelClass.table;
|
||||||
this.fields = fields || [];
|
this.fields = fields || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +56,7 @@ export default class Query {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public where(field: string, value: string | Date | Query | any, test: WhereTest = WhereTest.EQ, operator: WhereOperator = WhereOperator.AND): this {
|
public where(field: string, value: string | Date | ModelQuery<any> | any, test: WhereTest = WhereTest.EQ, operator: WhereOperator = WhereOperator.AND): this {
|
||||||
this._where.push(new WhereFieldValue(field, value, false, test, operator));
|
this._where.push(new WhereFieldValue(field, value, false, test, operator));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -61,25 +67,31 @@ export default class Query {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public first(): this {
|
|
||||||
return this.limit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sortBy(field: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
|
public sortBy(field: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
|
||||||
this._sortBy = field;
|
this._sortBy = field;
|
||||||
this._sortDirection = direction;
|
this._sortDirection = direction;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withTotalRowCount(): this {
|
/**
|
||||||
this._foundRows = true;
|
* @param relation The relation field name to eagerload
|
||||||
|
*/
|
||||||
|
public with(relation: string): this {
|
||||||
|
this.relations.push(relation);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public pivot(...fields: string[]): this {
|
||||||
|
this._pivot = fields;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString(final: boolean = false): string {
|
public toString(final: boolean = false): string {
|
||||||
let query = '';
|
let query = '';
|
||||||
|
|
||||||
let fields = this.fields?.join(',');
|
if (this._pivot) this.fields.push(...this._pivot);
|
||||||
|
|
||||||
|
let fields = this.fields.join(',');
|
||||||
|
|
||||||
let join = '';
|
let join = '';
|
||||||
if (this._leftJoin) {
|
if (this._leftJoin) {
|
||||||
@ -112,7 +124,7 @@ export default class Query {
|
|||||||
|
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case QueryType.SELECT:
|
case QueryType.SELECT:
|
||||||
query = `SELECT ${this._foundRows ? 'SQL_CALC_FOUND_ROWS' : ''} ${fields} FROM ${this.table} ${join} ${where} ${orderBy} ${limit}`;
|
query = `SELECT ${fields} FROM ${this.table} ${join} ${where} ${orderBy} ${limit}`;
|
||||||
break;
|
break;
|
||||||
case QueryType.UPDATE:
|
case QueryType.UPDATE:
|
||||||
query = `UPDATE ${this.table} SET ${fields} ${where} ${orderBy} ${limit}`;
|
query = `UPDATE ${this.table} SET ${fields} ${where} ${orderBy} ${limit}`;
|
||||||
@ -140,18 +152,80 @@ export default class Query {
|
|||||||
return variables;
|
return variables;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isCacheable(): boolean {
|
public async execute(connection?: Connection): Promise<QueryResult> {
|
||||||
return this.type === QueryType.SELECT && this.fields.length === 1 && this.fields[0] === '*';
|
return await query(this.build(), this.variables, connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async execute(connection?: Connection): Promise<QueryResult> {
|
public async get(connection?: Connection): Promise<ModelQueryResult<M>> {
|
||||||
const queryResult = await query(this.build(), this.variables, connection);
|
const queryResult = await this.execute();
|
||||||
if (this._foundRows) {
|
const models: ModelQueryResult<M> = [];
|
||||||
const foundRows = await query('SELECT FOUND_ROWS() as r', undefined, connection);
|
|
||||||
queryResult.foundRows = foundRows.results[0].r;
|
if (this._pivot) models.pivot = [];
|
||||||
|
|
||||||
|
// Eager loading init
|
||||||
|
const relationMap: { [p: string]: ModelRelation<any, any, any>[] } = {};
|
||||||
|
for (const relation of this.relations) {
|
||||||
|
relationMap[relation] = [];
|
||||||
}
|
}
|
||||||
return queryResult;
|
|
||||||
|
const factory = this.modelClass.getFactory<M>();
|
||||||
|
for (const result of queryResult.results) {
|
||||||
|
const model = factory(result);
|
||||||
|
models.push(model);
|
||||||
|
|
||||||
|
if (this._pivot) {
|
||||||
|
const obj: any = {};
|
||||||
|
for (const field of this._pivot) {
|
||||||
|
obj[field] = result[field];
|
||||||
|
}
|
||||||
|
models.pivot!.push(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eager loading init map
|
||||||
|
for (const relation of this.relations) {
|
||||||
|
relationMap[relation].push(model[relation]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eager loading execute
|
||||||
|
for (const relationName of this.relations) {
|
||||||
|
const relations = relationMap[relationName];
|
||||||
|
const allModels = await relations[0].eagerLoad(relations);
|
||||||
|
await Promise.all(relations.map(r => r.populate(allModels)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async paginate(page: number, perPage: number, connection?: Connection): Promise<ModelQueryResult<M>> {
|
||||||
|
this.limit(perPage, (page - 1) * perPage);
|
||||||
|
const result = await this.get(connection);
|
||||||
|
result.pagination = new Pagination<M>(result, page, perPage, await this.count(true, connection));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async first(): Promise<M | null> {
|
||||||
|
const models = await this.limit(1).get();
|
||||||
|
return models.length > 0 ? models[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async count(removeLimit: boolean = false, connection?: Connection): Promise<number> {
|
||||||
|
if (removeLimit) {
|
||||||
|
this._limit = undefined;
|
||||||
|
this._offset = undefined;
|
||||||
|
}
|
||||||
|
this._sortBy = undefined;
|
||||||
|
this._sortDirection = undefined;
|
||||||
|
|
||||||
|
this.fields.push('COUNT(*)');
|
||||||
|
let queryResult = await this.execute(connection);
|
||||||
|
return queryResult.results.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelQueryResult<M extends Model> extends Array<M> {
|
||||||
|
pagination?: Pagination<M>;
|
||||||
|
pivot?: { [p: string]: any }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueryType {
|
export enum QueryType {
|
||||||
@ -187,7 +261,7 @@ class FieldValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public toString(first: boolean = true): string {
|
public toString(first: boolean = true): string {
|
||||||
return `${!first ? ',' : ''}${this.field}${this.test}${this.raw || this.value instanceof Query ? this.value : (Array.isArray(this.value) ? '(?)' : '?')}`;
|
return `${!first ? ',' : ''}${this.field}${this.test}${this.raw || this.value instanceof ModelQuery ? this.value : (Array.isArray(this.value) ? '(?)' : '?')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get test(): string {
|
protected get test(): string {
|
||||||
@ -195,13 +269,13 @@ class FieldValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get variables(): any[] {
|
public get variables(): any[] {
|
||||||
return this.value instanceof Query ? this.value.variables : [this.value];
|
return this.value instanceof ModelQuery ? this.value.variables : [this.value];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SelectFieldValue extends FieldValue {
|
class SelectFieldValue extends FieldValue {
|
||||||
public toString(first: boolean = true): string {
|
public toString(first: boolean = true): string {
|
||||||
return `(${this.value instanceof Query ? this.value : '?'}) AS ${this.field}`;
|
return `(${this.value instanceof ModelQuery ? this.value : '?'}) AS ${this.field}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
165
src/db/ModelRelation.ts
Normal file
165
src/db/ModelRelation.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import ModelQuery, {ModelQueryResult, WhereTest} from "./ModelQuery";
|
||||||
|
import Model, {ModelClass} from "./Model";
|
||||||
|
|
||||||
|
export default abstract class ModelRelation<S extends Model, O extends Model, R extends O | O[] | null> {
|
||||||
|
protected readonly model: S;
|
||||||
|
protected readonly foreignModelClass: ModelClass<O>;
|
||||||
|
protected readonly query: ModelQuery<O>;
|
||||||
|
protected cachedModels?: R;
|
||||||
|
|
||||||
|
protected constructor(model: S, foreignModelClass: ModelClass<O>) {
|
||||||
|
this.model = model;
|
||||||
|
this.foreignModelClass = foreignModelClass;
|
||||||
|
this.query = this.foreignModelClass.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract clone(): ModelRelation<S, O, R>;
|
||||||
|
|
||||||
|
public constraint(queryModifier: QueryModifier<O>): this {
|
||||||
|
queryModifier(this.query);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract getModelID(): any;
|
||||||
|
|
||||||
|
protected abstract async compute(query: ModelQuery<O>): Promise<R>;
|
||||||
|
|
||||||
|
public async get(): Promise<R> {
|
||||||
|
if (this.cachedModels === undefined) {
|
||||||
|
this.cachedModels = await this.compute(this.query);
|
||||||
|
}
|
||||||
|
return this.cachedModels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract async eagerLoad(relations: ModelRelation<S, O, R>[]): Promise<ModelQueryResult<O>>;
|
||||||
|
|
||||||
|
public abstract async populate(models: ModelQueryResult<O>): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryModifier<M extends Model> = (query: ModelQuery<M>) => ModelQuery<M>;
|
||||||
|
|
||||||
|
export class OneModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O | null> {
|
||||||
|
protected readonly dbProperties: RelationDatabaseProperties;
|
||||||
|
|
||||||
|
constructor(model: S, foreignModelClass: ModelClass<O>, dbProperties: RelationDatabaseProperties) {
|
||||||
|
super(model, foreignModelClass);
|
||||||
|
this.dbProperties = dbProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clone(): OneModelRelation<S, O> {
|
||||||
|
return new OneModelRelation(this.model, this.foreignModelClass, this.dbProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getModelID() {
|
||||||
|
return this.model[this.dbProperties.localKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async compute(query: ModelQuery<O>): Promise<O | null> {
|
||||||
|
this.query.where(this.dbProperties.foreignKey, this.getModelID());
|
||||||
|
return await query.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async eagerLoad(relations: ModelRelation<S, O, O | null>[]): Promise<ModelQueryResult<O>> {
|
||||||
|
this.query.where(
|
||||||
|
this.dbProperties.foreignKey,
|
||||||
|
relations.map(r => r.getModelID()).filter(id => id !== null && id !== undefined),
|
||||||
|
WhereTest.IN
|
||||||
|
);
|
||||||
|
return await this.query.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async populate(models: ModelQueryResult<O>): Promise<void> {
|
||||||
|
this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID())[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ManyModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O[]> {
|
||||||
|
protected readonly dbProperties: RelationDatabaseProperties;
|
||||||
|
|
||||||
|
constructor(model: S, foreignModelClass: ModelClass<O>, dbProperties: RelationDatabaseProperties) {
|
||||||
|
super(model, foreignModelClass);
|
||||||
|
this.dbProperties = dbProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clone(): ManyModelRelation<S, O> {
|
||||||
|
return new ManyModelRelation<S, O>(this.model, this.foreignModelClass, this.dbProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getModelID(): any {
|
||||||
|
return this.model[this.dbProperties.localKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async compute(query: ModelQuery<O>): Promise<O[]> {
|
||||||
|
this.query.where(this.dbProperties.foreignKey, this.getModelID());
|
||||||
|
return await query.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async eagerLoad(relations: ModelRelation<S, O, O[]>[]): Promise<ModelQueryResult<O>> {
|
||||||
|
this.query.where(
|
||||||
|
this.dbProperties.foreignKey,
|
||||||
|
relations.map(r => r.getModelID()).filter(id => id !== null && id !== undefined),
|
||||||
|
WhereTest.IN
|
||||||
|
);
|
||||||
|
return await this.query.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async populate(models: ModelQueryResult<O>): Promise<void> {
|
||||||
|
this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class ManyThroughModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O[]> {
|
||||||
|
protected readonly dbProperties: PivotRelationDatabaseProperties;
|
||||||
|
|
||||||
|
constructor(model: S, foreignModelClass: ModelClass<O>, dbProperties: PivotRelationDatabaseProperties) {
|
||||||
|
super(model, foreignModelClass);
|
||||||
|
this.dbProperties = dbProperties;
|
||||||
|
this.query
|
||||||
|
.leftJoin(`${this.dbProperties.pivotTable} as pivot`)
|
||||||
|
.on(`pivot.${this.dbProperties.foreignPivotKey}`, `${this.foreignModelClass.table}.${this.dbProperties.foreignKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clone(): ManyThroughModelRelation<S, O> {
|
||||||
|
return new ManyThroughModelRelation<S, O>(this.model, this.foreignModelClass, this.dbProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getModelID(): any {
|
||||||
|
return this.model[this.dbProperties.localKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async compute(query: ModelQuery<O>): Promise<O[]> {
|
||||||
|
this.query.where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelID());
|
||||||
|
return await query.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async eagerLoad(relations: ModelRelation<S, O, O[]>[]): Promise<ModelQueryResult<O>> {
|
||||||
|
this.query.where(
|
||||||
|
`pivot.${this.dbProperties.localPivotKey}`,
|
||||||
|
relations.map(r => r.getModelID()),
|
||||||
|
WhereTest.IN);
|
||||||
|
this.query.pivot(`pivot.${this.dbProperties.localPivotKey}`, `pivot.${this.dbProperties.foreignPivotKey}`);
|
||||||
|
return await this.query.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async populate(models: ModelQueryResult<O>): Promise<void> {
|
||||||
|
const ids = models.pivot!
|
||||||
|
.filter(p => p[this.dbProperties.localPivotKey] === this.getModelID())
|
||||||
|
.map(p => p[this.dbProperties.foreignPivotKey]);
|
||||||
|
this.cachedModels = models.filter(m => ids.indexOf(m[this.dbProperties.foreignKey]) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RelationDatabaseProperties = {
|
||||||
|
localKey: string;
|
||||||
|
foreignKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PivotRelationDatabaseProperties = RelationDatabaseProperties & {
|
||||||
|
pivotTable: string;
|
||||||
|
localPivotKey: string;
|
||||||
|
foreignPivotKey: string;
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import Model from "./Model";
|
import Model from "./Model";
|
||||||
import Query, {WhereTest} from "./Query";
|
import ModelQuery, {WhereTest} from "./ModelQuery";
|
||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import {Type} from "../Utils";
|
import {Type} from "../Utils";
|
||||||
|
|
||||||
@ -174,11 +174,11 @@ export default class Validator<T> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unique<M extends Model>(model: M | Type<M>, foreignKey?: string, querySupplier?: () => Query): Validator<T> {
|
public unique<M extends Model>(model: M | Type<M>, foreignKey?: string, querySupplier?: () => ModelQuery<M>): Validator<T> {
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: async (val, thingName, c) => {
|
verifyStep: async (val, thingName, c) => {
|
||||||
if (!foreignKey) foreignKey = thingName;
|
if (!foreignKey) foreignKey = thingName;
|
||||||
let query: Query;
|
let query: ModelQuery<M>;
|
||||||
if (querySupplier) {
|
if (querySupplier) {
|
||||||
query = querySupplier().where(foreignKey, val);
|
query = querySupplier().where(foreignKey, val);
|
||||||
} else {
|
} else {
|
||||||
|
@ -11,14 +11,14 @@ export default class Log extends Model {
|
|||||||
private error_stack?: string;
|
private error_stack?: string;
|
||||||
private created_at?: Date;
|
private created_at?: Date;
|
||||||
|
|
||||||
protected defineProperties(): void {
|
protected init(): void {
|
||||||
this.defineProperty<number>('level', new Validator<number>().defined());
|
this.addProperty<number>('level', new Validator<number>().defined());
|
||||||
this.defineProperty<string>('message', new Validator<string>().defined().between(0, 65535));
|
this.addProperty<string>('message', new Validator<string>().defined().between(0, 65535));
|
||||||
this.defineProperty<Buffer>('log_id', new Validator<Buffer>().acceptUndefined().length(16));
|
this.addProperty<Buffer>('log_id', new Validator<Buffer>().acceptUndefined().length(16));
|
||||||
this.defineProperty<string>('error_name', new Validator<string>().acceptUndefined().between(0, 128));
|
this.addProperty<string>('error_name', new Validator<string>().acceptUndefined().between(0, 128));
|
||||||
this.defineProperty<string>('error_message', new Validator<string>().acceptUndefined().between(0, 512));
|
this.addProperty<string>('error_message', new Validator<string>().acceptUndefined().between(0, 512));
|
||||||
this.defineProperty<string>('error_stack', new Validator<string>().acceptUndefined().between(0, 65535));
|
this.addProperty<string>('error_stack', new Validator<string>().acceptUndefined().between(0, 65535));
|
||||||
this.defineProperty<Date>('created_at', new Validator<Date>());
|
this.addProperty<Date>('created_at', new Validator<Date>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLevel(): LogLevelKeys {
|
public getLevel(): LogLevelKeys {
|
||||||
|
@ -8,10 +8,10 @@ class FakeDummyModel extends Model {
|
|||||||
public date?: Date;
|
public date?: Date;
|
||||||
public date_default?: Date;
|
public date_default?: Date;
|
||||||
|
|
||||||
protected defineProperties(): void {
|
protected init(): void {
|
||||||
this.defineProperty<string>('name', new Validator().acceptUndefined().between(3, 256));
|
this.addProperty<string>('name', new Validator().acceptUndefined().between(3, 256));
|
||||||
this.defineProperty<Date>('date', new Validator());
|
this.addProperty<Date>('date', new Validator());
|
||||||
this.defineProperty<Date>('date_default', new Validator());
|
this.addProperty<Date>('date_default', new Validator());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ describe('Model', () => {
|
|||||||
expect(instance.date?.getTime()).toBeCloseTo(date.getTime(), -4);
|
expect(instance.date?.getTime()).toBeCloseTo(date.getTime(), -4);
|
||||||
expect(instance.date_default).toBeDefined();
|
expect(instance.date_default).toBeDefined();
|
||||||
|
|
||||||
instance = await FakeDummyModel.getById('1');
|
instance = await FakeDummyModel.getById(1);
|
||||||
expect(instance).toBeDefined();
|
expect(instance).toBeDefined();
|
||||||
expect(instance!.id).toBe(1);
|
expect(instance!.id).toBe(1);
|
||||||
expect(instance!.name).toBe('name1');
|
expect(instance!.name).toBe('name1');
|
||||||
|
Loading…
Reference in New Issue
Block a user