-
-
Notifications
You must be signed in to change notification settings - Fork 88
feat(sequelize): Add Sequelize adapter #237
Changes from all commits
748d7dc
bb0e792
6d4f411
e0b1dae
30363a1
f6b9518
cd44414
ddeba75
3b2da67
48961d1
a03a37c
564dc81
4cd4fe8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Change Log | ||
|
||
All notable changes to this project will be documented in this file. | ||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
<p align="center"> | ||
<br/> | ||
<a href="https://next-auth.js.org" target="_blank"><img height="64px" src="https://next-auth.js.org/img/logo/logo-sm.png" /></a> <img height="64px" src="https://raw.githubusercontent.com/nextauthjs/adapters/main/packages/sequelize/logo.svg" /> | ||
<h3 align="center"><b>Sequelize Adapter</b> - NextAuth.js</h3> | ||
<p align="center"> | ||
Open Source. Full Stack. Own Your Data. | ||
</p> | ||
<p align="center" style="align: center;"> | ||
<img src="https://github.com/nextauthjs/adapters/actions/workflows/release.yml/badge.svg" alt="CI Test" /> | ||
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/sequelize-adapter" alt="Bundle Size"/> | ||
<img src="https://img.shields.io/npm/v/@next-auth/sequelize-adapter" alt="@next-auth/sequelize-adapter Version" /> | ||
</p> | ||
</p> | ||
|
||
## Overview | ||
|
||
This is the Sequelize Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package. | ||
|
||
You can find the Sequelize schema in the docs at [next-auth.js.org/adapters/sequelize](https://next-auth.js.org/adapters/sequelize). | ||
|
||
## Getting Started | ||
|
||
1. Install `next-auth` and `@next-auth/sequelize-adapter` as well as `sequelize` and your [database driver](https://sequelize.org/master/manual/getting-started.html) of choice. | ||
|
||
```js | ||
npm install next-auth @next-auth/sequeluze-adapter sequelize sqlite3 | ||
npm install --save-dev sequelize | ||
``` | ||
|
||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object. | ||
|
||
```js | ||
import NextAuth from "next-auth" | ||
import SequelizeAdapter from "@next-auth/sequelize-adapter" | ||
import Sequelize from 'sequelize' | ||
|
||
const sequelize = new Sequelize("sqlite::memory:") | ||
|
||
// For more information on each option (and a full list of options) go to | ||
// https://next-auth.js.org/configuration/options | ||
export default NextAuth({ | ||
... | ||
adapter: SequelizeAdapter(sequelize) | ||
... | ||
}) | ||
``` | ||
|
||
## Updating the database schema | ||
|
||
In development, the sequelize adapter will create the necessary tables, foreign keys and indexes in your database. In production, synchronization is disabled. Best practice is to create the [required tables](https://next-auth.js.org/adapters/models) in your database via [migrations](https://sequelize.org/master/manual/migrations.html). | ||
|
||
In development, if you do not want the adapter to automatically create tables, you are able to pass `{ synchronize: false }` as the second option to `SequelizeAdapter` to disable this behavior: | ||
|
||
```js | ||
import NextAuth from "next-auth" | ||
import SequelizeAdapter from "@next-auth/sequelize-adapter" | ||
import Sequelize from 'sequelize' | ||
|
||
const sequelize = new Sequelize("sqlite::memory:") | ||
|
||
export default NextAuth({ | ||
... | ||
adapter: SequelizeAdapter(sequelize, { synchronize: false }) | ||
... | ||
}) | ||
``` | ||
|
||
## Using custom models | ||
|
||
Sequelize models are option to customization like so: | ||
|
||
```js | ||
import NextAuth from "next-auth" | ||
import SequelizeAdapter, { models } from "@next-auth/sequelize-adapter" | ||
import Sequelize, { DataTypes } from 'sequelize' | ||
|
||
const sequelize = new Sequelize("sqlite::memory:") | ||
|
||
export default NextAuth({ | ||
... | ||
adapter: SequelizeAdapter(sequelize, { | ||
models: { | ||
User: sequelize.define('user', { ...models.User, phoneNumber: DataTypes.STRING }) | ||
} | ||
}) | ||
... | ||
}) | ||
``` | ||
|
||
## Contributing | ||
|
||
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/adapters/blob/main/CONTRIBUTING.md). | ||
|
||
## License | ||
|
||
ISC |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require("../../jest.config") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"name": "@next-auth/sequelize-adapter", | ||
"version": "1.0.0", | ||
"description": "Sequelize adapter for next-auth.", | ||
"homepage": "https://next-auth.js.org", | ||
"repository": "https://github.com/nextauthjs/adapters", | ||
"bugs": { | ||
"url": "https://github.com/nextauthjs/adapters/issues" | ||
}, | ||
"author": "github.com/luke-j", | ||
"main": "dist/index.js", | ||
"license": "ISC", | ||
"keywords": [ | ||
"next-auth", | ||
"next.js", | ||
"oauth", | ||
"sequelize" | ||
], | ||
"private": false, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"scripts": { | ||
"test": "jest", | ||
"build": "tsc" | ||
}, | ||
"files": [ | ||
"README.md", | ||
"dist" | ||
], | ||
"peerDependencies": { | ||
"next-auth": ">4 || 4.0.0-beta.1 - 4.0.0-beta.x", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Corrected this as per #274 |
||
"sequelize": "^6.6.5" | ||
}, | ||
"devDependencies": { | ||
"sequelize": "^6.6.5" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
import type { Account as ApadterAccount } from "next-auth" | ||
import type { | ||
Adapter, | ||
AdapterUser, | ||
AdapterSession, | ||
VerificationToken, | ||
} from "next-auth/adapters" | ||
import { Sequelize, Model, ModelCtor } from "sequelize" | ||
import * as defaultModels from "./models" | ||
|
||
export { defaultModels as models } | ||
|
||
// @see https://sequelize.org/master/manual/typescript.html | ||
interface AccountInstance | ||
extends Model<ApadterAccount, Partial<ApadterAccount>>, | ||
ApadterAccount {} | ||
interface UserInstance | ||
extends Model<AdapterUser, Partial<AdapterUser>>, | ||
AdapterUser {} | ||
interface SessionInstance | ||
extends Model<AdapterSession, Partial<AdapterSession>>, | ||
AdapterSession {} | ||
interface VerificationTokenInstance | ||
extends Model<VerificationToken, Partial<VerificationToken>>, | ||
VerificationToken {} | ||
|
||
interface SequelizeAdapterOptions { | ||
synchronize?: boolean | ||
models?: Partial<{ | ||
User: ModelCtor<UserInstance> | ||
Account: ModelCtor<AccountInstance> | ||
Session: ModelCtor<SessionInstance> | ||
VerificationToken: ModelCtor<VerificationTokenInstance> | ||
}> | ||
} | ||
|
||
export default function SequelizeAdapter( | ||
client: Sequelize, | ||
options?: SequelizeAdapterOptions | ||
): Adapter { | ||
const { models, synchronize = true } = options ?? {} | ||
const defaultModelOptions = { underscored: true, timestamps: false } | ||
const { User, Account, Session, VerificationToken } = { | ||
User: | ||
models?.User ?? | ||
client.define<UserInstance>( | ||
"user", | ||
defaultModels.User, | ||
defaultModelOptions | ||
), | ||
Account: | ||
models?.Account ?? | ||
client.define<AccountInstance>( | ||
"account", | ||
defaultModels.Account, | ||
defaultModelOptions | ||
), | ||
Session: | ||
models?.Session ?? | ||
client.define<SessionInstance>( | ||
"session", | ||
defaultModels.Session, | ||
defaultModelOptions | ||
), | ||
VerificationToken: | ||
models?.VerificationToken ?? | ||
client.define<VerificationTokenInstance>( | ||
"verificationToken", | ||
defaultModels.VerificationToken, | ||
defaultModelOptions | ||
), | ||
} | ||
let _synced = false | ||
const sync = async () => { | ||
if (process.env.NODE_ENV !== "production" && synchronize && !_synced) { | ||
const syncOptions = | ||
typeof synchronize === "object" ? synchronize : undefined | ||
|
||
await Promise.all([ | ||
User.sync(syncOptions), | ||
Account.sync(syncOptions), | ||
Session.sync(syncOptions), | ||
VerificationToken.sync(syncOptions), | ||
]) | ||
|
||
_synced = true | ||
} | ||
} | ||
|
||
Account.belongsTo(User, { onDelete: "cascade" }) | ||
Session.belongsTo(User, { onDelete: "cascade" }) | ||
Comment on lines
+90
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this necessarily done runtime? I am not familiar with Sequelize, but usually, you would do some kind of seeding/setup step to define the database schema, right? Does Sequelize have anything similar to TypeORM's We have to be careful not to update things in production to prevent data loss. In development though, auto-syncing model changes might be useful. #224 how we handled this for the TypeORM adapter recently. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sequelize has the It would be best practice for the user to define the tables as migrations, but sequelize will automatically create the necessary tables/FKs/indexes with
I've added a section in the README about how to create tables like this with a mention that best practice is still migrations in e0b1dae. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should default to synchronizing if we detect We could reuse this warning by some rewording: https://next-auth.js.org/warnings#adapter_typeorm_updating_entities I would add Otherwise, we should create explain how to migrate manually: https://sequelize.org/master/manual/migrations.html There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @balazsorban44 I'm thinking about how we might achieve this. I remembered that eg. if (process.env.NODE_ENV !== 'production' && synchronize) {
await Promise.all([
User.sync(),
Account.sync(),
Session.sync(),
VerificationToken.sync()
])
} I'm wondering if including There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. Ultimately, this is why we had to get a connection manager in each adapter method with TypeORM, because getting a manager was an async operation. 😅 https://github.com/nextauthjs/adapters/blob/next/packages/typeorm-legacy/src/index.ts I don't want the adapter to be async as that would complicate the initialization. I see there is an all-in-one https://sequelize.org/master/class/lib/sequelize.js~Sequelize.html#instance-method-sync My idea is that we should create a wrapper for it, and add a I feel like we should stretch here, as the DX would be better. Especially useful if you would need to debug, I think. I am thinking of something like this: function SequelizeAdapter(client, options) {
// Make sure we only sync once per invocation
let _synced = false
const sync = async () => {
if (
process.env.NODE_ENV === "production" ||
_synced ||
options.sync === false
)
return
const syncOptions =
typeof options.sync === "object"
? options.sync
: {
// ... some default options, if we need it
}
await client.sync(syncOptions)
_synced = true
}
return {
async createUser() {
await sync() // call this first in each method
//...
},
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think if we want to do this this is probably the best option. I've done this in 48961d1 along with some tests. Unfortunately it's not the prettiest solution. If this is occurring in multiple adapters I think it's reasonable to consider an optional There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most adapters seem to connect/disconnect automatically, actually. In my experience, it is only TypeORM that needs manual connection handling so far. 😕 |
||
|
||
return { | ||
async createUser(user) { | ||
await sync() | ||
|
||
return await User.create(user) | ||
}, | ||
async getUser(id) { | ||
await sync() | ||
|
||
const userInstance = await User.findByPk(id) | ||
|
||
return userInstance?.get({ plain: true }) ?? null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I love All ORMs should have an option like this! 🙏 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree! Having an instance returned can be useful but the option for a plain object is always great. |
||
}, | ||
async getUserByEmail(email) { | ||
await sync() | ||
|
||
const userInstance = await User.findOne({ | ||
where: { email }, | ||
}) | ||
|
||
return userInstance?.get({ plain: true }) ?? null | ||
}, | ||
async getUserByAccount({ provider, providerAccountId }) { | ||
await sync() | ||
|
||
const accountInstance = await Account.findOne({ | ||
where: { provider, providerAccountId }, | ||
}) | ||
|
||
if (!accountInstance) { | ||
return null | ||
} | ||
|
||
const userInstance = await User.findByPk(accountInstance.userId) | ||
|
||
return userInstance?.get({ plain: true }) ?? null | ||
}, | ||
async updateUser(user) { | ||
await sync() | ||
|
||
await User.update(user, { where: { id: user.id } }) | ||
const userInstance = await User.findByPk(user.id) | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
return userInstance! | ||
}, | ||
async deleteUser(userId) { | ||
await sync() | ||
|
||
const userInstance = await User.findByPk(userId) | ||
|
||
await User.destroy({ where: { id: userId } }) | ||
|
||
return userInstance | ||
}, | ||
async linkAccount(account) { | ||
await sync() | ||
|
||
await Account.create(account) | ||
}, | ||
async unlinkAccount({ provider, providerAccountId }) { | ||
await sync() | ||
|
||
await Account.destroy({ | ||
where: { provider, providerAccountId }, | ||
}) | ||
}, | ||
async createSession(session) { | ||
await sync() | ||
|
||
return await Session.create(session) | ||
}, | ||
async getSessionAndUser(sessionToken) { | ||
await sync() | ||
|
||
const sessionInstance = await Session.findOne({ | ||
where: { sessionToken }, | ||
}) | ||
|
||
if (!sessionInstance) { | ||
return null | ||
} | ||
|
||
const userInstance = await User.findByPk(sessionInstance.userId) | ||
|
||
if (!userInstance) { | ||
return null | ||
} | ||
|
||
return { | ||
session: sessionInstance?.get({ plain: true }), | ||
user: userInstance?.get({ plain: true }), | ||
} | ||
}, | ||
async updateSession({ sessionToken, expires }) { | ||
await sync() | ||
|
||
await Session.update( | ||
{ expires, sessionToken }, | ||
{ where: { sessionToken } } | ||
) | ||
|
||
return await Session.findOne({ where: { sessionToken } }) | ||
}, | ||
async deleteSession(sessionToken) { | ||
await sync() | ||
|
||
await Session.destroy({ where: { sessionToken } }) | ||
}, | ||
async createVerificationToken(token) { | ||
await sync() | ||
|
||
return await VerificationToken.create(token) | ||
}, | ||
async useVerificationToken({ identifier, token }) { | ||
await sync() | ||
|
||
const tokenInstance = await VerificationToken.findOne({ | ||
where: { identifier, token }, | ||
}) | ||
|
||
await VerificationToken.destroy({ where: { identifier } }) | ||
|
||
return tokenInstance?.get({ plain: true }) ?? null | ||
}, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't need to run build on test, we will build before publishing anyway.