Anyone having issues with adding extra columns to the user model and then getting drizzle to save/retrieve that data? #8758
Replies: 10 comments 12 replies
-
I have this exact problem and cannot find a solution to it. I just want to extend the next-auth session with a userRole but it is always undefined.. |
Beta Was this translation helpful? Give feedback.
-
I figured out what the problem is. Take a look at the mysql adapter for example https://github.com/nextauthjs/next-auth/blob/5c2b0b6e517a4aed9d4df7dd8bbdf70531174065/packages/adapter-drizzle/src/lib/mysql.ts#L84C5-L84C5 The createTables function is creating an entirely new table using the standard values. Even though you pass in your tableFn, the tables get overwritten with the defaults. This causes the queries to only include the default values and not any additional columns you added. This is an issue not a discussion. I dont know why the issue was closed.. |
Beta Was this translation helpful? Give feedback.
-
I've tried to open 2 issues for this now but they keep getting closed because of the reproduction link.. |
Beta Was this translation helpful? Give feedback.
-
the issue is due to the drizzle adapter only accepting the example schema, which will not yield your custom columns when the adapter does the solution would be to create a custom adapter that uses your own schema: adapter.tsimport type { Adapter } from "@auth/core/adapters";
import * as schema from "./schema"; // this is where your custom tables are defined
import { and, eq } from "drizzle-orm";
import { PgDatabase } from "drizzle-orm/pg-core";
export function pgDrizzleAdapter(
client: InstanceType<typeof PgDatabase>
): Adapter {
const { users, accounts, sessions, verificationTokens } = schema;
return {
async createUser(data) {
return await client
.insert(users)
.values({ ...data, id: crypto.randomUUID() })
.returning()
.then((res) => res[0] ?? null);
},
async getUser(data) {
return await client
.select()
.from(users)
.where(eq(users.id, data))
.then((res) => res[0] ?? null);
},
async getUserByEmail(data) {
return await client
.select()
.from(users)
.where(eq(users.email, data))
.then((res) => res[0] ?? null);
},
async createSession(data) {
return await client
.insert(sessions)
.values(data)
.returning()
.then((res) => res[0]);
},
async getSessionAndUser(data) {
return await client
.select({
session: sessions,
user: users,
})
.from(sessions)
.where(eq(sessions.sessionToken, data))
.innerJoin(users, eq(users.id, sessions.userId))
.then((res) => res[0] ?? null);
},
async updateUser(data) {
if (!data.id) {
throw new Error("No user id.");
}
return await client
.update(users)
.set(data)
.where(eq(users.id, data.id))
.returning()
.then((res) => res[0]);
},
async updateSession(data) {
return await client
.update(sessions)
.set(data)
.where(eq(sessions.sessionToken, data.sessionToken))
.returning()
.then((res) => res[0]);
},
async linkAccount(rawAccount) {
const updatedAccount = await client
.insert(accounts)
.values(rawAccount)
.returning()
.then((res) => res[0]);
// Drizzle will return `null` for fields that are not defined.
// However, the return type is expecting `undefined`.
const account = {
...updatedAccount,
access_token: updatedAccount.accessToken ?? undefined,
token_type: updatedAccount.tokenType ?? undefined,
id_token: updatedAccount.idToken ?? undefined,
refresh_token: updatedAccount.refreshToken ?? undefined,
scope: updatedAccount.scope ?? undefined,
expires_at: updatedAccount.expiresAt ?? undefined,
session_state: updatedAccount.sessionState ?? undefined,
};
return account;
},
async getUserByAccount(account) {
const dbAccount =
(await client
.select()
.from(accounts)
.where(
and(
eq(accounts.providerAccountId, account.providerAccountId),
eq(accounts.provider, account.provider)
)
)
.leftJoin(users, eq(accounts.userId, users.id))
.then((res) => res[0])) ?? null;
if (!dbAccount) {
return null;
}
return dbAccount.users;
},
async deleteSession(sessionToken) {
const session = await client
.delete(sessions)
.where(eq(sessions.sessionToken, sessionToken))
.returning()
.then((res) => res[0] ?? null);
return session;
},
async createVerificationToken(token) {
return await client
.insert(verificationTokens)
.values(token)
.returning()
.then((res) => res[0]);
},
async useVerificationToken(token) {
try {
return await client
.delete(verificationTokens)
.where(
and(
eq(verificationTokens.identifier, token.identifier),
eq(verificationTokens.token, token.token)
)
)
.returning()
.then((res) => res[0] ?? null);
} catch (err) {
throw new Error("No verification token found.");
}
},
async deleteUser(id) {
await client
.delete(users)
.where(eq(users.id, id))
.returning()
.then((res) => res[0] ?? null);
},
async unlinkAccount(account) {
const { type, provider, providerAccountId, userId } = await client
.delete(accounts)
.where(
and(
eq(accounts.providerAccountId, account.providerAccountId),
eq(accounts.provider, account.provider)
)
)
.returning()
.then((res) => res[0] ?? null);
return { provider, type, providerAccountId, userId };
},
};
} schema.tsimport type { AdapterAccount } from "@auth/core/adapters";
import {
integer,
pgSchema,
primaryKey,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
export const authSchema = pgSchema("auth");
export const users = authSchema.table("users", {
id: uuid("id").notNull().primaryKey(),
name: text("name"),
email: text("email").notNull(),
emailVerified: timestamp("email_verified", { mode: "date" }),
image: text("image"),
customField: text("custom_field"),
});
export const accounts = authSchema.table(
"accounts",
{
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
type: text("type").$type<AdapterAccount["type"]>().notNull(),
provider: text("provider").notNull(),
providerAccountId: text("provider_account_id").notNull(),
refreshToken: text("refresh_token"),
accessToken: text("access_token"),
expiresAt: integer("expires_at"),
tokenType: text("token_type"),
scope: text("scope"),
idToken: text("id_token"),
sessionState: text("session_state"),
},
(account) => ({
compoundKey: primaryKey(account.provider, account.providerAccountId),
})
);
export const sessions = authSchema.table("sessions", {
sessionToken: text("session_token").notNull().primaryKey(),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: timestamp("expires", { mode: "date" }).notNull(),
});
export const verificationTokens = authSchema.table(
"verification_tokens",
{
identifier: text("identifier").notNull(),
token: text("token").notNull(),
expires: timestamp("expires", { mode: "date" }).notNull(),
},
(vt) => ({
compoundKey: primaryKey(vt.identifier, vt.token),
})
); this is not a workaround, it is the intended solution for the case when you have a custom drizzle schema. |
Beta Was this translation helpful? Give feedback.
-
Interesting bug. For me the problem is that I want to have plural names of the columns -> e.g. |
Beta Was this translation helpful? Give feedback.
-
I solved it by custom building an adapter. My requirement was an additional column named username (from Github provider) in pgDrizzleAdapter. Below code has some type sacrifices, but works. Also default Schema is replaced with mine.
import { PgDatabase } from "drizzle-orm/pg-core"
import { Adapter, AdapterUser } from "next-auth/adapters"
import { accounts, sessions, users, verificationTokens } from "./schema"
import { and, eq } from "drizzle-orm"
import { Awaitable } from "next-auth"
+ type CustomAdapter = Omit<Adapter, "createUser"> & {
+ createUser: (user: Omit<AdapterUser & { username: string }, "id">) => Awaitable<AdapterUser & { username: string }>
+ }
export function CustomPgDrizzleAdapter(client: InstanceType<typeof PgDatabase>): CustomAdapter {
return {
async createUser(data) {
return await client
.insert(users)
- .values({ ...data, id: crypto.randomUUID() })
+ .values({ ...data, id: crypto.randomUUID(), username: data.username })
.returning()
.then((res) => res[0] ?? null)
},
async getUser(data) {
return await client
.select()
.from(users)
.where(eq(users.id, data))
.then((res) => res[0] ?? null)
},
....
and export const authOptions: NextAuthOptions = {
- adapter: DrizzleAdapter(db),
+ adapter: CustomPgDrizzleAdapter(db) as unknown as Adapter,
providers: [
GithubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
profile: (profile) => {
console.warn("profile:", profile)
return {
id: profile.id.toString(),
name: profile.name || profile.login,
email: profile.email,
image: profile.avatar_url,
+ username: profile.login,
};
},
}),
],
callbacks: {
session: ({ session, user }) => {
return {
...session,
user: {
...session.user,
+ username: user.username,
id: user.id,
},
}
},
},
}; |
Beta Was this translation helpful? Give feedback.
-
This pull request might be interesting for others finding this. |
Beta Was this translation helpful? Give feedback.
-
For anyone having a similar issue, you can use you own table definitions like this:
I'm sorry for the |
Beta Was this translation helpful? Give feedback.
-
To resolve this issue, you should provide the schema to the adapter when initialising it.
// remaining code |
Beta Was this translation helpful? Give feedback.
-
Issue still persits. Anyone found a workaround other than rewriting the adapter? |
Beta Was this translation helpful? Give feedback.
-
Anyone having issues with adding extra columns to the user model and then getting drizzle to save/retrieve that data?
According to the docs it should be as simple as adding a new column to the schema and return the data from the profile() option.
https://authjs.dev/guides/basics/role-based-access-control#with-database
It should then be available in the user object in the session callback.
Yet nothing happens when you return something from the profile function, nor is the property on the user in the session callback.
Works fine with prisma.
Originally posted by @ramoneres in #7005 (comment)
Beta Was this translation helpful? Give feedback.
All reactions