All files / src base-model.ts

100% Statements 49/49
90.48% Branches 38/42
100% Functions 18/18
100% Lines 40/40
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146        1x 1x                                   1x             3x               2x 1x 1x     1x               2x   1x 1x                     26x   8x 5x     2x 2x 1x                       13x   3x 2x     2x 2x 1x                     13x   4x 3x     2x 2x 1x                       26x 26x 5x     5x 5x     26x   4x 3x     2x 2x 1x        
/**
 * Represents a wrapped constructor of a model, which is essentially
 * just the constructor with some merged definitions that add static methods.
 */
import QueryBuilder from "./query/builder";
import { getForeignKey, getPivotTableName } from "./util";
 
export type Wrapped<T, A> = T & {
    readonly tableName: string;
} & QueryBuilder<A>;
 
export type R<T> = QueryBuilder<T> & (() => Promise<T>);
export type A<T> = QueryBuilder<T> & (() => Promise<T[]>);
 
/**
 * Represents the types currently supported natively.
 */
export type DatabaseType = number | string | boolean;
export type KeyedDatabaseResult = { [key: string]: DatabaseType };
 
/**
 * Represents an abstract base model that every model inherits from.
 */
export default abstract class BaseModel {
    readonly id: number;
 
    /**
     * Returns a new builder that contains a WHERE clause for the current id.
     */
    public builder(): QueryBuilder<this> {
        return QueryBuilder.table<any>(Object.getPrototypeOf(this).tableName).where("id", this.id);
    }
 
    /**
     * Saves this object, inserting it if it doesn't exist or updating it otherwise.
     */
    public save(): Promise<void> {
        // Insert
        if (typeof this.id === "undefined") {
            return this.builder().insertAndGetId(this).then(id => {
                (<any>this).id = id;
            });
        } else {
            return this.builder().update(this);
        }
    }
 
    /**
     * Deletes this object, throwing if it is not currently stored.
     */
    public delete(): Promise<void> {
        if (typeof this.id === "undefined") throw new Error("Cannot delete object if it is not in the database.");
 
        return this.builder().delete().then(x => {
            (<any>this).id = undefined;
        });
    }
 
    /**
     * Indicates that this object has a single associated object of the provided
     * model type and optional foreign key (which will default to the name of the
     * current model with _id).
     */
    public hasOne<A, T extends Wrapped<any, A>>(model: T, foreignKey?: string): R<A> {
        let instance: Promise<A>;
        return <any>new Proxy(/* istanbul ignore next */ () => {}, {
            apply: () => {
                if (instance) return instance;
                return instance = model.where(foreignKey || getForeignKey(this.constructor.name), this.id).limit(1).first();
            },
            get: (obj: any, key) => {
                const builder: any = model.where(foreignKey || getForeignKey(this.constructor.name), this.id).limit(1);
                if (typeof builder[key] === "undefined") return undefined;
                return typeof builder[key] === "function" ? builder[key].bind(builder) : builder[key];
            }
        });
    }
 
    /**
     * Indicates that this object has a single associated object of the provided
     * model type. The object will be looked up by looking at the value of the specified
     * column and finding the row of the specified model with that ID.
     */
    public belongsTo<A, T extends Wrapped<any, A>, K extends keyof this>(model: T, foreignKey?: K): R<A> {
        let instance: Promise<A>;
        return <any>new Proxy(/* istanbul ignore next */ () => {}, {
            apply: () => {
                if (instance) return instance;
                return instance = model.where("id", (<any>this)[foreignKey || getForeignKey(model.name)]).first();
            },
            get: (obj: any, key) => {
                const builder: any = model.where("id", (<any>this)[foreignKey || getForeignKey(model.name)]);
                if (typeof builder[key] === "undefined") return undefined;
                return typeof builder[key] === "function" ? builder[key].bind(builder) : builder[key];
            }
        });
    }
 
    /**
     * Relationship that finds all members of the specified model table with their
     * foreign key set to the ID of the current object.
     */
    public hasMany<Q, T extends Wrapped<any, Q>>(model: T, foreignKey?: string): A<Q> {
        let instance: Promise<Q[]>;
        return <any>new Proxy(/* istanbul ignore next */ () => {}, {
            apply: () => {
                if (instance) return instance;
                return instance = model.where(foreignKey || getForeignKey(this.constructor.name), this.id).all();
            },
            get: (obj: any, key) => {
                const builder: any = model.where(foreignKey || getForeignKey(this.constructor.name), this.id);
                if (typeof builder[key] === "undefined") return undefined;
                return typeof builder[key] === "function" ? builder[key].bind(builder) : builder[key];
            }
        });
    }
 
    /**
     * Relationship that functions as a hasMany going both ways. This requires a pivot table
     * which joins the two models together.
     */
    public hasAndBelongsToMany<Q, T extends Wrapped<any, Q>>(model: T, pivotName?: string): A<Q> {
        let instance: Promise<Q[]>;
 
        const pivot = pivotName || getPivotTableName(this.constructor.name, model.name);
        const createBuilder = () => {
            const b = model
                .join(pivot, `${model.tableName}.id`, "=", `${pivot}.${getForeignKey(model.name)}`)
                .where(`${pivot}.${getForeignKey(this.constructor.name)}`, this.id);
            b.modelConstructor = model;
            return b;
        };
 
        return <any>new Proxy(/* istanbul ignore next */ () => {}, {
            apply: () => {
                if (instance) return instance;
                return instance = createBuilder().all();
            },
            get: (obj: any, key) => {
                const builder: any = createBuilder();
                if (typeof builder[key] === "undefined") return undefined;
                return typeof builder[key] === "function" ? builder[key].bind(builder) : builder[key];
            }
        });
    }
}