@auth/fauna-adapter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2022-2023, Balázs Orbán
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ <p align="center">
2
+ <br/>
3
+ <a href="https://authjs.dev" target="_blank">
4
+ <img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" />
5
+ </a>
6
+ <a href="https://fauna.com" target="_blank">
7
+ <img height="64px" src="https://authjs.dev/img/adapters/fauna.svg"/>
8
+ </a>
9
+ <h3 align="center"><b>Fauna Adapter</b> - NextAuth.js / Auth.js</a></h3>
10
+ <p align="center" style="align: center;">
11
+ <a href="https://npm.im/@auth/fauna-adapter">
12
+ <img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
13
+ </a>
14
+ <a href="https://npm.im/@auth/fauna-adapter">
15
+ <img alt="npm" src="https://img.shields.io/npm/v/@auth/fauna-adapter?color=green&label=@auth/fauna-adapter&style=flat-square">
16
+ </a>
17
+ <a href="https://www.npmtrends.com/@auth/fauna-adapter">
18
+ <img src="https://img.shields.io/npm/dm/@auth/fauna-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
19
+ </a>
20
+ <a href="https://github.com/nextauthjs/next-auth/stargazers">
21
+ <img src="https://img.shields.io/github/stars/nextauthjs/next-auth?style=flat-square" alt="Github Stars" />
22
+ </a>
23
+ </p>
24
+ </p>
25
+
26
+ ---
27
+
28
+ Check out the documentation at [authjs.dev](https://authjs.dev/reference/adapter/fauna).
package/index.d.ts ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
3
+ * <p style={{fontWeight: "normal"}}>Official <a href="https://docs.fauna.com/fauna/current/">Fauna</a> adapter for Auth.js / NextAuth.js.</p>
4
+ * <a href="https://fauna.com/features">
5
+ * <img style={{display: "block"}} src="https://authjs.dev/img/adapters/fauna.svg" height="30"/>
6
+ * </a>
7
+ * </div>
8
+ *
9
+ * ## Installation
10
+ *
11
+ * ```bash npm2yarn2pnpm
12
+ * npm install @auth/fauna-adapter faunadb
13
+ * ```
14
+ *
15
+ * @module @auth/fauna-adapter
16
+ */
17
+ import { Client as FaunaClient, ExprArg } from "faunadb";
18
+ import { Adapter } from "@auth/core/adapters";
19
+ export declare const collections: {
20
+ readonly Users: import("faunadb").Expr;
21
+ readonly Accounts: import("faunadb").Expr;
22
+ readonly Sessions: import("faunadb").Expr;
23
+ readonly VerificationTokens: import("faunadb").Expr;
24
+ };
25
+ export declare const indexes: {
26
+ readonly AccountByProviderAndProviderAccountId: import("faunadb").Expr;
27
+ readonly UserByEmail: import("faunadb").Expr;
28
+ readonly SessionByToken: import("faunadb").Expr;
29
+ readonly VerificationTokenByIdentifierAndToken: import("faunadb").Expr;
30
+ readonly SessionsByUser: import("faunadb").Expr;
31
+ readonly AccountsByUser: import("faunadb").Expr;
32
+ };
33
+ export declare const format: {
34
+ /** Takes a plain old JavaScript object and turns it into a Fauna object */
35
+ to(object: Record<string, any>): Record<string, unknown>;
36
+ /** Takes a Fauna object and returns a plain old JavaScript object */
37
+ from<T = Record<string, unknown>>(object: Record<string, any>): T;
38
+ };
39
+ /**
40
+ * Fauna throws an error when something is not found in the db,
41
+ * `next-auth` expects `null` to be returned
42
+ */
43
+ export declare function query(f: FaunaClient, format: (...args: any) => any): <T>(expr: ExprArg) => Promise<T | null>;
44
+ /**
45
+ *
46
+ * ## Setup
47
+ *
48
+ * This is the Fauna Adapter for [`next-auth`](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
49
+ *
50
+ * You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/adapters/fauna](https://authjs.dev/reference/adapters/fauna).
51
+ *
52
+ * ### Configure Auth.js
53
+ *
54
+ * ```javascript title="pages/api/auth/[...nextauth].js"
55
+ * import NextAuth from "next-auth"
56
+ * import { Client as FaunaClient } from "faunadb"
57
+ * import { FaunaAdapter } from "@auth/fauna-adapter"
58
+ *
59
+ * const client = new FaunaClient({
60
+ * secret: "secret",
61
+ * scheme: "http",
62
+ * domain: "localhost",
63
+ * port: 8443,
64
+ * })
65
+ *
66
+ * // For more information on each option (and a full list of options) go to
67
+ * // https://authjs.dev/reference/configuration/auth-options
68
+ * export default NextAuth({
69
+ * // https://authjs.dev/reference/providers/
70
+ * providers: [],
71
+ * adapter: FaunaAdapter(client)
72
+ * ...
73
+ * })
74
+ * ```
75
+ *
76
+ * ### Schema
77
+ *
78
+ * Run the following commands inside of the `Shell` tab in the Fauna dashboard to setup the appropriate collections and indexes.
79
+ *
80
+ * ```javascript
81
+ * CreateCollection({ name: "accounts" })
82
+ * CreateCollection({ name: "sessions" })
83
+ * CreateCollection({ name: "users" })
84
+ * CreateCollection({ name: "verification_tokens" })
85
+ * ```
86
+ *
87
+ * ```javascript
88
+ * CreateIndex({
89
+ * name: "account_by_provider_and_provider_account_id",
90
+ * source: Collection("accounts"),
91
+ * unique: true,
92
+ * terms: [
93
+ * { field: ["data", "provider"] },
94
+ * { field: ["data", "providerAccountId"] },
95
+ * ],
96
+ * })
97
+ * CreateIndex({
98
+ * name: "session_by_session_token",
99
+ * source: Collection("sessions"),
100
+ * unique: true,
101
+ * terms: [{ field: ["data", "sessionToken"] }],
102
+ * })
103
+ * CreateIndex({
104
+ * name: "user_by_email",
105
+ * source: Collection("users"),
106
+ * unique: true,
107
+ * terms: [{ field: ["data", "email"] }],
108
+ * })
109
+ * CreateIndex({
110
+ * name: "verification_token_by_identifier_and_token",
111
+ * source: Collection("verification_tokens"),
112
+ * unique: true,
113
+ * terms: [{ field: ["data", "identifier"] }, { field: ["data", "token"] }],
114
+ * })
115
+ * ```
116
+ *
117
+ * > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/adapters#models)
118
+ **/
119
+ export declare function FaunaAdapter(f: FaunaClient): Adapter;
120
+ //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EACL,MAAM,IAAI,WAAW,EACrB,OAAO,EAoBR,MAAM,SAAS,CAAA;AAEhB,OAAO,EACL,OAAO,EAIR,MAAM,qBAAqB,CAAA;AAE5B,eAAO,MAAM,WAAW;;;;;CAKd,CAAA;AAEV,eAAO,MAAM,OAAO;;;;;;;CAWV,CAAA;AAEV,eAAO,MAAM,MAAM;IACjB,2EAA2E;eAChE,OAAO,MAAM,EAAE,GAAG,CAAC;IAY9B,qEAAqE;8CAC3B,OAAO,MAAM,EAAE,GAAG,CAAC;CAY9D,CAAA;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,KAAK,GAAG,aACjC,OAAO,uBAsBxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA0EI;AACJ,wBAAgB,YAAY,CAAC,CAAC,EAAE,WAAW,GAAG,OAAO,CAmGpD"}
package/index.js ADDED
@@ -0,0 +1,218 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ /**
3
+ * <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
4
+ * <p style={{fontWeight: "normal"}}>Official <a href="https://docs.fauna.com/fauna/current/">Fauna</a> adapter for Auth.js / NextAuth.js.</p>
5
+ * <a href="https://fauna.com/features">
6
+ * <img style={{display: "block"}} src="https://authjs.dev/img/adapters/fauna.svg" height="30"/>
7
+ * </a>
8
+ * </div>
9
+ *
10
+ * ## Installation
11
+ *
12
+ * ```bash npm2yarn2pnpm
13
+ * npm install @auth/fauna-adapter faunadb
14
+ * ```
15
+ *
16
+ * @module @auth/fauna-adapter
17
+ */
18
+ import { Collection, Create, Delete, Exists, Get, If, Index, Let, Match, Ref, Select, Time, Update, Var, Paginate, Lambda, Do, Foreach, } from "faunadb";
19
+ export const collections = {
20
+ Users: Collection("users"),
21
+ Accounts: Collection("accounts"),
22
+ Sessions: Collection("sessions"),
23
+ VerificationTokens: Collection("verification_tokens"),
24
+ };
25
+ export const indexes = {
26
+ AccountByProviderAndProviderAccountId: Index("account_by_provider_and_provider_account_id"),
27
+ UserByEmail: Index("user_by_email"),
28
+ SessionByToken: Index("session_by_session_token"),
29
+ VerificationTokenByIdentifierAndToken: Index("verification_token_by_identifier_and_token"),
30
+ SessionsByUser: Index("sessions_by_user_id"),
31
+ AccountsByUser: Index("accounts_by_user_id"),
32
+ };
33
+ export const format = {
34
+ /** Takes a plain old JavaScript object and turns it into a Fauna object */
35
+ to(object) {
36
+ const newObject = {};
37
+ for (const key in object) {
38
+ const value = object[key];
39
+ if (value instanceof Date) {
40
+ newObject[key] = Time(value.toISOString());
41
+ }
42
+ else {
43
+ newObject[key] = value;
44
+ }
45
+ }
46
+ return newObject;
47
+ },
48
+ /** Takes a Fauna object and returns a plain old JavaScript object */
49
+ from(object) {
50
+ const newObject = {};
51
+ for (const key in object) {
52
+ const value = object[key];
53
+ if (value?.value && typeof value.value === "string") {
54
+ newObject[key] = new Date(value.value);
55
+ }
56
+ else {
57
+ newObject[key] = value;
58
+ }
59
+ }
60
+ return newObject;
61
+ },
62
+ };
63
+ /**
64
+ * Fauna throws an error when something is not found in the db,
65
+ * `next-auth` expects `null` to be returned
66
+ */
67
+ export function query(f, format) {
68
+ return async function (expr) {
69
+ try {
70
+ const result = await f.query(expr);
71
+ if (!result)
72
+ return null;
73
+ return format({ ...result.data, id: result.ref.id });
74
+ }
75
+ catch (error) {
76
+ if (error.name === "NotFound")
77
+ return null;
78
+ if (error.description?.includes("Number or numeric String expected"))
79
+ return null;
80
+ if (process.env.NODE_ENV === "test")
81
+ console.error(error);
82
+ throw error;
83
+ }
84
+ };
85
+ }
86
+ /**
87
+ *
88
+ * ## Setup
89
+ *
90
+ * This is the Fauna Adapter for [`next-auth`](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
91
+ *
92
+ * You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/adapters/fauna](https://authjs.dev/reference/adapters/fauna).
93
+ *
94
+ * ### Configure Auth.js
95
+ *
96
+ * ```javascript title="pages/api/auth/[...nextauth].js"
97
+ * import NextAuth from "next-auth"
98
+ * import { Client as FaunaClient } from "faunadb"
99
+ * import { FaunaAdapter } from "@auth/fauna-adapter"
100
+ *
101
+ * const client = new FaunaClient({
102
+ * secret: "secret",
103
+ * scheme: "http",
104
+ * domain: "localhost",
105
+ * port: 8443,
106
+ * })
107
+ *
108
+ * // For more information on each option (and a full list of options) go to
109
+ * // https://authjs.dev/reference/configuration/auth-options
110
+ * export default NextAuth({
111
+ * // https://authjs.dev/reference/providers/
112
+ * providers: [],
113
+ * adapter: FaunaAdapter(client)
114
+ * ...
115
+ * })
116
+ * ```
117
+ *
118
+ * ### Schema
119
+ *
120
+ * Run the following commands inside of the `Shell` tab in the Fauna dashboard to setup the appropriate collections and indexes.
121
+ *
122
+ * ```javascript
123
+ * CreateCollection({ name: "accounts" })
124
+ * CreateCollection({ name: "sessions" })
125
+ * CreateCollection({ name: "users" })
126
+ * CreateCollection({ name: "verification_tokens" })
127
+ * ```
128
+ *
129
+ * ```javascript
130
+ * CreateIndex({
131
+ * name: "account_by_provider_and_provider_account_id",
132
+ * source: Collection("accounts"),
133
+ * unique: true,
134
+ * terms: [
135
+ * { field: ["data", "provider"] },
136
+ * { field: ["data", "providerAccountId"] },
137
+ * ],
138
+ * })
139
+ * CreateIndex({
140
+ * name: "session_by_session_token",
141
+ * source: Collection("sessions"),
142
+ * unique: true,
143
+ * terms: [{ field: ["data", "sessionToken"] }],
144
+ * })
145
+ * CreateIndex({
146
+ * name: "user_by_email",
147
+ * source: Collection("users"),
148
+ * unique: true,
149
+ * terms: [{ field: ["data", "email"] }],
150
+ * })
151
+ * CreateIndex({
152
+ * name: "verification_token_by_identifier_and_token",
153
+ * source: Collection("verification_tokens"),
154
+ * unique: true,
155
+ * terms: [{ field: ["data", "identifier"] }, { field: ["data", "token"] }],
156
+ * })
157
+ * ```
158
+ *
159
+ * > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/adapters#models)
160
+ **/
161
+ export function FaunaAdapter(f) {
162
+ const { Users, Accounts, Sessions, VerificationTokens } = collections;
163
+ const { AccountByProviderAndProviderAccountId, AccountsByUser, SessionByToken, SessionsByUser, UserByEmail, VerificationTokenByIdentifierAndToken, } = indexes;
164
+ const { to, from } = format;
165
+ const q = query(f, from);
166
+ return {
167
+ createUser: async (data) => (await q(Create(Users, { data: to(data) }))),
168
+ getUser: async (id) => await q(Get(Ref(Users, id))),
169
+ getUserByEmail: async (email) => await q(Get(Match(UserByEmail, email))),
170
+ async getUserByAccount({ provider, providerAccountId }) {
171
+ const key = [provider, providerAccountId];
172
+ const ref = Match(AccountByProviderAndProviderAccountId, key);
173
+ const user = await q(Let({ ref }, If(Exists(Var("ref")), Get(Ref(Users, Select(["data", "userId"], Get(Var("ref"))))), null)));
174
+ return user;
175
+ },
176
+ updateUser: async (data) => (await q(Update(Ref(Users, data.id), { data: to(data) }))),
177
+ async deleteUser(userId) {
178
+ await f.query(Do(Foreach(Paginate(Match(SessionsByUser, userId)), Lambda("ref", Delete(Var("ref")))), Foreach(Paginate(Match(AccountsByUser, userId)), Lambda("ref", Delete(Var("ref")))), Delete(Ref(Users, userId))));
179
+ },
180
+ linkAccount: async (data) => (await q(Create(Accounts, { data: to(data) }))),
181
+ async unlinkAccount({ provider, providerAccountId }) {
182
+ const id = [provider, providerAccountId];
183
+ await q(Delete(Select("ref", Get(Match(AccountByProviderAndProviderAccountId, id)))));
184
+ },
185
+ createSession: async (data) => (await q(Create(Sessions, { data: to(data) }))),
186
+ async getSessionAndUser(sessionToken) {
187
+ const session = await q(Get(Match(SessionByToken, sessionToken)));
188
+ if (!session)
189
+ return null;
190
+ const user = await q(Get(Ref(Users, session.userId)));
191
+ return { session, user: user };
192
+ },
193
+ async updateSession(data) {
194
+ const ref = Select("ref", Get(Match(SessionByToken, data.sessionToken)));
195
+ return await q(Update(ref, { data: to(data) }));
196
+ },
197
+ async deleteSession(sessionToken) {
198
+ await q(Delete(Select("ref", Get(Match(SessionByToken, sessionToken)))));
199
+ },
200
+ async createVerificationToken(data) {
201
+ // @ts-expect-error
202
+ const { id: _id, ...verificationToken } = await q(Create(VerificationTokens, { data: to(data) }));
203
+ return verificationToken;
204
+ },
205
+ async useVerificationToken({ identifier, token }) {
206
+ const key = [identifier, token];
207
+ const object = Get(Match(VerificationTokenByIdentifierAndToken, key));
208
+ const verificationToken = await q(object);
209
+ if (!verificationToken)
210
+ return null;
211
+ // Verification tokens can be used only once
212
+ await q(Delete(Select("ref", object)));
213
+ // @ts-expect-error
214
+ delete verificationToken.id;
215
+ return verificationToken;
216
+ },
217
+ };
218
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@auth/fauna-adapter",
3
+ "version": "1.0.0",
4
+ "description": "Fauna Adapter for Auth.js",
5
+ "homepage": "https://authjs.dev",
6
+ "repository": "https://github.com/nextauthjs/next-auth",
7
+ "bugs": {
8
+ "url": "https://github.com/nextauthjs/next-auth/issues"
9
+ },
10
+ "author": "Bhanu Teja P",
11
+ "contributors": [
12
+ "Nico Domino <yo@ndo.dev>",
13
+ "Balázs Orbán <info@balazsorban.com>"
14
+ ],
15
+ "type": "module",
16
+ "types": "./index.d.ts",
17
+ "files": [
18
+ "*.js",
19
+ "*.d.ts*",
20
+ "src"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./index.d.ts",
25
+ "import": "./index.js"
26
+ }
27
+ },
28
+ "license": "ISC",
29
+ "keywords": [
30
+ "next-auth",
31
+ "next.js",
32
+ "fauna",
33
+ "faunadb"
34
+ ],
35
+ "private": false,
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@auth/core": "0.8.2"
41
+ },
42
+ "peerDependencies": {
43
+ "faunadb": "^4.3.0"
44
+ },
45
+ "devDependencies": {
46
+ "@fauna-labs/fauna-schema-migrate": "^2.1.3",
47
+ "faunadb": "^4.3.0",
48
+ "jest": "^27.4.3",
49
+ "@next-auth/adapter-test": "0.0.0",
50
+ "@next-auth/tsconfig": "0.0.0"
51
+ },
52
+ "jest": {
53
+ "preset": "@next-auth/adapter-test/jest"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc",
57
+ "dev": "tsc -w",
58
+ "clean": "rm -rf dist",
59
+ "migrate": "fauna-schema-migrate generate",
60
+ "test": "./tests/test.sh"
61
+ }
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,301 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ /**
3
+ * <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
4
+ * <p style={{fontWeight: "normal"}}>Official <a href="https://docs.fauna.com/fauna/current/">Fauna</a> adapter for Auth.js / NextAuth.js.</p>
5
+ * <a href="https://fauna.com/features">
6
+ * <img style={{display: "block"}} src="https://authjs.dev/img/adapters/fauna.svg" height="30"/>
7
+ * </a>
8
+ * </div>
9
+ *
10
+ * ## Installation
11
+ *
12
+ * ```bash npm2yarn2pnpm
13
+ * npm install @auth/fauna-adapter faunadb
14
+ * ```
15
+ *
16
+ * @module @auth/fauna-adapter
17
+ */
18
+ import {
19
+ Client as FaunaClient,
20
+ ExprArg,
21
+ Collection,
22
+ Create,
23
+ Delete,
24
+ Exists,
25
+ Get,
26
+ If,
27
+ Index,
28
+ Let,
29
+ Match,
30
+ Ref,
31
+ Select,
32
+ Time,
33
+ Update,
34
+ Var,
35
+ Paginate,
36
+ Lambda,
37
+ Do,
38
+ Foreach,
39
+ errors,
40
+ } from "faunadb"
41
+
42
+ import {
43
+ Adapter,
44
+ AdapterSession,
45
+ AdapterUser,
46
+ VerificationToken,
47
+ } from "@auth/core/adapters"
48
+
49
+ export const collections = {
50
+ Users: Collection("users"),
51
+ Accounts: Collection("accounts"),
52
+ Sessions: Collection("sessions"),
53
+ VerificationTokens: Collection("verification_tokens"),
54
+ } as const
55
+
56
+ export const indexes = {
57
+ AccountByProviderAndProviderAccountId: Index(
58
+ "account_by_provider_and_provider_account_id"
59
+ ),
60
+ UserByEmail: Index("user_by_email"),
61
+ SessionByToken: Index("session_by_session_token"),
62
+ VerificationTokenByIdentifierAndToken: Index(
63
+ "verification_token_by_identifier_and_token"
64
+ ),
65
+ SessionsByUser: Index("sessions_by_user_id"),
66
+ AccountsByUser: Index("accounts_by_user_id"),
67
+ } as const
68
+
69
+ export const format = {
70
+ /** Takes a plain old JavaScript object and turns it into a Fauna object */
71
+ to(object: Record<string, any>) {
72
+ const newObject: Record<string, unknown> = {}
73
+ for (const key in object) {
74
+ const value = object[key]
75
+ if (value instanceof Date) {
76
+ newObject[key] = Time(value.toISOString())
77
+ } else {
78
+ newObject[key] = value
79
+ }
80
+ }
81
+ return newObject
82
+ },
83
+ /** Takes a Fauna object and returns a plain old JavaScript object */
84
+ from<T = Record<string, unknown>>(object: Record<string, any>): T {
85
+ const newObject: Record<string, unknown> = {}
86
+ for (const key in object) {
87
+ const value = object[key]
88
+ if (value?.value && typeof value.value === "string") {
89
+ newObject[key] = new Date(value.value)
90
+ } else {
91
+ newObject[key] = value
92
+ }
93
+ }
94
+ return newObject as T
95
+ },
96
+ }
97
+
98
+ /**
99
+ * Fauna throws an error when something is not found in the db,
100
+ * `next-auth` expects `null` to be returned
101
+ */
102
+ export function query(f: FaunaClient, format: (...args: any) => any) {
103
+ return async function <T>(expr: ExprArg): Promise<T | null> {
104
+ try {
105
+ const result = await f.query<{
106
+ data: T
107
+ ref: { id: string }
108
+ } | null>(expr)
109
+ if (!result) return null
110
+ return format({ ...result.data, id: result.ref.id })
111
+ } catch (error) {
112
+ if ((error as errors.FaunaError).name === "NotFound") return null
113
+ if (
114
+ (error as errors.FaunaError).description?.includes(
115
+ "Number or numeric String expected"
116
+ )
117
+ )
118
+ return null
119
+
120
+ if (process.env.NODE_ENV === "test") console.error(error)
121
+
122
+ throw error
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ *
129
+ * ## Setup
130
+ *
131
+ * This is the Fauna Adapter for [`next-auth`](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
132
+ *
133
+ * You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/adapters/fauna](https://authjs.dev/reference/adapters/fauna).
134
+ *
135
+ * ### Configure Auth.js
136
+ *
137
+ * ```javascript title="pages/api/auth/[...nextauth].js"
138
+ * import NextAuth from "next-auth"
139
+ * import { Client as FaunaClient } from "faunadb"
140
+ * import { FaunaAdapter } from "@auth/fauna-adapter"
141
+ *
142
+ * const client = new FaunaClient({
143
+ * secret: "secret",
144
+ * scheme: "http",
145
+ * domain: "localhost",
146
+ * port: 8443,
147
+ * })
148
+ *
149
+ * // For more information on each option (and a full list of options) go to
150
+ * // https://authjs.dev/reference/configuration/auth-options
151
+ * export default NextAuth({
152
+ * // https://authjs.dev/reference/providers/
153
+ * providers: [],
154
+ * adapter: FaunaAdapter(client)
155
+ * ...
156
+ * })
157
+ * ```
158
+ *
159
+ * ### Schema
160
+ *
161
+ * Run the following commands inside of the `Shell` tab in the Fauna dashboard to setup the appropriate collections and indexes.
162
+ *
163
+ * ```javascript
164
+ * CreateCollection({ name: "accounts" })
165
+ * CreateCollection({ name: "sessions" })
166
+ * CreateCollection({ name: "users" })
167
+ * CreateCollection({ name: "verification_tokens" })
168
+ * ```
169
+ *
170
+ * ```javascript
171
+ * CreateIndex({
172
+ * name: "account_by_provider_and_provider_account_id",
173
+ * source: Collection("accounts"),
174
+ * unique: true,
175
+ * terms: [
176
+ * { field: ["data", "provider"] },
177
+ * { field: ["data", "providerAccountId"] },
178
+ * ],
179
+ * })
180
+ * CreateIndex({
181
+ * name: "session_by_session_token",
182
+ * source: Collection("sessions"),
183
+ * unique: true,
184
+ * terms: [{ field: ["data", "sessionToken"] }],
185
+ * })
186
+ * CreateIndex({
187
+ * name: "user_by_email",
188
+ * source: Collection("users"),
189
+ * unique: true,
190
+ * terms: [{ field: ["data", "email"] }],
191
+ * })
192
+ * CreateIndex({
193
+ * name: "verification_token_by_identifier_and_token",
194
+ * source: Collection("verification_tokens"),
195
+ * unique: true,
196
+ * terms: [{ field: ["data", "identifier"] }, { field: ["data", "token"] }],
197
+ * })
198
+ * ```
199
+ *
200
+ * > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/adapters#models)
201
+ **/
202
+ export function FaunaAdapter(f: FaunaClient): Adapter {
203
+ const { Users, Accounts, Sessions, VerificationTokens } = collections
204
+ const {
205
+ AccountByProviderAndProviderAccountId,
206
+ AccountsByUser,
207
+ SessionByToken,
208
+ SessionsByUser,
209
+ UserByEmail,
210
+ VerificationTokenByIdentifierAndToken,
211
+ } = indexes
212
+ const { to, from } = format
213
+ const q = query(f, from)
214
+ return {
215
+ createUser: async (data) => (await q(Create(Users, { data: to(data) })))!,
216
+ getUser: async (id) => await q(Get(Ref(Users, id))),
217
+ getUserByEmail: async (email) => await q(Get(Match(UserByEmail, email))),
218
+ async getUserByAccount({ provider, providerAccountId }) {
219
+ const key = [provider, providerAccountId]
220
+ const ref = Match(AccountByProviderAndProviderAccountId, key)
221
+ const user = await q<AdapterUser>(
222
+ Let(
223
+ { ref },
224
+ If(
225
+ Exists(Var("ref")),
226
+ Get(Ref(Users, Select(["data", "userId"], Get(Var("ref"))))),
227
+ null
228
+ )
229
+ )
230
+ )
231
+ return user
232
+ },
233
+ updateUser: async (data) =>
234
+ (await q(Update(Ref(Users, data.id), { data: to(data) })))!,
235
+ async deleteUser(userId) {
236
+ await f.query(
237
+ Do(
238
+ Foreach(
239
+ Paginate(Match(SessionsByUser, userId)),
240
+ Lambda("ref", Delete(Var("ref")))
241
+ ),
242
+ Foreach(
243
+ Paginate(Match(AccountsByUser, userId)),
244
+ Lambda("ref", Delete(Var("ref")))
245
+ ),
246
+ Delete(Ref(Users, userId))
247
+ )
248
+ )
249
+ },
250
+ linkAccount: async (data) =>
251
+ (await q(Create(Accounts, { data: to(data) })))!,
252
+ async unlinkAccount({ provider, providerAccountId }) {
253
+ const id = [provider, providerAccountId]
254
+ await q(
255
+ Delete(
256
+ Select("ref", Get(Match(AccountByProviderAndProviderAccountId, id)))
257
+ )
258
+ )
259
+ },
260
+ createSession: async (data) =>
261
+ (await q<AdapterSession>(Create(Sessions, { data: to(data) })))!,
262
+ async getSessionAndUser(sessionToken) {
263
+ const session = await q<AdapterSession>(
264
+ Get(Match(SessionByToken, sessionToken))
265
+ )
266
+ if (!session) return null
267
+
268
+ const user = await q<AdapterUser>(Get(Ref(Users, session.userId)))
269
+
270
+ return { session, user: user! }
271
+ },
272
+ async updateSession(data) {
273
+ const ref = Select("ref", Get(Match(SessionByToken, data.sessionToken)))
274
+ return await q(Update(ref, { data: to(data) }))
275
+ },
276
+ async deleteSession(sessionToken) {
277
+ await q(Delete(Select("ref", Get(Match(SessionByToken, sessionToken)))))
278
+ },
279
+ async createVerificationToken(data) {
280
+ // @ts-expect-error
281
+ const { id: _id, ...verificationToken } = await q<VerificationToken>(
282
+ Create(VerificationTokens, { data: to(data) })
283
+ )
284
+ return verificationToken
285
+ },
286
+ async useVerificationToken({ identifier, token }) {
287
+ const key = [identifier, token]
288
+ const object = Get(Match(VerificationTokenByIdentifierAndToken, key))
289
+
290
+ const verificationToken = await q<VerificationToken>(object)
291
+ if (!verificationToken) return null
292
+
293
+ // Verification tokens can be used only once
294
+ await q(Delete(Select("ref", object)))
295
+
296
+ // @ts-expect-error
297
+ delete verificationToken.id
298
+ return verificationToken
299
+ },
300
+ }
301
+ }