From 0970ff31162f7f768f2d491b554dfab65dba7fc2 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Thu, 4 Jun 2020 14:59:23 +0200 Subject: [PATCH] Make model primaryKey dynamic (allows for composite primary keys) --- package.json | 2 +- src/auth/AuthGuard.ts | 2 +- src/auth/magic_link/MagicLinkController.ts | 2 +- src/db/Model.ts | 82 +++++++++++++++------- test/Model.test.ts | 2 +- 5 files changed, 60 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 3f2ea2b..50e39d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wms-core", - "version": "0.4.35", + "version": "0.5.0", "description": "Node web framework", "repository": "git@gitlab.com:ArisuOngaku/wms-core.git", "author": "Alice Gaudon ", diff --git a/src/auth/AuthGuard.ts b/src/auth/AuthGuard.ts index 778c7c4..0cde25a 100644 --- a/src/auth/AuthGuard.ts +++ b/src/auth/AuthGuard.ts @@ -9,7 +9,7 @@ export default abstract class AuthGuard

{ public async getUserForSession(session: Express.Session): Promise { if (!await this.isAuthenticated(session)) return null; - return await User.getById(session.auth_id); + return await User.getById(`${session.auth_id}`); } public async authenticateOrRegister(session: Express.Session, proof: P, registerCallback?: (connection: Connection, userID: number) => Promise<(() => Promise)[]>): Promise { diff --git a/src/auth/magic_link/MagicLinkController.ts b/src/auth/magic_link/MagicLinkController.ts index d9c3c6f..758b8a1 100644 --- a/src/auth/magic_link/MagicLinkController.ts +++ b/src/auth/magic_link/MagicLinkController.ts @@ -85,7 +85,7 @@ export default abstract class MagicLinkController extends Controller { let success = true; let err; - const magicLink = await MagicLink.getById(id); + const magicLink = await MagicLink.getById(`${id}`); if (!magicLink) { res.status(404); err = `Couldn't find this magic link. Perhaps it has already expired.`; diff --git a/src/db/Model.ts b/src/db/Model.ts index ea0ef37..c4c2fc1 100644 --- a/src/db/Model.ts +++ b/src/db/Model.ts @@ -6,7 +6,7 @@ import {Request} from "express"; import Pagination from "../Pagination"; export default abstract class Model { - public static async getById(id: number): Promise { + public static async getById(id: string): Promise { const cachedModel = ModelCache.get(this.table, id); if (cachedModel?.constructor === this) { return cachedModel; @@ -48,7 +48,7 @@ export default abstract class Model { const models: T[] = []; const factory = this.getFactory(); for (const result of results.results) { - const cachedModel = ModelCache.get(this.table, result.id); + const cachedModel = ModelCache.get(this.table, this.getPrimaryKey(result)); if (cachedModel && cachedModel.constructor === this) { cachedModel.updateWithData(result); models.push(cachedModel); @@ -61,6 +61,14 @@ export default abstract class Model { return models; } + protected static getPrimaryKey(modelData: any): string { + return this.getPrimaryKeyFields().map(f => `${modelData[f]}`).join(','); + } + + protected static getPrimaryKeyFields(): string[] { + return ['id']; + } + public static async loadRelation(models: T[], relation: string, model: Function, localField: string) { const loadMap: { [p: number]: (model: T) => void } = {}; const ids = models.map(m => { @@ -85,15 +93,27 @@ export default abstract class Model { protected readonly properties: ModelProperty[] = []; private readonly relations: { [p: string]: (Model | null) } = {}; public id?: number; + private readonly automaticIdProperty: boolean; [key: string]: any; - public constructor(data: any) { - this.defineProperty('id', new Validator()); + public constructor(data: any, automaticIdProperty: boolean = true) { + this.automaticIdProperty = automaticIdProperty; + if (automaticIdProperty) { + this.defineProperty('id', new Validator()); + } this.defineProperties(); this.updateWithData(data); } + public getPrimaryKey(): string { + return (this.constructor).getPrimaryKey(this); + } + + public getPrimaryKeyFields(): string { + return (this.constructor).getPrimaryKeyFields(); + } + protected abstract defineProperties(): void; protected defineProperty(name: string, validator?: Validator | RegExp) { @@ -112,7 +132,7 @@ export default abstract class Model { } private updateWithData(data: any) { - this.id = data['id']; + if (this.automaticIdProperty) this.id = data['id']; for (const prop of this.properties) { if (data[prop.name] !== undefined) { @@ -141,7 +161,7 @@ export default abstract class Model { const callback = async () => { if (needs_full_update) { - this.updateWithData((await (this.constructor).select().where('id', this.id!).first().execute()).results[0]); + this.updateWithData((await (this.constructor).select().where('id', this.id!).first().execute()).results[0]); } if (!exists) { @@ -169,16 +189,19 @@ export default abstract class Model { const values = []; if (exists) { + const data: any = {}; for (const prop of this.properties) { if (prop.value !== undefined) { - props.push(prop.name + '=?'); - values.push(prop.value); + data[prop.name] = prop.value; } else { needs_full_update = true; } } - values.push(this.id); - await query(`UPDATE ${this.table} SET ${props.join(',')} WHERE id=?`, values, connection); + let query = Query.update(this.table, data); + for (const indexField of this.getPrimaryKeyFields()) { + query = query.where(indexField, this[indexField]); + } + await query.execute(connection); } else { const props_holders = []; for (const prop of this.properties) { @@ -192,7 +215,7 @@ export default abstract class Model { } const result = await query(`INSERT INTO ${this.table} (${props.join(', ')}) VALUES(${props_holders.join(', ')})`, values, connection); - this.id = result.other.insertId; + if (this.automaticIdProperty) this.id = result.other.insertId; } return needs_full_update; @@ -213,20 +236,25 @@ export default abstract class Model { public async exists(): Promise { if (!this.id) return false; - const result = await query(`SELECT 1 FROM ${this.table} WHERE id=? LIMIT 1`, [ - this.id, - ]); + let query = Query.select(this.table, '1'); + for (const indexField of this.getPrimaryKeyFields()) { + query = query.where(indexField, this[indexField]); + } + query = query.first(); + const result = await query.execute(); return result.results.length > 0; } public async delete(): Promise { if (!(await this.exists())) throw new Error('This model instance doesn\'t exist in DB.'); - await query(`DELETE FROM ${this.table} WHERE id=?`, [ - this.id, - ]); + let query = Query.delete(this.table); + for (const indexField of this.getPrimaryKeyFields()) { + query = query.where(indexField, this[indexField]); + } + await query.execute(); ModelCache.forget(this); - this.id = undefined; + if (this.automaticIdProperty) this.id = undefined; } public async validate(onlyFormat: boolean = false, connection?: Connection): Promise { @@ -273,38 +301,40 @@ class ModelProperty { export class ModelCache { private static readonly caches: { [key: string]: { - [key: number]: Model + [key: string]: Model } } = {}; public static cache(instance: Model) { - if (instance.id === undefined) throw new Error('Cannot cache an instance with an undefined id.'); + 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[instance.id]) tableCache[instance.id] = instance; + if (!tableCache[primaryKey]) tableCache[primaryKey] = instance; } public static forget(instance: Model) { - if (instance.id === undefined) throw new Error('Cannot forget an instance with an undefined id.'); + 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[instance.id]) delete tableCache[instance.id]; + if (tableCache[primaryKey]) delete tableCache[primaryKey]; } public static all(table: string): { - [key: number]: Model + [key: string]: Model } | undefined { return this.caches[table]; } - public static get(table: string, id: number): Model | undefined { + public static get(table: string, primaryKey: string): Model | undefined { const tableCache = this.all(table); if (!tableCache) return undefined; - return tableCache[id]; + return tableCache[primaryKey]; } } diff --git a/test/Model.test.ts b/test/Model.test.ts index 2f488b5..53e4e35 100644 --- a/test/Model.test.ts +++ b/test/Model.test.ts @@ -54,7 +54,7 @@ describe('Model', () => { expect(instance.date?.getTime()).toBeCloseTo(date.getTime(), -4); expect(instance.date_default).toBeDefined(); - instance = await FakeDummyModel.getById(1); + instance = await FakeDummyModel.getById('1'); expect(instance).toBeDefined(); expect(instance!.id).toBe(1); expect(instance!.name).toBe('name1');