K
Kysely•12mo ago
virk

Guidance to create a generic wrapper over Kysely

I am trying to create a generic authentication wrapper over Kysely. The idea is - You can pass some config to this function, which includes a reference to Kysely instance, a database table name and an array of column names. - The function will return an authenticator instance you can use to perform user lookups during Login. The internals of this function are not important for this discussion. This is what I have so far. You can copy/paste the following code to run it yourself.
import { Pool } from 'pg'
import { AnyColumn, ColumnType, Generated, Kysely, PostgresDialect } from 'kysely'

/**
* Interfaces for the table and the database
*/
interface UserTable {
id: Generated<number>
email: string
username: string
password: string
created_at: ColumnType<Date, string | undefined, never>
}
interface Database {
users: UserTable
}

/**
* PostgreSQL dialect and kysely instance
*/
const dialect = new PostgresDialect({ pool: new Pool(poolConfig) })
const db = new Kysely<Database>({ dialect })

/**
* Authenticator function that need kysely instance
* and an unknown table + columns
*/
function createAuthenticator<
DB,
TB extends keyof DB & string,
Columns extends AnyColumn<DB, TB>
>(config: {
db: Kysely<DB>
table: TB,
uids: Columns[]
}) {
config
.db
.selectFrom(config.table)
// The where clause gives an error
.where(config.uids[0], '=', 'foobar')
}

/**
* Using function to create authenticator
*/
createAuthenticator({
db: db,
table: 'users',
uids: ['email'],
})
import { Pool } from 'pg'
import { AnyColumn, ColumnType, Generated, Kysely, PostgresDialect } from 'kysely'

/**
* Interfaces for the table and the database
*/
interface UserTable {
id: Generated<number>
email: string
username: string
password: string
created_at: ColumnType<Date, string | undefined, never>
}
interface Database {
users: UserTable
}

/**
* PostgreSQL dialect and kysely instance
*/
const dialect = new PostgresDialect({ pool: new Pool(poolConfig) })
const db = new Kysely<Database>({ dialect })

/**
* Authenticator function that need kysely instance
* and an unknown table + columns
*/
function createAuthenticator<
DB,
TB extends keyof DB & string,
Columns extends AnyColumn<DB, TB>
>(config: {
db: Kysely<DB>
table: TB,
uids: Columns[]
}) {
config
.db
.selectFrom(config.table)
// The where clause gives an error
.where(config.uids[0], '=', 'foobar')
}

/**
* Using function to create authenticator
*/
createAuthenticator({
db: db,
table: 'users',
uids: ['email'],
})
The .where(config.uids[0]) method call gives a type error saying, "The Columns are not assignable to the input accepted by the where method". So, I need some guidance on which approach to take for this use case.
7 Replies
koskimas
koskimas•12mo ago
It's very hard or impossible to write generic wrappers like this for kysely. Kysely's super strict typing doesn't work well with generics. Even if you managed to get this working with some insane type gymnastics, the next version would probably break it.
virk
virk•12mo ago
Ahh okay. Thanks for your reply 🙂
Igal
Igal•12mo ago
You'd jump through hoops making this viable, and it'll look pretty ugly and not type-safe under the hood due to how specific kysely is and typescript's limitations - but you could probably make a decent user experience still. and could lock to a specific kysely version.
virk
virk•11mo ago
I have another use-case, lemme ask you guys before I give up on this one. So, here I am trying to create a generic firstOrCreate helper function. This is how the implementation looks like so far.
function firstOrCreate<DB, TB extends keyof DB>(
db: Kysely<DB>,
table: TB & string,
searchAttributes: Readonly<FilterObject<DB, TB>>
) {
db.selectFrom(table).where((eb) => eb.and(searchAttributes))
}
function firstOrCreate<DB, TB extends keyof DB>(
db: Kysely<DB>,
table: TB & string,
searchAttributes: Readonly<FilterObject<DB, TB>>
) {
db.selectFrom(table).where((eb) => eb.and(searchAttributes))
}
I can execute the function with no errors.
firstOrCreate(db, 'users', { name: 'foo' })
firstOrCreate(db, 'users', { name: 'foo' })
However, the eb.and(searchAttributes) method call gives an error --- Is it too much to ask from Kysely internals to work with generic functions like this? Or you think it can work?
virk
virk•11mo ago
Here's the error screenshot
No description
Igal
Igal•11mo ago
@virk The error you're seeing is due to a common typescript limitation. You cannot have both generic helpers and type-safety within them when using something specific as a kysely method. DB is anything and TB is just a string in your function's context, so anything you put in eb.and is just too wide and not assignable to the expected types. Your function is still type-safe, autocompletion friendly and produces the same SQL as long you got arguments and return type covered - which is what your consumer cares about. The internals cannot be type-safe - you must escape it with searchAttributes as any - which is what you care about. Using db.selectFrom(table) as SelectQueryBuilder<any, any, {}> can reduce amount of as any downstream.
virk
virk•11mo ago
Hey, thanks for the reply. Cool, just wanted to make sure I am not shooting myself in foot by typecasting to any
Want results from more Discord servers?
Add your server