Fix missing fields by default and fix model construction flow

This commit is contained in:
Alice Gaudon 2020-07-24 15:40:10 +02:00
parent 4b6829bfd6
commit c0dd48d064
11 changed files with 85 additions and 77 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "wms-core", "name": "wms-core",
"version": "0.19.0", "version": "0.19.1",
"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>",

View File

@ -1,6 +1,8 @@
import config from "config"; import config from "config";
import {v4 as uuid} from "uuid"; import {v4 as uuid} from "uuid";
import Log from "./models/Log"; import Log from "./models/Log";
import ModelFactory from "./db/ModelFactory";
import {bufferToUUID} from "./Utils";
export default class Logger { export default class Logger {
private static logLevel: LogLevelKeys = <LogLevelKeys>config.get<string>('log_level'); private static logLevel: LogLevelKeys = <LogLevelKeys>config.get<string>('log_level');
@ -71,19 +73,15 @@ export default class Logger {
} }
}).join(' '); }).join(' ');
const log = new Log({}); const shouldSaveToDB = levelIndex <= LogLevel[this.dbLogLevel];
log.setLevel(level);
log.message = computedMsg;
log.setError(error);
let logID = Buffer.alloc(16);
uuid({}, logID);
log.setLogID(logID);
let output = `[${level}] `; let output = `[${level}] `;
let pad = output.length; const pad = output.length;
if (levelIndex <= LogLevel[this.dbLogLevel]) output += `${log.getLogID()} - `;
const logID = Buffer.alloc(16);
uuid({}, logID);
if (shouldSaveToDB) output += `${logID} - `;
output += computedMsg.replace(/\n/g, '\n' + ' '.repeat(pad)); output += computedMsg.replace(/\n/g, '\n' + ' '.repeat(pad));
switch (level) { switch (level) {
@ -106,14 +104,19 @@ export default class Logger {
break; break;
} }
if (levelIndex <= LogLevel[this.dbLogLevel]) { if (shouldSaveToDB) {
const log = ModelFactory.get(Log).make({});
log.setLevel(level);
log.message = computedMsg;
log.setError(error);
log.setLogID(logID);
log.save().catch(err => { log.save().catch(err => {
if (!silent && err.message.indexOf('ECONNREFUSED') < 0) { if (!silent && err.message.indexOf('ECONNREFUSED') < 0) {
console.error({save_err: err, error}); console.error({save_err: err, error});
} }
}); });
} }
return log.getLogID(); return bufferToUUID(logID);
} }
return null; return null;
} }

View File

@ -35,3 +35,15 @@ export function cryptoRandomDictionary(size: number, dictionary: string): string
} }
export type Type<T> = { new(...args: any[]): T }; export type Type<T> = { new(...args: any[]): T };
export function bufferToUUID(buffer: Buffer): string {
const chars = buffer.toString('hex');
let out = '';
let i = 0;
for (const l of [8, 4, 4, 4, 12]) {
if (i > 0) out += '-';
out += chars.substr(i, l);
i += l;
}
return out;
}

View File

@ -24,13 +24,14 @@ export default class MagicLink extends Model implements AuthProof {
return config.get<number>('magic_link.validity_period') * 1000; return config.get<number>('magic_link.validity_period') * 1000;
} }
private session_id?: string; public readonly id?: number = undefined;
private email?: string; private session_id?: string = undefined;
private token?: string; private email?: string = undefined;
private action_type?: string; private token?: string = undefined;
private original_url?: string; private action_type?: string = undefined;
private generated_at?: Date; private original_url?: string = undefined;
private authorized?: boolean; private generated_at?: Date = undefined;
private authorized?: boolean = undefined;
constructor(data: any) { constructor(data: any) {
super(data); super(data);

View File

@ -12,10 +12,11 @@ export default class User extends Model {
return config.get<boolean>('approval_mode') && MysqlConnectionManager.hasMigration(AddApprovedFieldToUsersTable); return config.get<boolean>('approval_mode') && MysqlConnectionManager.hasMigration(AddApprovedFieldToUsersTable);
} }
public name?: string; public readonly id?: number = undefined;
public is_admin!: boolean; public name?: string = undefined;
public created_at?: Date; public is_admin: boolean = false;
public updated_at?: Date; public created_at?: Date = undefined;
public updated_at?: Date = undefined;
public readonly emails = new ManyModelRelation(this, ModelFactory.get(UserEmail), { public readonly emails = new ManyModelRelation(this, ModelFactory.get(UserEmail), {
localKey: 'id', localKey: 'id',
@ -26,8 +27,6 @@ export default class User extends Model {
public constructor(data: any) { public constructor(data: any) {
super(data); super(data);
if (this.approved === undefined) this.approved = false;
if (this.is_admin === undefined) this.is_admin = false;
} }
protected init(): void { protected init(): void {

View File

@ -2,5 +2,5 @@ import ModelComponent from "../../db/ModelComponent";
import User from "./User"; import User from "./User";
export default class UserApprovedComponent extends ModelComponent<User> { export default class UserApprovedComponent extends ModelComponent<User> {
public approved!: boolean; public approved: boolean = false;
} }

View File

@ -6,10 +6,11 @@ import {OneModelRelation} from "../../db/ModelRelation";
import ModelFactory from "../../db/ModelFactory"; import ModelFactory from "../../db/ModelFactory";
export default class UserEmail extends Model { export default class UserEmail extends Model {
public user_id?: number; public readonly id?: number = undefined;
public readonly email!: string; public user_id?: number = undefined;
private main!: boolean; public readonly email?: string = undefined;
public created_at?: Date; private main?: boolean = undefined;
public created_at?: Date = undefined;
public readonly user = new OneModelRelation<UserEmail, User>(this, ModelFactory.get(User), { public readonly user = new OneModelRelation<UserEmail, User>(this, ModelFactory.get(User), {
localKey: 'user_id', localKey: 'user_id',
@ -36,7 +37,7 @@ export default class UserEmail extends Model {
} }
public isMain(): boolean { public isMain(): boolean {
return this.main; return Boolean(this.main);
} }
public setMain() { public setMain() {

View File

@ -29,19 +29,16 @@ export default abstract class Model {
return ModelFactory.get(this).paginate(request, perPage, query); return ModelFactory.get(this).paginate(request, perPage, query);
} }
protected readonly _factory!: ModelFactory<any>; protected readonly _factory: ModelFactory<any>;
private readonly _components: ModelComponent<any>[] = []; private readonly _components: ModelComponent<any>[] = [];
private readonly _validators: { [key: string]: Validator<any> } = {}; private readonly _validators: { [key: string]: Validator<any> } = {};
[key: string]: any; [key: string]: any;
public constructor(data: any) { public constructor(factory: ModelFactory<any>) {
if (!factory || !(factory instanceof ModelFactory)) throw new Error('Cannot instantiate model directly.');
this._factory = factory;
this.init(); this.init();
this.updateWithData(data);
}
public setFactory(factory: ModelFactory<any>) {
(this as any)._factory = factory;
} }
protected abstract init(): void; protected abstract init(): void;
@ -66,7 +63,7 @@ export default abstract class Model {
throw new Error(`Component ${type.name} was not initialized for this ${this.constructor.name}.`); throw new Error(`Component ${type.name} was not initialized for this ${this.constructor.name}.`);
} }
private updateWithData(data: any) { public updateWithData(data: any) {
for (const property of this._properties) { for (const property of this._properties) {
if (data[property] !== undefined) { if (data[property] !== undefined) {
this[property] = data[property]; this[property] = data[property];
@ -128,7 +125,7 @@ export default abstract class Model {
} }
} }
let query = this._factory.update(data); let query = this._factory.update(data);
for (const indexField of this.getPrimaryKeyFields()) { for (const indexField of this._factory.getPrimaryKeyFields()) {
query = query.where(indexField, this[indexField]); query = query.where(indexField, this[indexField]);
} }
await query.execute(connection); await query.execute(connection);
@ -154,7 +151,7 @@ export default abstract class Model {
public async exists(): Promise<boolean> { public async exists(): Promise<boolean> {
let query = this._factory.select('1'); let query = this._factory.select('1');
for (const indexField of this.getPrimaryKeyFields()) { for (const indexField of this._factory.getPrimaryKeyFields()) {
query = query.where(indexField, this[indexField]); query = query.where(indexField, this[indexField]);
} }
return (await query.limit(1).execute()).results.length > 0; return (await query.limit(1).execute()).results.length > 0;
@ -164,7 +161,7 @@ export default abstract class Model {
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 = this._factory.delete(); let query = this._factory.delete();
for (const indexField of this.getPrimaryKeyFields()) { for (const indexField of this._factory.getPrimaryKeyFields()) {
query = query.where(indexField, this[indexField]); query = query.where(indexField, this[indexField]);
} }
await query.execute(); await query.execute();

View File

@ -25,22 +25,21 @@ export default class ModelFactory<T extends Model> {
this.modelType = modelType; this.modelType = modelType;
} }
public addComponent(modelComponentFactory: ModelComponentFactory<T>) { public addComponent(modelComponentFactory: ModelComponentFactory<T>) {
this.components.push(modelComponentFactory); this.components.push(modelComponentFactory);
} }
public make(data: any): T { public make(data: any): T {
const model = new this.modelType(data); const model = new this.modelType(this, data);
for (const component of this.components) { for (const component of this.components) {
model.addComponent(new component(model)); model.addComponent(new component(model));
} }
model.setFactory(this); model.updateWithData(data);
return model; return model;
} }
public get table(): string { public get table(): string {
return this.constructor.name return this.modelType.name
.replace(/(?:^|\.?)([A-Z])/g, (x, y) => '_' + y.toLowerCase()) .replace(/(?:^|\.?)([A-Z])/g, (x, y) => '_' + y.toLowerCase())
.replace(/^_/, '') .replace(/^_/, '')
+ 's'; + 's';

View File

@ -1,14 +1,16 @@
import Model from "../db/Model"; import Model from "../db/Model";
import {LogLevel, LogLevelKeys} from "../Logger"; import {LogLevel, LogLevelKeys} from "../Logger";
import {bufferToUUID} from "../Utils";
export default class Log extends Model { export default class Log extends Model {
private level?: number; public readonly id?: number = undefined;
public message?: string; private level?: number = undefined;
private log_id?: Buffer; public message?: string = undefined;
private error_name?: string; private log_id?: Buffer = undefined;
private error_message?: string; private error_name?: string = undefined;
private error_stack?: string; private error_message?: string = undefined;
private created_at?: Date; private error_stack?: string = undefined;
private created_at?: Date = undefined;
protected init(): void { protected init(): void {
this.setValidation('level').defined(); this.setValidation('level').defined();
@ -29,16 +31,7 @@ export default class Log extends Model {
} }
public getLogID(): string | null { public getLogID(): string | null {
if (!this.log_id) return null; return this.log_id ? bufferToUUID(this.log_id!) : null;
const chars = this.log_id!.toString('hex');
let out = '';
let i = 0;
for (const l of [8, 4, 4, 4, 12]) {
if (i > 0) out += '-';
out += chars.substr(i, l);
i += l;
}
return out;
} }
public setLogID(buffer: Buffer) { public setLogID(buffer: Buffer) {

View File

@ -1,22 +1,22 @@
import MysqlConnectionManager from "../src/db/MysqlConnectionManager"; import MysqlConnectionManager from "../src/db/MysqlConnectionManager";
import Model from "../src/db/Model"; import Model from "../src/db/Model";
import Validator from "../src/db/Validator";
import {MIGRATIONS} from "./_migrations"; import {MIGRATIONS} from "./_migrations";
import ModelFactory from "../src/db/ModelFactory";
class FakeDummyModel extends Model { class FakeDummyModel extends Model {
public name?: string; public id?: number = undefined;
public date?: Date; public name?: string = undefined;
public date_default?: Date; public date?: Date = undefined;
public date_default?: Date = undefined;
protected init(): void { protected init(): void {
this.addProperty<string>('name', new Validator().acceptUndefined().between(3, 256)); this.setValidation('name').acceptUndefined().between(3, 256);
this.addProperty<Date>('date', new Validator());
this.addProperty<Date>('date_default', new Validator());
} }
} }
beforeAll(async (done) => { beforeAll(async (done) => {
MysqlConnectionManager.registerMigrations(MIGRATIONS); MysqlConnectionManager.registerMigrations(MIGRATIONS);
ModelFactory.register(FakeDummyModel);
await MysqlConnectionManager.prepare(); await MysqlConnectionManager.prepare();
done(); done();
}); });
@ -28,13 +28,15 @@ afterAll(async (done) => {
describe('Model', () => { describe('Model', () => {
it('should have a proper table name', async () => { it('should have a proper table name', async () => {
expect(FakeDummyModel.table).toBe('fake_dummy_models'); const factory = ModelFactory.get(FakeDummyModel);
expect(new FakeDummyModel({}).table).toBe('fake_dummy_models'); expect(factory.table).toBe('fake_dummy_models');
expect(factory.make({}).table).toBe('fake_dummy_models');
}); });
it('should insert and retrieve properly', async () => { it('should insert and retrieve properly', async () => {
await MysqlConnectionManager.query(`DROP TABLE IF EXISTS ${FakeDummyModel.table}`); const factory = ModelFactory.get(FakeDummyModel);
await MysqlConnectionManager.query(`CREATE TABLE ${FakeDummyModel.table}( await MysqlConnectionManager.query(`DROP TABLE IF EXISTS ${(factory.table)}`);
await MysqlConnectionManager.query(`CREATE TABLE ${(factory.table)}(
id INT NOT NULL AUTO_INCREMENT, id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(256), name VARCHAR(256),
date DATETIME, date DATETIME,
@ -43,11 +45,12 @@ describe('Model', () => {
)`); )`);
const date = new Date(569985); const date = new Date(569985);
let instance: FakeDummyModel | null = new FakeDummyModel({ let instance: FakeDummyModel | null = factory.make({
name: 'name1', name: 'name1',
date: date, date: date,
}); });
console.log(instance)
await instance.save(); await instance.save();
expect(instance.id).toBe(1); expect(instance.id).toBe(1);
expect(instance.name).toBe('name1'); expect(instance.name).toBe('name1');