Skip to content

Instantly share code, notes, and snippets.

@marcosrjjunior
Last active July 15, 2024 10:37
Show Gist options
  • Save marcosrjjunior/36827753f7fc0fb48e499b0630cbb279 to your computer and use it in GitHub Desktop.
Save marcosrjjunior/36827753f7fc0fb48e499b0630cbb279 to your computer and use it in GitHub Desktop.
Custom kysely adapter to use with authjs (nextauth)

Custom kysely adapter to use with authjs (nextauth) (snake_case)

  • authjs use a mix of snake_case and camel_case for the columns which is kind weird models.
  • This code is following the same model and using all columns on snake_case format.

Instructions

  • Create the migration and the custom kysely.
  • Use the adapter in your auth.ts file
import Google from 'next-auth/providers/google'
import { db } from '../db'
import { KyselyAdapter } from './custom-kysely-adapter'

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: KyselyAdapter(db),
  providers: [Google],
  // ...
})
import { Kysely, SqliteAdapter } from 'kysely'
export function KyselyAdapter(db: Kysely<any>): any {
const { adapter } = db.getExecutor()
const { supportsReturning } = adapter
const isSqlite = adapter instanceof SqliteAdapter
/** If the database is SQLite, turn dates into an ISO string */
const to = isSqlite ? format.to : <T>(x: T) => x as T
/** If the database is SQLite, turn ISO strings into dates */
const from = isSqlite ? format.from : <T>(x: T) => x as T
return {
async createUser(data) {
const user = { ...data, id: crypto.randomUUID() }
const userData = to(user)
await db
.insertInto('users')
.values({
id: userData.id,
name: userData.name,
email: userData.email,
image: userData.image,
email_verified: userData.emailVerified,
})
.executeTakeFirstOrThrow()
return user
},
async getUser(id) {
const result = await db
.selectFrom('users')
.selectAll()
.where('id', '=', id)
.executeTakeFirst()
if (!result) return null
return from(result)
},
async getUserByEmail(email) {
const result = await db
.selectFrom('users')
.selectAll()
.where('email', '=', email)
.executeTakeFirst()
if (!result) return null
return from(result)
},
async getUserByAccount({ providerAccountId, provider }) {
const result = await db
.selectFrom('users')
.innerJoin('accounts', 'users.id', 'accounts.user_id')
.selectAll('users')
.where('accounts.provider_account_id', '=', providerAccountId)
.where('accounts.provider', '=', provider)
.executeTakeFirst()
if (!result) return null
return from(result)
},
async updateUser({ id, ...user }) {
const userData = to(user) as any
const query = db.updateTable('users').set(userData).where('id', '=', id)
const result = supportsReturning
? query.returningAll().executeTakeFirstOrThrow()
: query
.executeTakeFirstOrThrow()
.then(() =>
db
.selectFrom('users')
.selectAll()
.where('id', '=', id)
.executeTakeFirstOrThrow(),
)
return from(await result)
},
async deleteUser(userId) {
await db
.deleteFrom('users')
.where('users.id', '=', userId)
.executeTakeFirst()
},
async linkAccount(account) {
const accountData = to(account)
await db
.insertInto('accounts')
.values({
user_id: accountData.userId,
type: accountData.type,
provider: accountData.provider,
provider_account_id: accountData.providerAccountId,
access_token: accountData.access_token,
expires_at: accountData.expires_at,
token_type: accountData.token_type,
scope: accountData.scope,
id_token: accountData.id_token,
})
.executeTakeFirstOrThrow()
return account
},
async unlinkAccount({ providerAccountId, provider }) {
await db
.deleteFrom('accounts')
.where('accounts.provider_account_id', '=', providerAccountId)
.where('accounts.provider', '=', provider)
.executeTakeFirstOrThrow()
},
async createSession(session) {
const sessionData = to(session)
await db
.insertInto('sessions')
.values({
user_id: sessionData.userId,
session_token: sessionData.sessionToken,
expires: sessionData.expires,
})
.execute()
return session
},
async getSessionAndUser(session_token) {
const result = await db
.selectFrom('sessions')
.innerJoin('users', 'users.id', 'sessions.user_id')
.selectAll('users')
.select(['sessions.expires', 'sessions.user_id'])
.where('sessions.session_token', '=', session_token)
.executeTakeFirst()
if (!result) return null
const { user_id, expires, ...user } = result
const session = { session_token, user_id, expires }
return { user: from(user), session: from(session) }
},
async updateSession(session) {
const sessionData = to(session)
const query = db
.updateTable('sessions')
.set({
user_id: sessionData.userId,
session_token: sessionData.sessionToken,
expires: sessionData.expires,
})
.where('sessions.session_token', '=', session.sessionToken)
const result = supportsReturning
? await query.returningAll().executeTakeFirstOrThrow()
: await query.executeTakeFirstOrThrow().then(async () => {
return await db
.selectFrom('sessions')
.selectAll()
.where('sessions.session_token', '=', sessionData.sessionToken)
.executeTakeFirstOrThrow()
})
return from(result)
},
async deleteSession(sessionToken) {
await db
.deleteFrom('sessions')
.where('sessions.session_token', '=', sessionToken)
.executeTakeFirstOrThrow()
},
async createVerificationToken(data) {
await db.insertInto('verification_tokens').values(to(data)).execute()
return data
},
async useVerificationToken({ identifier, token }) {
const query = db
.deleteFrom('verification_tokens')
.where('verification_tokens.token', '=', token)
.where('verification_tokens.identifier', '=', identifier)
const result = supportsReturning
? await query.returningAll().executeTakeFirst()
: await db
.selectFrom('verification_tokens')
.selectAll()
.where('token', '=', token)
.executeTakeFirst()
.then(async res => {
await query.executeTakeFirst()
return res
})
if (!result) return null
return from(result)
},
}
}
// import { Users } from '../db/schema/public/Users'
// import { Accounts } from '../db/schema/public/Accounts'
// import { Sessions } from '../db/schema/public/Sessions'
// import { VerificationTokens } from '../db/schema/public/VerificationTokens'
// export interface Database {
// users: Users
// accounts: Accounts
// sessions: Sessions
// verification_tokens: VerificationTokens
// }
// https://github.com/honeinc/is-iso-date/blob/8831e79b5b5ee615920dcb350a355ffc5cbf7aed/index.js#L5
const isoDateRE =
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
const isDate = (val: any): val is ConstructorParameters<typeof Date>[0] =>
!!(val && isoDateRE.test(val) && !isNaN(Date.parse(val)))
export const format = {
from<T>(object?: Record<string, any>): T {
const newObject: Record<string, unknown> = {}
for (const key in object) {
const value = object[key]
if (isDate(value)) newObject[key] = new Date(value)
else newObject[key] = value
}
return newObject as T
},
to<T>(object: Record<string, any>): T {
const newObject: Record<string, unknown> = {}
for (const [key, value] of Object.entries(object))
newObject[key] = value instanceof Date ? value.toISOString() : value
return newObject as T
},
}
// migration 20240329T063030-adding_user_tables.ts
import { Kysely, sql } from 'kysely'
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('users')
.addColumn('id', 'uuid', col =>
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
)
.addColumn('name', 'text')
.addColumn('email', 'text', col => col.unique().notNull())
.addColumn('email_verified', 'timestamptz')
.addColumn('image', 'text')
.execute()
await db.schema
.createTable('accounts')
.addColumn('id', 'uuid', col =>
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
)
.addColumn('user_id', 'uuid', col =>
col.references('users.id').onDelete('cascade').notNull(),
)
.addColumn('type', 'text', col => col.notNull())
.addColumn('provider', 'text', col => col.notNull())
.addColumn('provider_account_id', 'text', col => col.notNull())
.addColumn('refresh_token', 'text')
.addColumn('access_token', 'text')
.addColumn('expires_at', 'bigint')
.addColumn('token_type', 'text')
.addColumn('scope', 'text')
.addColumn('id_token', 'text')
.addColumn('session_state', 'text')
.execute()
await db.schema
.createTable('sessions')
.addColumn('id', 'uuid', col =>
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
)
.addColumn('user_id', 'uuid', col =>
col.references('users.id').onDelete('cascade').notNull(),
)
.addColumn('session_token', 'text', col => col.notNull().unique())
.addColumn('expires', 'timestamptz', col => col.notNull())
.execute()
await db.schema
.createTable('verification_tokens')
.addColumn('identifier', 'text', col => col.notNull())
.addColumn('token', 'text', col => col.notNull().unique())
.addColumn('expires', 'timestamptz', col => col.notNull())
.execute()
await db.schema
.createIndex('accounts_user_id_index')
.on('accounts')
.column('user_id')
.execute()
await db.schema
.createIndex('sessions_user_id_index')
.on('sessions')
.column('user_id')
.execute()
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('accounts').ifExists().execute()
await db.schema.dropTable('sessions').ifExists().execute()
await db.schema.dropTable('users').ifExists().execute()
await db.schema.dropTable('verification_tokens').ifExists().execute()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment