I'm trying to create an inheritance based base models for my application. The goal being that the base model has similar functions to ActiveRecord in Rails present and a new model can just extend that but I'm running into a few issues.
I'm using purely kysely and I'm not going to add any ORMs. Firstly I'm thinking of doing something like:
import { Exercise } from "../backend/interface/Exercise";
import { BaseModel } from "../BaseModel"
export class ExerciseModel extends BaseModel<Exercise> {
constructor() {
super("exercise");
}
}
rather than this model:
import { Exercise } from "../../interface/Exercise/Exercise";
export class ExerciseModel {
static async findById(id: number) {
const result = await db
.selectFrom('exercise')
.where('id', '=', id)
.execute()
return result[0] as Exercise
}
static async delete(id: number) {
await db.deleteFrom("exercise").where("id", "=", id).execute();
}
static async findAll() {
const result = await db.selectFrom("exercise").execute();
return result as Exercise[];
}
static async update(id: number, name: string) {
await db
.updateTable("exercise")
.set({ name })
.where("id", "=", id)
.execute();
}
static async create(name: string, muscle_group_id: number) {
await db
.insertInto("exercise")
.values({
name: name,
muscle_group_id: muscle_group_id
})
.returningAll()
.executeTakeFirstOrThrow();
}
}
which works however apart from the string name of the database being provided (ok the create and update but I could just put them in the ExerciseModel and not the BaseModel), everything would have to be replicated for all my models.
I tried doing something like this:
export class BaseModel {
protected readonly tableName: string;
constructor(tablename: string){
this.tableName = tablename
}
static async findById(id: number) {
const result = await db
.selectFrom(this.tableName)
.where('id', '=', id)
.execute()
return result[0]
}
static tableName(tableName: any) {
throw new Error("Method not implemented.");
}
static async delete(id: number) {
await db.deleteFrom(this.tableName).where('id', '=', id).execute()
}
}
But I ran into errors:
Overload 1 of 2, '(from: TableExpression<Models, "exercise">[]): SelectQueryBuilder<From<Models, TableExpression<Models, "exercise">>, any, {}>', gave the following error.
Argument of type '(tableName: any) => void' is not assignable to parameter of type 'TableExpression<Models, "exercise">[]'.
Overload 2 of 2, '(from: TableExpression<Models, "exercise">): SelectQueryBuilder<From<Models, TableExpression<Models, "exercise">>, any, {}>', gave the following error.
Argument of type '(tableName: any) => void' is not assignable to parameter of type 'TableExpression<Models, "exercise">'.
For reference the db object was created like
import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
import * as dotenv from 'dotenv'
import { Models } from "./models/Models";
dotenv.config()
export const db = new Kysely<Models>({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: process.env.DATABASE_URL,
}),
}),
});
while the Models interface looks like this for the time being
export interface Models{
exercise: Exercise
}
I would love to see better ideas as well, but any help would be appreciated
Here's one way to implement it -
Here's a Playground link.
NOTE: The class members are not static, because Typescript itself is a static language, and it determines types statically, so if you have static methods, the Generic type is never inferred and you'll end up getting intellisense for the all the models for each method at once, even the return types, so it's best to instantiate the class and use its member functions.