Table Abstraction

Hi all, I'm new to drizzle through sveltekit. I'm a big fan of declarative abstraction, and I would like to make a class to wrap around tables. Essentially, I would like it to apply default columns, like id, created, updated, archived, lifetime etc. so all tables have the same default structure, along with their other structure. (Obviously I wouldn't do this with linking tables and stuff). How could I do this with 100% type safety? My end goal is for it to communicate through websockets and update the front end model in real time for all clients, handle permissions, and have an event emitter system. It would handle the "updated" column, and would delete if it's passed the data's lifetime. I also would like to integrate my rust-like "Errors as Types" code on top, along with a lot of other features. However, I need to start with the fundamentals and see if this is even possible. Here's some pseudocode for what I'm thinking
class TableCreator<T extends WHAT_GOES_HERE> {
public readonly table: PgDrizzleTableType;
constructor(name: string, colmap: SomeType<T>) {
this.table = pgTable(name, {
...colmap,
id: text('id').primaryKey(),
// ... rest
});
}
// Here's why I want this:
fromId(id: string): Promise<Result<SomeGoodTypeHandler<T>>> {
try {
return new Ok(/* Grab from id */);
} catch (e) {
return new Err(e);
}
}
}
class TableCreator<T extends WHAT_GOES_HERE> {
public readonly table: PgDrizzleTableType;
constructor(name: string, colmap: SomeType<T>) {
this.table = pgTable(name, {
...colmap,
id: text('id').primaryKey(),
// ... rest
});
}
// Here's why I want this:
fromId(id: string): Promise<Result<SomeGoodTypeHandler<T>>> {
try {
return new Ok(/* Grab from id */);
} catch (e) {
return new Err(e);
}
}
}
When I try something to this effect, I cannot figure out the correct generics to handle this situation. Anyone know of a way for me to get started down the right path? I also am completely aware of the XY problem, so if I'm there please let me know lol
1 Reply
PicardyTheThird
PicardyTheThirdOP5d ago
My other thought would be to use pgTable outside this class, pass in the output, then the class will take the cols and run pgTable again with its own columns and try to pull the right types from the original. But that feels kinda spaghetti Okay it took me digging deep through the weeds of these generics but I finally cracked it. Here's the answer:
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core';
import type { PgColumnBuilderBase, PgTableWithColumns } from 'drizzle-orm/pg-core';
import { type BuildColumns } from 'drizzle-orm';

type Blank = Record<string, PgColumnBuilderBase>;

export type TableBuilderConfig<T extends Blank, Name extends string> = {
name: Name;
structure: T;
};

const globalCols = {
id: text('id').primaryKey(),
created: timestamp('created').defaultNow().notNull(),
updated: timestamp('updated').defaultNow().notNull(),
archived: boolean('archived').default(false).notNull(),
};

type Table<T extends Blank, TableName extends string> = PgTableWithColumns<{
name: TableName;
schema: undefined;
columns: BuildColumns<TableName, T, 'pg'>;
dialect: "pg";
}>;

export class TableBuilder<T extends Blank, Name extends string> {
public readonly table: Table<T & typeof globalCols, Name>;

constructor(public readonly data: TableBuilderConfig<T, Name>) {

this.table = pgTable(data.name, {
...globalCols,
...data.structure,
});
}
}

const s = new TableBuilder({
name: 'Accounts',
// database: null,
structure: {
username: text('username').notNull(),
email: text('email').notNull().unique(),
password: text('password').notNull(),
}
});
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core';
import type { PgColumnBuilderBase, PgTableWithColumns } from 'drizzle-orm/pg-core';
import { type BuildColumns } from 'drizzle-orm';

type Blank = Record<string, PgColumnBuilderBase>;

export type TableBuilderConfig<T extends Blank, Name extends string> = {
name: Name;
structure: T;
};

const globalCols = {
id: text('id').primaryKey(),
created: timestamp('created').defaultNow().notNull(),
updated: timestamp('updated').defaultNow().notNull(),
archived: boolean('archived').default(false).notNull(),
};

type Table<T extends Blank, TableName extends string> = PgTableWithColumns<{
name: TableName;
schema: undefined;
columns: BuildColumns<TableName, T, 'pg'>;
dialect: "pg";
}>;

export class TableBuilder<T extends Blank, Name extends string> {
public readonly table: Table<T & typeof globalCols, Name>;

constructor(public readonly data: TableBuilderConfig<T, Name>) {

this.table = pgTable(data.name, {
...globalCols,
...data.structure,
});
}
}

const s = new TableBuilder({
name: 'Accounts',
// database: null,
structure: {
username: text('username').notNull(),
email: text('email').notNull().unique(),
password: text('password').notNull(),
}
});

Did you find this page helpful?