Make model primaryKey dynamic (allows for composite primary keys)

This commit is contained in:
Alice Gaudon 2020-06-04 14:59:23 +02:00
parent b85fbe6c21
commit 0970ff3116
5 changed files with 60 additions and 30 deletions

View File

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

View File

@ -9,7 +9,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> {

View File

@ -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.`;

View File

@ -6,7 +6,7 @@ import {Request} from "express";
import Pagination from "../Pagination"; import Pagination from "../Pagination";
export default abstract class Model { export default abstract class Model {
public static async getById<T extends Model>(id: number): Promise<T | null> { public static async getById<T extends Model>(id: string): Promise<T | null> {
const cachedModel = ModelCache.get(this.table, id); const cachedModel = ModelCache.get(this.table, id);
if (cachedModel?.constructor === this) { if (cachedModel?.constructor === this) {
return <T>cachedModel; return <T>cachedModel;
@ -48,7 +48,7 @@ export default abstract class Model {
const models: T[] = []; const models: T[] = [];
const factory = this.getFactory<T>(); const factory = this.getFactory<T>();
for (const result of results.results) { 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) { if (cachedModel && cachedModel.constructor === this) {
cachedModel.updateWithData(result); cachedModel.updateWithData(result);
models.push(<T>cachedModel); models.push(<T>cachedModel);
@ -61,6 +61,14 @@ export default abstract class Model {
return models; 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<T extends Model>(models: T[], relation: string, model: Function, localField: string) { public static async loadRelation<T extends Model>(models: T[], relation: string, model: Function, localField: string) {
const loadMap: { [p: number]: (model: T) => void } = {}; const loadMap: { [p: number]: (model: T) => void } = {};
const ids = models.map(m => { const ids = models.map(m => {
@ -85,15 +93,27 @@ export default abstract class Model {
protected readonly properties: ModelProperty<any>[] = []; protected readonly properties: ModelProperty<any>[] = [];
private readonly relations: { [p: string]: (Model | null) } = {}; private readonly relations: { [p: string]: (Model | null) } = {};
public id?: number; public id?: number;
private readonly automaticIdProperty: boolean;
[key: string]: any; [key: string]: any;
public constructor(data: any) { public constructor(data: any, automaticIdProperty: boolean = true) {
this.defineProperty<number>('id', new Validator()); this.automaticIdProperty = automaticIdProperty;
if (automaticIdProperty) {
this.defineProperty<number>('id', new Validator());
}
this.defineProperties(); this.defineProperties();
this.updateWithData(data); this.updateWithData(data);
} }
public getPrimaryKey(): string {
return (<any>this.constructor).getPrimaryKey(this);
}
public getPrimaryKeyFields(): string {
return (<any>this.constructor).getPrimaryKeyFields();
}
protected abstract defineProperties(): void; protected abstract defineProperties(): void;
protected defineProperty<T>(name: string, validator?: Validator<T> | RegExp) { protected defineProperty<T>(name: string, validator?: Validator<T> | RegExp) {
@ -112,7 +132,7 @@ export default abstract class Model {
} }
private updateWithData(data: any) { private updateWithData(data: any) {
this.id = data['id']; if (this.automaticIdProperty) this.id = data['id'];
for (const prop of this.properties) { for (const prop of this.properties) {
if (data[prop.name] !== undefined) { if (data[prop.name] !== undefined) {
@ -141,7 +161,7 @@ export default abstract class Model {
const callback = async () => { const callback = async () => {
if (needs_full_update) { if (needs_full_update) {
this.updateWithData((await (<Model><unknown>this.constructor).select().where('id', this.id!).first().execute()).results[0]); this.updateWithData((await (<any>this.constructor).select().where('id', this.id!).first().execute()).results[0]);
} }
if (!exists) { if (!exists) {
@ -169,16 +189,19 @@ export default abstract class Model {
const values = []; const values = [];
if (exists) { if (exists) {
const data: any = {};
for (const prop of this.properties) { for (const prop of this.properties) {
if (prop.value !== undefined) { if (prop.value !== undefined) {
props.push(prop.name + '=?'); data[prop.name] = prop.value;
values.push(prop.value);
} else { } else {
needs_full_update = true; needs_full_update = true;
} }
} }
values.push(this.id); let query = Query.update(this.table, data);
await query(`UPDATE ${this.table} SET ${props.join(',')} WHERE id=?`, values, connection); for (const indexField of this.getPrimaryKeyFields()) {
query = query.where(indexField, this[indexField]);
}
await query.execute(connection);
} else { } else {
const props_holders = []; const props_holders = [];
for (const prop of this.properties) { 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); 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; return needs_full_update;
@ -213,20 +236,25 @@ export default abstract class Model {
public async exists(): Promise<boolean> { public async exists(): Promise<boolean> {
if (!this.id) return false; if (!this.id) return false;
const result = await query(`SELECT 1 FROM ${this.table} WHERE id=? LIMIT 1`, [ let query = Query.select(this.table, '1');
this.id, 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; 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.');
await query(`DELETE FROM ${this.table} WHERE id=?`, [ let query = Query.delete(this.table);
this.id, for (const indexField of this.getPrimaryKeyFields()) {
]); query = query.where(indexField, this[indexField]);
}
await query.execute();
ModelCache.forget(this); ModelCache.forget(this);
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[]> {
@ -273,38 +301,40 @@ class ModelProperty<T> {
export class ModelCache { export class ModelCache {
private static readonly caches: { private static readonly caches: {
[key: string]: { [key: string]: {
[key: number]: Model [key: string]: Model
} }
} = {}; } = {};
public static cache(instance: 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]; let tableCache = this.caches[instance.table];
if (!tableCache) 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) { 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]; let tableCache = this.caches[instance.table];
if (!tableCache) return; if (!tableCache) return;
if (tableCache[instance.id]) delete tableCache[instance.id]; if (tableCache[primaryKey]) delete tableCache[primaryKey];
} }
public static all(table: string): { public static all(table: string): {
[key: number]: Model [key: string]: Model
} | undefined { } | undefined {
return this.caches[table]; 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); const tableCache = this.all(table);
if (!tableCache) return undefined; if (!tableCache) return undefined;
return tableCache[id]; return tableCache[primaryKey];
} }
} }

View File

@ -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');