@auth/fauna-adapter 1.3.3 → 2.1.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/index.d.ts +137 -63
- package/index.d.ts.map +1 -1
- package/index.js +230 -144
- package/package.json +8 -8
- package/src/index.ts +256 -215
package/index.d.ts
CHANGED
|
@@ -9,58 +9,40 @@
|
|
|
9
9
|
* ## Installation
|
|
10
10
|
*
|
|
11
11
|
* ```bash npm2yarn
|
|
12
|
-
* npm install @auth/fauna-adapter
|
|
12
|
+
* npm install @auth/fauna-adapter fauna
|
|
13
13
|
* ```
|
|
14
14
|
*
|
|
15
15
|
* @module @auth/fauna-adapter
|
|
16
16
|
*/
|
|
17
|
-
import { Client
|
|
18
|
-
import { Adapter } from "@auth/core/adapters";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
readonly Accounts: import("faunadb").Expr;
|
|
22
|
-
readonly Sessions: import("faunadb").Expr;
|
|
23
|
-
readonly VerificationTokens: import("faunadb").Expr;
|
|
17
|
+
import { Client, TimeStub, QueryValue, QueryValueObject } from "fauna";
|
|
18
|
+
import type { Adapter, AdapterUser, AdapterSession, VerificationToken, AdapterAccount } from "@auth/core/adapters";
|
|
19
|
+
type ToFauna<T> = {
|
|
20
|
+
[P in keyof T]: T[P] extends Date | null ? TimeStub | null : T[P] extends undefined ? null : T[P] extends QueryValue ? T[P] : QueryValueObject;
|
|
24
21
|
};
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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;
|
|
22
|
+
export type FaunaUser = ToFauna<AdapterUser>;
|
|
23
|
+
export type FaunaSession = ToFauna<AdapterSession>;
|
|
24
|
+
export type FaunaVerificationToken = ToFauna<VerificationToken> & {
|
|
25
|
+
id: string;
|
|
38
26
|
};
|
|
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>;
|
|
27
|
+
export type FaunaAccount = ToFauna<AdapterAccount>;
|
|
44
28
|
/**
|
|
45
29
|
*
|
|
46
30
|
* ## Setup
|
|
47
31
|
*
|
|
48
|
-
* This is the Fauna Adapter for [
|
|
32
|
+
* This is the Fauna Adapter for [Auth.js](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` and other framework packages. It is not a standalone package.
|
|
49
33
|
*
|
|
50
|
-
* You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/
|
|
34
|
+
* You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/adapter/fauna](https://authjs.dev/reference/adapter/fauna).
|
|
51
35
|
*
|
|
52
36
|
* ### Configure Auth.js
|
|
53
37
|
*
|
|
54
38
|
* ```javascript title="pages/api/auth/[...nextauth].js"
|
|
55
39
|
* import NextAuth from "next-auth"
|
|
56
|
-
* import { Client
|
|
40
|
+
* import { Client } from "fauna"
|
|
57
41
|
* import { FaunaAdapter } from "@auth/fauna-adapter"
|
|
58
42
|
*
|
|
59
|
-
* const client = new
|
|
43
|
+
* const client = new Client({
|
|
60
44
|
* secret: "secret",
|
|
61
|
-
*
|
|
62
|
-
* domain: "localhost",
|
|
63
|
-
* port: 8443,
|
|
45
|
+
* endpoint: new URL('http://localhost:8443')
|
|
64
46
|
* })
|
|
65
47
|
*
|
|
66
48
|
* // For more information on each option (and a full list of options) go to
|
|
@@ -75,46 +57,138 @@ export declare function query(f: FaunaClient, format: (...args: any) => any): <T
|
|
|
75
57
|
*
|
|
76
58
|
* ### Schema
|
|
77
59
|
*
|
|
78
|
-
* Run the following
|
|
60
|
+
* Run the following FQL code inside the `Shell` tab in the Fauna dashboard to set up the appropriate collections and indexes.
|
|
79
61
|
*
|
|
80
62
|
* ```javascript
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
63
|
+
* Collection.create({
|
|
64
|
+
* name: "Account",
|
|
65
|
+
* indexes: {
|
|
66
|
+
* byUserId: {
|
|
67
|
+
* terms: [
|
|
68
|
+
* { field: "userId" }
|
|
69
|
+
* ]
|
|
70
|
+
* },
|
|
71
|
+
* byProviderAndProviderAccountId: {
|
|
72
|
+
* terms [
|
|
73
|
+
* { field: "provider" },
|
|
74
|
+
* { field: "providerAccountId" }
|
|
75
|
+
* ]
|
|
76
|
+
* },
|
|
77
|
+
* }
|
|
78
|
+
* })
|
|
79
|
+
* Collection.create({
|
|
80
|
+
* name: "Session",
|
|
81
|
+
* constraints: [
|
|
82
|
+
* {
|
|
83
|
+
* unique: ["sessionToken"],
|
|
84
|
+
* status: "active",
|
|
85
|
+
* }
|
|
86
|
+
* ],
|
|
87
|
+
* indexes: {
|
|
88
|
+
* bySessionToken: {
|
|
89
|
+
* terms: [
|
|
90
|
+
* { field: "sessionToken" }
|
|
91
|
+
* ]
|
|
92
|
+
* },
|
|
93
|
+
* byUserId: {
|
|
94
|
+
* terms [
|
|
95
|
+
* { field: "userId" }
|
|
96
|
+
* ]
|
|
97
|
+
* },
|
|
98
|
+
* }
|
|
99
|
+
* })
|
|
100
|
+
* Collection.create({
|
|
101
|
+
* name: "User",
|
|
102
|
+
* constraints: [
|
|
103
|
+
* {
|
|
104
|
+
* unique: ["email"],
|
|
105
|
+
* status: "active",
|
|
106
|
+
* }
|
|
107
|
+
* ],
|
|
108
|
+
* indexes: {
|
|
109
|
+
* byEmail: {
|
|
110
|
+
* terms [
|
|
111
|
+
* { field: "email" }
|
|
112
|
+
* ]
|
|
113
|
+
* },
|
|
114
|
+
* }
|
|
115
|
+
* })
|
|
116
|
+
* Collection.create({
|
|
117
|
+
* name: "VerificationToken",
|
|
118
|
+
* indexes: {
|
|
119
|
+
* byIdentifierAndToken: {
|
|
120
|
+
* terms [
|
|
121
|
+
* { field: "identifier" },
|
|
122
|
+
* { field: "token" }
|
|
123
|
+
* ]
|
|
124
|
+
* },
|
|
125
|
+
* }
|
|
126
|
+
* })
|
|
85
127
|
* ```
|
|
86
128
|
*
|
|
129
|
+
* > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/core/adapters#models)
|
|
130
|
+
*
|
|
131
|
+
* ### Migrating from v1
|
|
132
|
+
* In v2, we've renamed the collections to use uppercase naming, in accordance with Fauna best practices. If you're migrating from v1, you'll need to rename your collections to match the new naming scheme.
|
|
133
|
+
* Additionally, we've renamed the indexes to match the new method-like index names.
|
|
134
|
+
*
|
|
135
|
+
* #### Migration script
|
|
136
|
+
* Run this FQL script inside a Fauna shell for the database you're migrating from v1 to v2 (it will rename your collections and indexes to match):
|
|
137
|
+
*
|
|
87
138
|
* ```javascript
|
|
88
|
-
*
|
|
89
|
-
* name: "
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
139
|
+
* Collection.byName("accounts")!.update({
|
|
140
|
+
* name: "Account"
|
|
141
|
+
* indexes: {
|
|
142
|
+
* byUserId: {
|
|
143
|
+
* terms: [{ field: "userId" }]
|
|
144
|
+
* },
|
|
145
|
+
* byProviderAndProviderAccountId: {
|
|
146
|
+
* terms: [{ field: "provider" }, { field: "providerAccountId" }]
|
|
147
|
+
* },
|
|
148
|
+
* account_by_provider_and_provider_account_id: null,
|
|
149
|
+
* accounts_by_user_id: null
|
|
150
|
+
* }
|
|
96
151
|
* })
|
|
97
|
-
*
|
|
98
|
-
* name: "
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
152
|
+
* Collection.byName("sessions")!.update({
|
|
153
|
+
* name: "Session",
|
|
154
|
+
* indexes: {
|
|
155
|
+
* bySessionToken: {
|
|
156
|
+
* terms: [{ field: "sessionToken" }]
|
|
157
|
+
* },
|
|
158
|
+
* byUserId: {
|
|
159
|
+
* terms: [{ field: "userId" }]
|
|
160
|
+
* },
|
|
161
|
+
* session_by_session_token: null,
|
|
162
|
+
* sessions_by_user_id: null
|
|
163
|
+
* }
|
|
102
164
|
* })
|
|
103
|
-
*
|
|
104
|
-
* name: "
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
165
|
+
* Collection.byName("users")!.update({
|
|
166
|
+
* name: "User",
|
|
167
|
+
* indexes: {
|
|
168
|
+
* byEmail: {
|
|
169
|
+
* terms: [{ field: "email" }]
|
|
170
|
+
* },
|
|
171
|
+
* user_by_email: null
|
|
172
|
+
* }
|
|
108
173
|
* })
|
|
109
|
-
*
|
|
110
|
-
* name: "
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
174
|
+
* Collection.byName("verification_tokens")!.update({
|
|
175
|
+
* name: "VerificationToken",
|
|
176
|
+
* indexes: {
|
|
177
|
+
* byIdentifierAndToken: {
|
|
178
|
+
* terms: [{ field: "identifier" }, { field: "token" }]
|
|
179
|
+
* },
|
|
180
|
+
* verification_token_by_identifier_and_token: null
|
|
181
|
+
* }
|
|
114
182
|
* })
|
|
115
183
|
* ```
|
|
116
184
|
*
|
|
117
|
-
* > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/core/adapters#models)
|
|
118
185
|
**/
|
|
119
|
-
export declare function FaunaAdapter(
|
|
186
|
+
export declare function FaunaAdapter(client: Client): Adapter;
|
|
187
|
+
export declare const format: {
|
|
188
|
+
/** Takes an object that's coming from the database and converts it to plain JavaScript. */
|
|
189
|
+
from<T>(object?: Record<string, any>): T;
|
|
190
|
+
/** Takes an object that's coming from Auth.js and prepares it to be written to the database. */
|
|
191
|
+
to<T_1>(object: Record<string, any>): T_1;
|
|
192
|
+
};
|
|
193
|
+
export {};
|
|
120
194
|
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAqB,UAAU,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAA;AAEzF,OAAO,KAAK,EACV,OAAO,EACP,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,cAAc,EACf,MAAM,qBAAqB,CAAA;AAE5B,KAAK,OAAO,CAAC,CAAC,IAAI;KACf,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,GAAG,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,gBAAgB;CAC/I,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;AAC5C,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;AAClD,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,iBAAiB,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAA;AAChF,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA6JI;AACJ,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CA4HpD;AAED,eAAO,MAAM,MAAM;IACjB,2FAA2F;qBAC3E,OAAO,MAAM,EAAE,GAAG,CAAC;IASnC,gGAAgG;oBAClF,OAAO,MAAM,EAAE,GAAG,CAAC;CAUlC,CAAA"}
|
package/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
1
|
/**
|
|
3
2
|
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
4
3
|
* <p style={{fontWeight: "normal"}}>Official <a href="https://docs.fauna.com/fauna/current/">Fauna</a> adapter for Auth.js / NextAuth.js.</p>
|
|
@@ -10,99 +9,30 @@
|
|
|
10
9
|
* ## Installation
|
|
11
10
|
*
|
|
12
11
|
* ```bash npm2yarn
|
|
13
|
-
* npm install @auth/fauna-adapter
|
|
12
|
+
* npm install @auth/fauna-adapter fauna
|
|
14
13
|
* ```
|
|
15
14
|
*
|
|
16
15
|
* @module @auth/fauna-adapter
|
|
17
16
|
*/
|
|
18
|
-
import {
|
|
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
|
-
}
|
|
17
|
+
import { TimeStub, fql, NullDocument } from "fauna";
|
|
86
18
|
/**
|
|
87
19
|
*
|
|
88
20
|
* ## Setup
|
|
89
21
|
*
|
|
90
|
-
* This is the Fauna Adapter for [
|
|
22
|
+
* This is the Fauna Adapter for [Auth.js](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` and other framework packages. It is not a standalone package.
|
|
91
23
|
*
|
|
92
|
-
* You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/
|
|
24
|
+
* You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/adapter/fauna](https://authjs.dev/reference/adapter/fauna).
|
|
93
25
|
*
|
|
94
26
|
* ### Configure Auth.js
|
|
95
27
|
*
|
|
96
28
|
* ```javascript title="pages/api/auth/[...nextauth].js"
|
|
97
29
|
* import NextAuth from "next-auth"
|
|
98
|
-
* import { Client
|
|
30
|
+
* import { Client } from "fauna"
|
|
99
31
|
* import { FaunaAdapter } from "@auth/fauna-adapter"
|
|
100
32
|
*
|
|
101
|
-
* const client = new
|
|
33
|
+
* const client = new Client({
|
|
102
34
|
* secret: "secret",
|
|
103
|
-
*
|
|
104
|
-
* domain: "localhost",
|
|
105
|
-
* port: 8443,
|
|
35
|
+
* endpoint: new URL('http://localhost:8443')
|
|
106
36
|
* })
|
|
107
37
|
*
|
|
108
38
|
* // For more information on each option (and a full list of options) go to
|
|
@@ -117,102 +47,258 @@ export function query(f, format) {
|
|
|
117
47
|
*
|
|
118
48
|
* ### Schema
|
|
119
49
|
*
|
|
120
|
-
* Run the following
|
|
50
|
+
* Run the following FQL code inside the `Shell` tab in the Fauna dashboard to set up the appropriate collections and indexes.
|
|
121
51
|
*
|
|
122
52
|
* ```javascript
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
53
|
+
* Collection.create({
|
|
54
|
+
* name: "Account",
|
|
55
|
+
* indexes: {
|
|
56
|
+
* byUserId: {
|
|
57
|
+
* terms: [
|
|
58
|
+
* { field: "userId" }
|
|
59
|
+
* ]
|
|
60
|
+
* },
|
|
61
|
+
* byProviderAndProviderAccountId: {
|
|
62
|
+
* terms [
|
|
63
|
+
* { field: "provider" },
|
|
64
|
+
* { field: "providerAccountId" }
|
|
65
|
+
* ]
|
|
66
|
+
* },
|
|
67
|
+
* }
|
|
68
|
+
* })
|
|
69
|
+
* Collection.create({
|
|
70
|
+
* name: "Session",
|
|
71
|
+
* constraints: [
|
|
72
|
+
* {
|
|
73
|
+
* unique: ["sessionToken"],
|
|
74
|
+
* status: "active",
|
|
75
|
+
* }
|
|
76
|
+
* ],
|
|
77
|
+
* indexes: {
|
|
78
|
+
* bySessionToken: {
|
|
79
|
+
* terms: [
|
|
80
|
+
* { field: "sessionToken" }
|
|
81
|
+
* ]
|
|
82
|
+
* },
|
|
83
|
+
* byUserId: {
|
|
84
|
+
* terms [
|
|
85
|
+
* { field: "userId" }
|
|
86
|
+
* ]
|
|
87
|
+
* },
|
|
88
|
+
* }
|
|
89
|
+
* })
|
|
90
|
+
* Collection.create({
|
|
91
|
+
* name: "User",
|
|
92
|
+
* constraints: [
|
|
93
|
+
* {
|
|
94
|
+
* unique: ["email"],
|
|
95
|
+
* status: "active",
|
|
96
|
+
* }
|
|
97
|
+
* ],
|
|
98
|
+
* indexes: {
|
|
99
|
+
* byEmail: {
|
|
100
|
+
* terms [
|
|
101
|
+
* { field: "email" }
|
|
102
|
+
* ]
|
|
103
|
+
* },
|
|
104
|
+
* }
|
|
105
|
+
* })
|
|
106
|
+
* Collection.create({
|
|
107
|
+
* name: "VerificationToken",
|
|
108
|
+
* indexes: {
|
|
109
|
+
* byIdentifierAndToken: {
|
|
110
|
+
* terms [
|
|
111
|
+
* { field: "identifier" },
|
|
112
|
+
* { field: "token" }
|
|
113
|
+
* ]
|
|
114
|
+
* },
|
|
115
|
+
* }
|
|
116
|
+
* })
|
|
127
117
|
* ```
|
|
128
118
|
*
|
|
119
|
+
* > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/core/adapters#models)
|
|
120
|
+
*
|
|
121
|
+
* ### Migrating from v1
|
|
122
|
+
* In v2, we've renamed the collections to use uppercase naming, in accordance with Fauna best practices. If you're migrating from v1, you'll need to rename your collections to match the new naming scheme.
|
|
123
|
+
* Additionally, we've renamed the indexes to match the new method-like index names.
|
|
124
|
+
*
|
|
125
|
+
* #### Migration script
|
|
126
|
+
* Run this FQL script inside a Fauna shell for the database you're migrating from v1 to v2 (it will rename your collections and indexes to match):
|
|
127
|
+
*
|
|
129
128
|
* ```javascript
|
|
130
|
-
*
|
|
131
|
-
* name: "
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
129
|
+
* Collection.byName("accounts")!.update({
|
|
130
|
+
* name: "Account"
|
|
131
|
+
* indexes: {
|
|
132
|
+
* byUserId: {
|
|
133
|
+
* terms: [{ field: "userId" }]
|
|
134
|
+
* },
|
|
135
|
+
* byProviderAndProviderAccountId: {
|
|
136
|
+
* terms: [{ field: "provider" }, { field: "providerAccountId" }]
|
|
137
|
+
* },
|
|
138
|
+
* account_by_provider_and_provider_account_id: null,
|
|
139
|
+
* accounts_by_user_id: null
|
|
140
|
+
* }
|
|
138
141
|
* })
|
|
139
|
-
*
|
|
140
|
-
* name: "
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
142
|
+
* Collection.byName("sessions")!.update({
|
|
143
|
+
* name: "Session",
|
|
144
|
+
* indexes: {
|
|
145
|
+
* bySessionToken: {
|
|
146
|
+
* terms: [{ field: "sessionToken" }]
|
|
147
|
+
* },
|
|
148
|
+
* byUserId: {
|
|
149
|
+
* terms: [{ field: "userId" }]
|
|
150
|
+
* },
|
|
151
|
+
* session_by_session_token: null,
|
|
152
|
+
* sessions_by_user_id: null
|
|
153
|
+
* }
|
|
144
154
|
* })
|
|
145
|
-
*
|
|
146
|
-
* name: "
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
155
|
+
* Collection.byName("users")!.update({
|
|
156
|
+
* name: "User",
|
|
157
|
+
* indexes: {
|
|
158
|
+
* byEmail: {
|
|
159
|
+
* terms: [{ field: "email" }]
|
|
160
|
+
* },
|
|
161
|
+
* user_by_email: null
|
|
162
|
+
* }
|
|
150
163
|
* })
|
|
151
|
-
*
|
|
152
|
-
* name: "
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
164
|
+
* Collection.byName("verification_tokens")!.update({
|
|
165
|
+
* name: "VerificationToken",
|
|
166
|
+
* indexes: {
|
|
167
|
+
* byIdentifierAndToken: {
|
|
168
|
+
* terms: [{ field: "identifier" }, { field: "token" }]
|
|
169
|
+
* },
|
|
170
|
+
* verification_token_by_identifier_and_token: null
|
|
171
|
+
* }
|
|
156
172
|
* })
|
|
157
173
|
* ```
|
|
158
174
|
*
|
|
159
|
-
* > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/core/adapters#models)
|
|
160
175
|
**/
|
|
161
|
-
export function FaunaAdapter(
|
|
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);
|
|
176
|
+
export function FaunaAdapter(client) {
|
|
166
177
|
return {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
async createUser(user) {
|
|
179
|
+
const response = await client.query(fql `User.create(${format.to(user)})`);
|
|
180
|
+
return format.from(response.data);
|
|
181
|
+
},
|
|
182
|
+
async getUser(id) {
|
|
183
|
+
const response = await client.query(fql `User.byId(${id})`);
|
|
184
|
+
if (response.data instanceof NullDocument)
|
|
185
|
+
return null;
|
|
186
|
+
return format.from(response.data);
|
|
187
|
+
},
|
|
188
|
+
async getUserByEmail(email) {
|
|
189
|
+
const response = await client.query(fql `User.byEmail(${email}).first()`);
|
|
190
|
+
if (response.data === null)
|
|
191
|
+
return null;
|
|
192
|
+
return format.from(response.data);
|
|
193
|
+
},
|
|
170
194
|
async getUserByAccount({ provider, providerAccountId }) {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
195
|
+
const response = await client.query(fql `
|
|
196
|
+
let account = Account.byProviderAndProviderAccountId(${provider}, ${providerAccountId}).first()
|
|
197
|
+
if (account != null) {
|
|
198
|
+
User.byId(account.userId)
|
|
199
|
+
} else {
|
|
200
|
+
null
|
|
201
|
+
}
|
|
202
|
+
`);
|
|
203
|
+
return format.from(response.data);
|
|
204
|
+
},
|
|
205
|
+
async updateUser(user) {
|
|
206
|
+
const _user = { ...user };
|
|
207
|
+
delete _user.id;
|
|
208
|
+
const response = await client.query(fql `User.byId(${user.id}).update(${format.to(_user)})`);
|
|
209
|
+
return format.from(response.data);
|
|
175
210
|
},
|
|
176
|
-
updateUser: async (data) => (await q(Update(Ref(Users, data.id), { data: to(data) }))),
|
|
177
211
|
async deleteUser(userId) {
|
|
178
|
-
await
|
|
212
|
+
await client.query(fql `
|
|
213
|
+
// Delete the user's sessions
|
|
214
|
+
Session.byUserId(${userId}).forEach(session => session.delete())
|
|
215
|
+
|
|
216
|
+
// Delete the user's accounts
|
|
217
|
+
Account.byUserId(${userId}).forEach(account => account.delete())
|
|
218
|
+
|
|
219
|
+
// Delete the user
|
|
220
|
+
User.byId(${userId}).delete()
|
|
221
|
+
`);
|
|
222
|
+
},
|
|
223
|
+
async linkAccount(account) {
|
|
224
|
+
await client.query(fql `Account.create(${format.to(account)})`);
|
|
225
|
+
return account;
|
|
179
226
|
},
|
|
180
|
-
linkAccount: async (data) => (await q(Create(Accounts, { data: to(data) }))),
|
|
181
227
|
async unlinkAccount({ provider, providerAccountId }) {
|
|
182
|
-
const
|
|
183
|
-
|
|
228
|
+
const response = await client.query(fql `Account.byProviderAndProviderAccountId(${provider}, ${providerAccountId}).first().delete()`);
|
|
229
|
+
return format.from(response.data);
|
|
184
230
|
},
|
|
185
|
-
createSession: async (data) => (await q(Create(Sessions, { data: to(data) }))),
|
|
186
231
|
async getSessionAndUser(sessionToken) {
|
|
187
|
-
const
|
|
188
|
-
|
|
232
|
+
const response = await client.query(fql `
|
|
233
|
+
let session = Session.bySessionToken(${sessionToken}).first()
|
|
234
|
+
if (session != null) {
|
|
235
|
+
let user = User.byId(session.userId)
|
|
236
|
+
if (user != null) {
|
|
237
|
+
[user, session]
|
|
238
|
+
} else {
|
|
239
|
+
null
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
null
|
|
243
|
+
}
|
|
244
|
+
`);
|
|
245
|
+
if (response.data === null)
|
|
189
246
|
return null;
|
|
190
|
-
const user =
|
|
191
|
-
return { session, user: user };
|
|
247
|
+
const [user, session] = response.data ?? [];
|
|
248
|
+
return { session: format.from(session), user: format.from(user) };
|
|
192
249
|
},
|
|
193
|
-
async
|
|
194
|
-
|
|
195
|
-
return
|
|
250
|
+
async createSession(session) {
|
|
251
|
+
await client.query(fql `Session.create(${format.to(session)})`);
|
|
252
|
+
return session;
|
|
253
|
+
},
|
|
254
|
+
async updateSession(session) {
|
|
255
|
+
const response = await client.query(fql `Session.bySessionToken(${session.sessionToken}).first().update(${format.to(session)})`);
|
|
256
|
+
return format.from(response.data);
|
|
196
257
|
},
|
|
197
258
|
async deleteSession(sessionToken) {
|
|
198
|
-
await
|
|
259
|
+
await client.query(fql `Session.bySessionToken(${sessionToken}).first().delete()`);
|
|
199
260
|
},
|
|
200
|
-
async createVerificationToken(
|
|
201
|
-
|
|
202
|
-
const { id: _id, ...verificationToken } = await q(Create(VerificationTokens, { data: to(data) }));
|
|
261
|
+
async createVerificationToken(verificationToken) {
|
|
262
|
+
await client.query(fql `VerificationToken.create(${format.to(verificationToken)})`);
|
|
203
263
|
return verificationToken;
|
|
204
264
|
},
|
|
205
265
|
async useVerificationToken({ identifier, token }) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
const verificationToken = await q(object);
|
|
209
|
-
if (!verificationToken)
|
|
266
|
+
const response = await client.query(fql `VerificationToken.byIdentifierAndToken(${identifier}, ${token}).first()`);
|
|
267
|
+
if (response.data === null)
|
|
210
268
|
return null;
|
|
211
|
-
//
|
|
212
|
-
await
|
|
213
|
-
|
|
214
|
-
delete
|
|
215
|
-
return
|
|
269
|
+
// Delete the verification token so it can only be used once
|
|
270
|
+
await client.query(fql `VerificationToken.byId(${response.data.id}).delete()`);
|
|
271
|
+
const _verificationToken = { ...response.data };
|
|
272
|
+
delete _verificationToken.id;
|
|
273
|
+
return format.from(_verificationToken);
|
|
216
274
|
},
|
|
217
275
|
};
|
|
218
276
|
}
|
|
277
|
+
export const format = {
|
|
278
|
+
/** Takes an object that's coming from the database and converts it to plain JavaScript. */
|
|
279
|
+
from(object = {}) {
|
|
280
|
+
if (!object)
|
|
281
|
+
return null;
|
|
282
|
+
const newObject = {};
|
|
283
|
+
for (const [key, value] of Object.entries(object))
|
|
284
|
+
if (key === "coll" || key === "ts")
|
|
285
|
+
continue;
|
|
286
|
+
else if (value instanceof TimeStub)
|
|
287
|
+
newObject[key] = value.toDate();
|
|
288
|
+
else
|
|
289
|
+
newObject[key] = value;
|
|
290
|
+
return newObject;
|
|
291
|
+
},
|
|
292
|
+
/** Takes an object that's coming from Auth.js and prepares it to be written to the database. */
|
|
293
|
+
to(object) {
|
|
294
|
+
const newObject = {};
|
|
295
|
+
for (const [key, value] of Object.entries(object))
|
|
296
|
+
if (value instanceof Date)
|
|
297
|
+
newObject[key] = TimeStub.fromDate(value);
|
|
298
|
+
else if (typeof value === "string" && !isNaN(Date.parse(value)))
|
|
299
|
+
newObject[key] = TimeStub.from(value);
|
|
300
|
+
else
|
|
301
|
+
newObject[key] = value ?? null;
|
|
302
|
+
return newObject;
|
|
303
|
+
},
|
|
304
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@auth/fauna-adapter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Fauna Adapter for Auth.js",
|
|
5
5
|
"homepage": "https://authjs.dev",
|
|
6
6
|
"repository": "https://github.com/nextauthjs/next-auth",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"author": "Bhanu Teja P",
|
|
11
11
|
"contributors": [
|
|
12
12
|
"Nico Domino <yo@ndo.dev>",
|
|
13
|
-
"Balázs Orbán <info@balazsorban.com>"
|
|
13
|
+
"Balázs Orbán <info@balazsorban.com>",
|
|
14
|
+
"Aske Hippe Brun <mail@askehippebrun.com>"
|
|
14
15
|
],
|
|
15
16
|
"type": "module",
|
|
16
17
|
"types": "./index.d.ts",
|
|
@@ -37,20 +38,19 @@
|
|
|
37
38
|
"access": "public"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@auth/core": "0.
|
|
41
|
+
"@auth/core": "0.28.0"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
|
-
"
|
|
44
|
+
"fauna": "^1.3.1"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
|
-
"
|
|
47
|
-
"
|
|
47
|
+
"fauna": "^1.3.1",
|
|
48
|
+
"fauna-shell": "1.2.1"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
51
|
+
"fauna": "fauna",
|
|
50
52
|
"build": "tsc",
|
|
51
53
|
"dev": "tsc -w",
|
|
52
|
-
"clean": "rm -rf dist",
|
|
53
|
-
"migrate": "fauna-schema-migrate generate",
|
|
54
54
|
"test": "./test/test.sh"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
1
|
/**
|
|
3
2
|
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
4
3
|
* <p style={{fontWeight: "normal"}}>Official <a href="https://docs.fauna.com/fauna/current/">Fauna</a> adapter for Auth.js / NextAuth.js.</p>
|
|
@@ -10,140 +9,48 @@
|
|
|
10
9
|
* ## Installation
|
|
11
10
|
*
|
|
12
11
|
* ```bash npm2yarn
|
|
13
|
-
* npm install @auth/fauna-adapter
|
|
12
|
+
* npm install @auth/fauna-adapter fauna
|
|
14
13
|
* ```
|
|
15
14
|
*
|
|
16
15
|
* @module @auth/fauna-adapter
|
|
17
16
|
*/
|
|
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"
|
|
17
|
+
import { Client, TimeStub, fql, NullDocument, QueryValue, QueryValueObject } from "fauna"
|
|
41
18
|
|
|
42
|
-
import {
|
|
19
|
+
import type {
|
|
43
20
|
Adapter,
|
|
44
|
-
AdapterSession,
|
|
45
21
|
AdapterUser,
|
|
22
|
+
AdapterSession,
|
|
46
23
|
VerificationToken,
|
|
24
|
+
AdapterAccount,
|
|
47
25
|
} from "@auth/core/adapters"
|
|
48
26
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
},
|
|
27
|
+
type ToFauna<T> = {
|
|
28
|
+
[P in keyof T]: T[P] extends Date | null ? TimeStub | null : T[P] extends undefined ? null : T[P] extends QueryValue ? T[P] : QueryValueObject
|
|
96
29
|
}
|
|
97
30
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
}
|
|
31
|
+
export type FaunaUser = ToFauna<AdapterUser>
|
|
32
|
+
export type FaunaSession = ToFauna<AdapterSession>
|
|
33
|
+
export type FaunaVerificationToken = ToFauna<VerificationToken> & { id: string }
|
|
34
|
+
export type FaunaAccount = ToFauna<AdapterAccount>
|
|
126
35
|
|
|
127
36
|
/**
|
|
128
37
|
*
|
|
129
38
|
* ## Setup
|
|
130
39
|
*
|
|
131
|
-
* This is the Fauna Adapter for [
|
|
40
|
+
* This is the Fauna Adapter for [Auth.js](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` and other framework packages. It is not a standalone package.
|
|
132
41
|
*
|
|
133
|
-
* You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/
|
|
42
|
+
* You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/adapter/fauna](https://authjs.dev/reference/adapter/fauna).
|
|
134
43
|
*
|
|
135
44
|
* ### Configure Auth.js
|
|
136
45
|
*
|
|
137
46
|
* ```javascript title="pages/api/auth/[...nextauth].js"
|
|
138
47
|
* import NextAuth from "next-auth"
|
|
139
|
-
* import { Client
|
|
48
|
+
* import { Client } from "fauna"
|
|
140
49
|
* import { FaunaAdapter } from "@auth/fauna-adapter"
|
|
141
50
|
*
|
|
142
|
-
* const client = new
|
|
51
|
+
* const client = new Client({
|
|
143
52
|
* secret: "secret",
|
|
144
|
-
*
|
|
145
|
-
* domain: "localhost",
|
|
146
|
-
* port: 8443,
|
|
53
|
+
* endpoint: new URL('http://localhost:8443')
|
|
147
54
|
* })
|
|
148
55
|
*
|
|
149
56
|
* // For more information on each option (and a full list of options) go to
|
|
@@ -158,144 +65,278 @@ export function query(f: FaunaClient, format: (...args: any) => any) {
|
|
|
158
65
|
*
|
|
159
66
|
* ### Schema
|
|
160
67
|
*
|
|
161
|
-
* Run the following
|
|
68
|
+
* Run the following FQL code inside the `Shell` tab in the Fauna dashboard to set up the appropriate collections and indexes.
|
|
162
69
|
*
|
|
163
70
|
* ```javascript
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
71
|
+
* Collection.create({
|
|
72
|
+
* name: "Account",
|
|
73
|
+
* indexes: {
|
|
74
|
+
* byUserId: {
|
|
75
|
+
* terms: [
|
|
76
|
+
* { field: "userId" }
|
|
77
|
+
* ]
|
|
78
|
+
* },
|
|
79
|
+
* byProviderAndProviderAccountId: {
|
|
80
|
+
* terms [
|
|
81
|
+
* { field: "provider" },
|
|
82
|
+
* { field: "providerAccountId" }
|
|
83
|
+
* ]
|
|
84
|
+
* },
|
|
85
|
+
* }
|
|
86
|
+
* })
|
|
87
|
+
* Collection.create({
|
|
88
|
+
* name: "Session",
|
|
89
|
+
* constraints: [
|
|
90
|
+
* {
|
|
91
|
+
* unique: ["sessionToken"],
|
|
92
|
+
* status: "active",
|
|
93
|
+
* }
|
|
94
|
+
* ],
|
|
95
|
+
* indexes: {
|
|
96
|
+
* bySessionToken: {
|
|
97
|
+
* terms: [
|
|
98
|
+
* { field: "sessionToken" }
|
|
99
|
+
* ]
|
|
100
|
+
* },
|
|
101
|
+
* byUserId: {
|
|
102
|
+
* terms [
|
|
103
|
+
* { field: "userId" }
|
|
104
|
+
* ]
|
|
105
|
+
* },
|
|
106
|
+
* }
|
|
107
|
+
* })
|
|
108
|
+
* Collection.create({
|
|
109
|
+
* name: "User",
|
|
110
|
+
* constraints: [
|
|
111
|
+
* {
|
|
112
|
+
* unique: ["email"],
|
|
113
|
+
* status: "active",
|
|
114
|
+
* }
|
|
115
|
+
* ],
|
|
116
|
+
* indexes: {
|
|
117
|
+
* byEmail: {
|
|
118
|
+
* terms [
|
|
119
|
+
* { field: "email" }
|
|
120
|
+
* ]
|
|
121
|
+
* },
|
|
122
|
+
* }
|
|
123
|
+
* })
|
|
124
|
+
* Collection.create({
|
|
125
|
+
* name: "VerificationToken",
|
|
126
|
+
* indexes: {
|
|
127
|
+
* byIdentifierAndToken: {
|
|
128
|
+
* terms [
|
|
129
|
+
* { field: "identifier" },
|
|
130
|
+
* { field: "token" }
|
|
131
|
+
* ]
|
|
132
|
+
* },
|
|
133
|
+
* }
|
|
134
|
+
* })
|
|
168
135
|
* ```
|
|
169
136
|
*
|
|
137
|
+
* > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/core/adapters#models)
|
|
138
|
+
*
|
|
139
|
+
* ### Migrating from v1
|
|
140
|
+
* In v2, we've renamed the collections to use uppercase naming, in accordance with Fauna best practices. If you're migrating from v1, you'll need to rename your collections to match the new naming scheme.
|
|
141
|
+
* Additionally, we've renamed the indexes to match the new method-like index names.
|
|
142
|
+
*
|
|
143
|
+
* #### Migration script
|
|
144
|
+
* Run this FQL script inside a Fauna shell for the database you're migrating from v1 to v2 (it will rename your collections and indexes to match):
|
|
145
|
+
*
|
|
170
146
|
* ```javascript
|
|
171
|
-
*
|
|
172
|
-
* name: "
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
147
|
+
* Collection.byName("accounts")!.update({
|
|
148
|
+
* name: "Account"
|
|
149
|
+
* indexes: {
|
|
150
|
+
* byUserId: {
|
|
151
|
+
* terms: [{ field: "userId" }]
|
|
152
|
+
* },
|
|
153
|
+
* byProviderAndProviderAccountId: {
|
|
154
|
+
* terms: [{ field: "provider" }, { field: "providerAccountId" }]
|
|
155
|
+
* },
|
|
156
|
+
* account_by_provider_and_provider_account_id: null,
|
|
157
|
+
* accounts_by_user_id: null
|
|
158
|
+
* }
|
|
179
159
|
* })
|
|
180
|
-
*
|
|
181
|
-
* name: "
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
160
|
+
* Collection.byName("sessions")!.update({
|
|
161
|
+
* name: "Session",
|
|
162
|
+
* indexes: {
|
|
163
|
+
* bySessionToken: {
|
|
164
|
+
* terms: [{ field: "sessionToken" }]
|
|
165
|
+
* },
|
|
166
|
+
* byUserId: {
|
|
167
|
+
* terms: [{ field: "userId" }]
|
|
168
|
+
* },
|
|
169
|
+
* session_by_session_token: null,
|
|
170
|
+
* sessions_by_user_id: null
|
|
171
|
+
* }
|
|
185
172
|
* })
|
|
186
|
-
*
|
|
187
|
-
* name: "
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
173
|
+
* Collection.byName("users")!.update({
|
|
174
|
+
* name: "User",
|
|
175
|
+
* indexes: {
|
|
176
|
+
* byEmail: {
|
|
177
|
+
* terms: [{ field: "email" }]
|
|
178
|
+
* },
|
|
179
|
+
* user_by_email: null
|
|
180
|
+
* }
|
|
191
181
|
* })
|
|
192
|
-
*
|
|
193
|
-
* name: "
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
182
|
+
* Collection.byName("verification_tokens")!.update({
|
|
183
|
+
* name: "VerificationToken",
|
|
184
|
+
* indexes: {
|
|
185
|
+
* byIdentifierAndToken: {
|
|
186
|
+
* terms: [{ field: "identifier" }, { field: "token" }]
|
|
187
|
+
* },
|
|
188
|
+
* verification_token_by_identifier_and_token: null
|
|
189
|
+
* }
|
|
197
190
|
* })
|
|
198
191
|
* ```
|
|
199
192
|
*
|
|
200
|
-
* > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/core/adapters#models)
|
|
201
193
|
**/
|
|
202
|
-
export function FaunaAdapter(
|
|
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)
|
|
194
|
+
export function FaunaAdapter(client: Client): Adapter {
|
|
214
195
|
return {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
196
|
+
async createUser(user) {
|
|
197
|
+
const response = await client.query<FaunaUser>(
|
|
198
|
+
fql`User.create(${format.to(user)})`,
|
|
199
|
+
)
|
|
200
|
+
return format.from(response.data)
|
|
201
|
+
},
|
|
202
|
+
async getUser(id) {
|
|
203
|
+
const response = await client.query<FaunaUser | NullDocument>(
|
|
204
|
+
fql`User.byId(${id})`,
|
|
205
|
+
)
|
|
206
|
+
if (response.data instanceof NullDocument) return null
|
|
207
|
+
return format.from(response.data)
|
|
208
|
+
},
|
|
209
|
+
async getUserByEmail(email) {
|
|
210
|
+
const response = await client.query<FaunaUser>(
|
|
211
|
+
fql`User.byEmail(${email}).first()`,
|
|
212
|
+
)
|
|
213
|
+
if (response.data === null) return null
|
|
214
|
+
return format.from(response.data)
|
|
215
|
+
},
|
|
218
216
|
async getUserByAccount({ provider, providerAccountId }) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
217
|
+
const response = await client.query<FaunaUser>(fql`
|
|
218
|
+
let account = Account.byProviderAndProviderAccountId(${provider}, ${providerAccountId}).first()
|
|
219
|
+
if (account != null) {
|
|
220
|
+
User.byId(account.userId)
|
|
221
|
+
} else {
|
|
222
|
+
null
|
|
223
|
+
}
|
|
224
|
+
`)
|
|
225
|
+
return format.from(response.data)
|
|
226
|
+
},
|
|
227
|
+
async updateUser(user) {
|
|
228
|
+
const _user: Partial<AdapterUser> = { ...user }
|
|
229
|
+
delete _user.id
|
|
230
|
+
const response = await client.query<FaunaUser>(
|
|
231
|
+
fql`User.byId(${user.id}).update(${format.to(_user)})`,
|
|
230
232
|
)
|
|
231
|
-
return
|
|
233
|
+
return format.from(response.data)
|
|
232
234
|
},
|
|
233
|
-
updateUser: async (data) =>
|
|
234
|
-
(await q(Update(Ref(Users, data.id), { data: to(data) })))!,
|
|
235
235
|
async deleteUser(userId) {
|
|
236
|
-
await
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
236
|
+
await client.query(fql`
|
|
237
|
+
// Delete the user's sessions
|
|
238
|
+
Session.byUserId(${userId}).forEach(session => session.delete())
|
|
239
|
+
|
|
240
|
+
// Delete the user's accounts
|
|
241
|
+
Account.byUserId(${userId}).forEach(account => account.delete())
|
|
242
|
+
|
|
243
|
+
// Delete the user
|
|
244
|
+
User.byId(${userId}).delete()
|
|
245
|
+
`)
|
|
246
|
+
},
|
|
247
|
+
async linkAccount(account) {
|
|
248
|
+
await client.query<FaunaAccount>(
|
|
249
|
+
fql`Account.create(${format.to(account)})`,
|
|
248
250
|
)
|
|
251
|
+
return account
|
|
249
252
|
},
|
|
250
|
-
linkAccount: async (data) =>
|
|
251
|
-
(await q(Create(Accounts, { data: to(data) })))!,
|
|
252
253
|
async unlinkAccount({ provider, providerAccountId }) {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
Delete(
|
|
256
|
-
Select("ref", Get(Match(AccountByProviderAndProviderAccountId, id)))
|
|
257
|
-
)
|
|
254
|
+
const response = await client.query<FaunaAccount>(
|
|
255
|
+
fql`Account.byProviderAndProviderAccountId(${provider}, ${providerAccountId}).first().delete()`,
|
|
258
256
|
)
|
|
257
|
+
return format.from<AdapterAccount>(response.data)
|
|
259
258
|
},
|
|
260
|
-
createSession: async (data) =>
|
|
261
|
-
(await q<AdapterSession>(Create(Sessions, { data: to(data) })))!,
|
|
262
259
|
async getSessionAndUser(sessionToken) {
|
|
263
|
-
const
|
|
264
|
-
|
|
260
|
+
const response = await client.query<[FaunaUser, FaunaSession]>(fql`
|
|
261
|
+
let session = Session.bySessionToken(${sessionToken}).first()
|
|
262
|
+
if (session != null) {
|
|
263
|
+
let user = User.byId(session.userId)
|
|
264
|
+
if (user != null) {
|
|
265
|
+
[user, session]
|
|
266
|
+
} else {
|
|
267
|
+
null
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
null
|
|
271
|
+
}
|
|
272
|
+
`)
|
|
273
|
+
if (response.data === null) return null
|
|
274
|
+
const [user, session] = response.data ?? []
|
|
275
|
+
return { session: format.from(session), user: format.from(user) }
|
|
276
|
+
},
|
|
277
|
+
async createSession(session) {
|
|
278
|
+
await client.query<FaunaSession>(
|
|
279
|
+
fql`Session.create(${format.to(session)})`,
|
|
265
280
|
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const user = await q<AdapterUser>(Get(Ref(Users, session.userId)))
|
|
269
|
-
|
|
270
|
-
return { session, user: user! }
|
|
281
|
+
return session
|
|
271
282
|
},
|
|
272
|
-
async updateSession(
|
|
273
|
-
const
|
|
274
|
-
|
|
283
|
+
async updateSession(session) {
|
|
284
|
+
const response = await client.query<FaunaSession>(
|
|
285
|
+
fql`Session.bySessionToken(${
|
|
286
|
+
session.sessionToken
|
|
287
|
+
}).first().update(${format.to(session)})`,
|
|
288
|
+
)
|
|
289
|
+
return format.from(response.data)
|
|
275
290
|
},
|
|
276
291
|
async deleteSession(sessionToken) {
|
|
277
|
-
await
|
|
292
|
+
await client.query(
|
|
293
|
+
fql`Session.bySessionToken(${sessionToken}).first().delete()`,
|
|
294
|
+
)
|
|
278
295
|
},
|
|
279
|
-
async createVerificationToken(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
296
|
+
async createVerificationToken(verificationToken) {
|
|
297
|
+
await client.query<FaunaVerificationToken>(
|
|
298
|
+
fql`VerificationToken.create(${format.to(
|
|
299
|
+
verificationToken,
|
|
300
|
+
)})`,
|
|
283
301
|
)
|
|
284
302
|
return verificationToken
|
|
285
303
|
},
|
|
286
304
|
async useVerificationToken({ identifier, token }) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return verificationToken
|
|
305
|
+
const response = await client.query<FaunaVerificationToken>(
|
|
306
|
+
fql`VerificationToken.byIdentifierAndToken(${identifier}, ${token}).first()`,
|
|
307
|
+
)
|
|
308
|
+
if (response.data === null) return null
|
|
309
|
+
// Delete the verification token so it can only be used once
|
|
310
|
+
await client.query(
|
|
311
|
+
fql`VerificationToken.byId(${response.data.id}).delete()`,
|
|
312
|
+
)
|
|
313
|
+
const _verificationToken: Partial<FaunaVerificationToken> = { ...response.data }
|
|
314
|
+
delete _verificationToken.id
|
|
315
|
+
return format.from(_verificationToken)
|
|
299
316
|
},
|
|
300
317
|
}
|
|
301
318
|
}
|
|
319
|
+
|
|
320
|
+
export const format = {
|
|
321
|
+
/** Takes an object that's coming from the database and converts it to plain JavaScript. */
|
|
322
|
+
from<T>(object: Record<string, any> = {}): T {
|
|
323
|
+
if (!object) return null as unknown as T
|
|
324
|
+
const newObject: Record<string, unknown> = {}
|
|
325
|
+
for (const [key, value] of Object.entries(object))
|
|
326
|
+
if (key === "coll" || key === "ts") continue
|
|
327
|
+
else if (value instanceof TimeStub) newObject[key] = value.toDate()
|
|
328
|
+
else newObject[key] = value
|
|
329
|
+
return newObject as T
|
|
330
|
+
},
|
|
331
|
+
/** Takes an object that's coming from Auth.js and prepares it to be written to the database. */
|
|
332
|
+
to<T>(object: Record<string, any>): T {
|
|
333
|
+
const newObject: Record<string, unknown> = {}
|
|
334
|
+
for (const [key, value] of Object.entries(object))
|
|
335
|
+
if (value instanceof Date) newObject[key] = TimeStub.fromDate(value)
|
|
336
|
+
else if (typeof value === "string" && !isNaN(Date.parse(value)))
|
|
337
|
+
newObject[key] = TimeStub.from(value)
|
|
338
|
+
else newObject[key] = value ?? null
|
|
339
|
+
|
|
340
|
+
return newObject as T
|
|
341
|
+
},
|
|
342
|
+
}
|