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",
"version": "0.4.35",
"version": "0.5.0",
"description": "Node web framework",
"repository": "git@gitlab.com:ArisuOngaku/wms-core.git",
"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> {
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> {

View File

@ -85,7 +85,7 @@ export default abstract class MagicLinkController extends Controller {
let success = true;
let err;
const magicLink = await MagicLink.getById<MagicLink>(id);
const magicLink = await MagicLink.getById<MagicLink>(`${id}`);
if (!magicLink) {
res.status(404);
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";
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);
if (cachedModel?.constructor === this) {
return <T>cachedModel;
@ -48,7 +48,7 @@ export default abstract class Model {
const models: T[] = [];
const factory = this.getFactory<T>();
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(<T>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<T extends Model>(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<any>[] = [];
private readonly relations: { [p: string]: (Model | null) } = {};
public id?: number;
private readonly automaticIdProperty: boolean;
[key: string]: any;
public constructor(data: any) {
this.defineProperty<number>('id', new Validator());
public constructor(data: any, automaticIdProperty: boolean = true) {
this.automaticIdProperty = automaticIdProperty;
if (automaticIdProperty) {
this.defineProperty<number>('id', new Validator());
}
this.defineProperties();
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 defineProperty<T>(name: string, validator?: Validator<T> | 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 (<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) {
@ -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<boolean> {
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<void> {
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<void[]> {
@ -273,38 +301,40 @@ class ModelProperty<T> {
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];
}
}

View File

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