Compare commits

...

2 Commits

Author SHA1 Message Date
Balázs Orbán
be6a85d7b5 import Account as type 2022-09-14 14:15:48 +02:00
Balázs Orbán
946717523b feat(adapters): add Hasura Adapter 2022-09-14 14:05:26 +02:00
8 changed files with 362 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
<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>&nbsp;&nbsp;&nbsp;&nbsp;<img height="64px" src="https://hasura.io/brand-assets/hasura-logo-primary-dark.svg" />
<h3 align="center"><b>Hasura Adapter</b> - NextAuth.js</h3>
<p align="center">
Open Source. Full Stack. Own Your Data.
</p>
</p>
TODO

View File

@@ -0,0 +1,44 @@
{
"name": "@next-auth/hasura-adapter",
"version": "1.0.0",
"description": "Hasura adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "Balázs Orbán <info@balazsorban.com>",
"contributors": [],
"main": "dist/index.js",
"files": [
"dist",
"index.d.ts"
],
"license": "ISC",
"keywords": [
"next-auth",
"next.js",
"hasura",
"graphql"
],
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "tsc --watch",
"build": "tsc",
"test": "./tests/test.sh"
},
"peerDependencies": {
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"next-auth": "workspace:*"
},
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}

View File

@@ -0,0 +1,49 @@
import type { Account } from "next-auth"
import type {
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
export interface Model<T> {
name: string
fields: Array<keyof T>
}
export interface Models {
User: Model<AdapterUser>
Account: Model<Account>
Session: Model<AdapterSession>
VerificationToken: Model<VerificationToken>
}
export const models: Models = {
User: {
name: "User",
fields: ["email", "id", "image", "name", "emailVerified"],
},
Account: {
name: "Account",
fields: [
"id",
"type",
"provider",
"providerAccountId",
"expires_at",
"token_type",
"scope",
"access_token",
"refresh_token",
"id_token",
"session_state",
],
},
Session: {
name: "Session",
fields: ["expires", "id", "sessionToken"],
},
VerificationToken: {
name: "VerificationToken",
fields: ["identifier", "token", "expires"],
},
}

View File

@@ -0,0 +1 @@
# TODO

View File

@@ -0,0 +1,209 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-base-to-string */
import { format } from "./utils"
import { Models, Model, models } from "./graphql/models"
import type { Adapter, AdapterUser } from "next-auth/adapters"
import type { Account } from "next-auth"
export type { Models, Model }
export interface HasuraClientOptions {
/** GraphQL endpoint */
url: string
/**
* `X-Hasura-Admin-Secret` header value
*
* [Securing the GraphQL endpoint](https://hasura.io/docs/latest/deployment/securing-graphql-endpoint/)
*/
adminSecret: string
}
export interface HasuraAdapterOptions {
client: HasuraClientOptions
models?: Partial<Models>
}
export function HasuraAdapter(options: HasuraAdapterOptions): Adapter {
const c = client(options.client)
const m: Models = {
User: { ...models.User, ...options.models?.User },
Account: { ...models.Account, ...options.models?.Account },
Session: { ...models.Session, ...options.models?.Session },
VerificationToken: {
...models.VerificationToken,
...options.models?.VerificationToken,
},
}
return {
async createUser(data) {
const result = await c.run<AdapterUser[]>(
/* GraphQL */ `
mutation ($data: [${m.User.name}_insert_input!]!) {
insert_${m.User.name}(objects: $data) {
returning {
${m.User.fields.join(" ")}
}
}
}
`,
{ data }
)
return format.from(result?.[0])
},
async getUser(id) {
const result = await c.run<AdapterUser>(
/* GraphQL */ `
query ($id: String!) {
${m.User.name}(where: { id: { _eq: $id } }) {
${m.User.fields.join(" ")}
}
}
`,
{ id }
)
return format.from(result)
},
async getUserByEmail(email) {
const result = await c.run<AdapterUser[]>(
/* GraphQL */ `
query ($email: String!) {
${m.User.name}(where: { email: { _eq: $email } }) {
${m.User.fields.join(" ")}
}
}
`,
{ email }
)
return result?.[0] ?? null
},
async getUserByAccount(provider_providerAccountId) {
const result = await c.run<Array<AdapterUser & { Accounts: Account }>>(
/* GraphQL */ `
query getUserByAccount($providerAccountId: String!, $provider: String!) {
${m.User.name}(
where: {Accounts: {providerAccountId: {_eq: $providerAccountId}, provider: {_eq: $provider}}}
) {
${m.User.fields.join(" ")}
}
}
`,
provider_providerAccountId
)
if (!result?.length) return null
const { Accounts: _, ...user } = result[0]
return user
},
async updateUser({ id, ...input }) {
throw new HasuraAdapterError("`updateUser` not implemented")
},
async deleteUser(id) {
throw new HasuraAdapterError("`deleteUser` not implemented")
},
async linkAccount(data) {
throw new HasuraAdapterError("`linkAccount` not implemented")
},
async unlinkAccount(provider_providerAccountId) {
throw new HasuraAdapterError("`unlinkAccount` not implemented")
},
async getSessionAndUser(sessionToken) {
throw new HasuraAdapterError("`getSessionAndUser` not implemented")
},
async createSession(data) {
throw new HasuraAdapterError("`createSession` not implemented")
},
async updateSession({ sessionToken, ...input }) {
throw new HasuraAdapterError("`updateSession` not implemented")
},
async deleteSession(sessionToken) {
throw new HasuraAdapterError("`deleteSession` not implemented")
},
async createVerificationToken(input) {
throw new HasuraAdapterError("`createVerificationToken` not implemented")
},
async useVerificationToken(params) {
throw new HasuraAdapterError("`useVerificationToken` not implemented")
},
}
}
export class HasuraAdapterError extends Error {
name = "HasuraAdapterError"
query?: string
variables?: any
constructor(
e: Error | string | Array<Error | string>,
query?: string,
variables?: any
) {
// @ts-expect-error
super(Array.isArray(e) ? e.map((e) => e.message ?? e).join("\n") : e.m ?? e)
this.query = query
this.variables = variables
}
toJSON() {
return {
name: this.name,
message: this.message,
query: this.query,
variables: this.variables,
}
}
toString() {
const items = [
this.message,
this.query,
JSON.stringify(this.variables, null, 2),
].filter(Boolean)
return `${this.name}: ${items.join("\n")}`
}
}
export function client(options: HasuraClientOptions) {
if (!globalThis.fetch) {
throw new HasuraAdapterError("Please provide a `fetch` implementation")
}
const { url, adminSecret } = options
if (!adminSecret) {
throw new HasuraAdapterError("Please provide an API key")
}
if (!url) {
throw new HasuraAdapterError("Please provide a GraphQL endpoint")
}
return {
async run<T>(
query: string,
variables?: Record<string, any>
): Promise<T | null> {
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Hasura-Admin-Secret": adminSecret,
},
body: JSON.stringify({ query, variables }),
})
const { data = {}, errors } = await response.json()
if (errors?.length) {
throw new HasuraAdapterError(errors, query, variables)
}
return Object.values(data)[0] as any
} catch (error) {
if (error instanceof HasuraAdapterError) throw error
throw new HasuraAdapterError(error as Error, query, variables)
}
},
}
}
export { format, models }

View File

@@ -0,0 +1,26 @@
// https://github.com/honeinc/is-iso-date/blob/master/index.js
const isoDateRE =
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
function isDate(value: any) {
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
}
export const format = {
from<T extends Record<string, any> | null>(
data?: T
): T extends null ? null : T {
if (!data) return null as any
const newObject: Record<string, unknown> = {}
for (const key in data) {
const value = data[key]
if (isDate(value)) {
newObject[key] = new Date(value)
} else {
newObject[key] = value
}
}
return newObject as any
},
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": ["tests", "src"],
"exclude": [
"./*.js",
"./*.d.ts",
]
}

View File

@@ -0,0 +1,15 @@
{
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": [
"."
],
"exclude": [
"tests",
"dist",
"jest.config.js"
]
}