├── src ├── graphql │ ├── resolvers │ │ ├── index.ts │ │ ├── UserResolver.ts │ │ └── AuthResolver.ts │ ├── schema.gql │ ├── index.ts │ └── builder.ts ├── @types │ └── sessions.d.ts ├── pages │ ├── index.tsx │ ├── _app.tsx │ ├── authenticated.tsx │ ├── __generated__ │ │ └── authenticated.generated.tsx │ └── api │ │ └── graphql.tsx ├── styles │ └── globals.css ├── components │ ├── Test.tsx │ └── __generated__ │ │ └── Test.generated.tsx ├── utils │ ├── apolloClient.ts │ ├── prisma.ts │ ├── unAuthenticatedRoute.ts │ ├── authenticatedRoute.ts │ ├── auth.ts │ └── sessions.ts └── __generated__ │ └── generated.d.ts ├── .prettierrc ├── next-env.d.ts ├── .babelrc ├── public ├── favicon.ico └── vercel.svg ├── graphql.config.js ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20210428094940_initial_migration │ │ └── migration.sql │ └── 20210429215940_new_user │ │ └── migration.sql └── schema.prisma ├── apollo.config.js ├── .gitignore ├── codegen.yml ├── .env ├── tsconfig.json ├── README.md ├── package.json └── graphql.schema.json /src/graphql/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import "./UserResolver"; 2 | import "./AuthResolver"; 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 80 5 | } 6 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | "superjson-next" // 👈 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kush-daga/Next-Prisma-GraphQL-Template/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /graphql.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | schema: "./src/graphql/schema.gql", 3 | documents: "src/**/*.{graphql,js,ts,jsx,tsx}", 4 | }; 5 | -------------------------------------------------------------------------------- /src/@types/sessions.d.ts: -------------------------------------------------------------------------------- 1 | declare module "http" { 2 | interface IncomingMessage { 3 | session: import("next-iron-session").Session; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | includes: ["./src/**/*.tsx"], 4 | service: { 5 | name: "testing-prisma", 6 | localSchemaFile: "./src/graphql/schema.gql", 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20210428094940_initial_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL, 4 | "firstName" TEXT NOT NULL, 5 | "lastName" TEXT NOT NULL, 6 | "email" TEXT NOT NULL, 7 | "photoUrl" TEXT NOT NULL, 8 | 9 | PRIMARY KEY ("id") 10 | ); 11 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 | Create Next App 8 | 9 | 10 |
Hello world
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/graphql/resolvers/UserResolver.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | import { builder } from "../builder"; 3 | 4 | export const UserObject = builder.objectRef("User").implement({ 5 | fields: t => ({ 6 | id: t.exposeID("id", {}), 7 | name: t.exposeString("name", {}), 8 | email: t.exposeString("email", {}) 9 | }) 10 | }); 11 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Test.tsx: -------------------------------------------------------------------------------- 1 | //Just for testing need to change to apollo-client later.. 2 | import { gql } from "apollo-server-core"; 3 | import { UserQuery } from "./__generated__/Test.generated"; 4 | 5 | export const Test = () => { 6 | const userQuery = gql` 7 | query User { 8 | me { 9 | id 10 | } 11 | } 12 | `; 13 | 14 | return
Hello
; 15 | }; 16 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from "next/dist/next-server/lib/router/router"; 2 | import "../styles/globals.css"; 3 | import { client } from "../utils/apolloClient"; 4 | import { ApolloProvider } from "@apollo/client/react"; 5 | 6 | function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /src/graphql/schema.gql: -------------------------------------------------------------------------------- 1 | scalar DateTime 2 | 3 | input LoginInput { 4 | email: String! 5 | password: String! 6 | } 7 | 8 | type Mutation { 9 | login(input: LoginInput!): User! 10 | logout: Boolean! 11 | signUp(input: SignUpInput!): User! 12 | } 13 | 14 | type Query { 15 | me: User 16 | } 17 | 18 | input SignUpInput { 19 | email: String! 20 | name: String! 21 | password: String! 22 | } 23 | 24 | type User { 25 | email: String! 26 | id: ID! 27 | name: String! 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/apolloClient.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache } from "@apollo/client"; 2 | 3 | const cache = new InMemoryCache(); 4 | 5 | export const client = new ApolloClient({ 6 | // Provide required constructor fields 7 | cache: cache, 8 | uri: "/api/graphql", 9 | ssrMode: true, 10 | // Provide some optional constructor fields 11 | name: "testing-prisma", 12 | version: "1.0", 13 | defaultOptions: { 14 | watchQuery: { 15 | fetchPolicy: "cache-and-network" 16 | } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /src/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import { printSchema, lexicographicSortSchema } from "graphql"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { builder } from "../../src/graphql/builder"; 5 | import "./resolvers"; 6 | 7 | const schema = builder.toSchema({}); 8 | const schemaAsString = printSchema(lexicographicSortSchema(schema)); 9 | 10 | if (process.env.NODE_ENV === "development") { 11 | fs.writeFileSync( 12 | path.join(process.cwd(), "src/graphql/schema.gql"), 13 | schemaAsString 14 | ); 15 | } 16 | 17 | export default schema; 18 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "src/graphql/schema.gql" 3 | documents: "./src/**/!(*.generated).{graphql,js,ts,jsx,tsx}" 4 | generates: 5 | src/__generated__/generated.d.ts: 6 | - "typescript" 7 | src/: 8 | preset: near-operation-file 9 | presetConfig: 10 | extension: .generated.tsx 11 | baseTypesPath: /__generated__/generated.d.ts 12 | folder: __generated__ 13 | plugins: 14 | - "typescript" 15 | - "typescript-operations" 16 | ./graphql.schema.json: 17 | plugins: 18 | - "introspection" 19 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL and SQLite. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL="postgresql://postgres:password@localhost:5432/tryprisma?schema=public" 8 | COOKIE_SECRET="sthgsibetterthannthgsothisisarandomasspasswordwithtoomanycharacters" -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": [ 18 | "next-env.d.ts", 19 | "**/*.ts", 20 | "**/*.tsx", 21 | "src/pages/api/graphql.js" 22 | ], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | interface CustomNodeJsGlobal extends NodeJS.Global { 3 | prisma: PrismaClient; 4 | } 5 | // PrismaClient is attached to the `global` object in development to prevent 6 | // exhausting your database connection limit. 7 | // 8 | // Learn more: 9 | // https://pris.ly/d/help/next-js-best-practices 10 | 11 | let prisma: PrismaClient; 12 | declare const global: CustomNodeJsGlobal; 13 | 14 | if (process.env.NODE_ENV === "production") { 15 | prisma = new PrismaClient(); 16 | } else { 17 | if (!global.prisma) { 18 | global.prisma = new PrismaClient(); 19 | } 20 | prisma = global.prisma; 21 | } 22 | export default prisma; 23 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = env("DATABASE_URL") 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | model User { 14 | id String @id @default(cuid()) 15 | name String 16 | email String @unique 17 | Session Session[] 18 | hashedPassword Bytes 19 | } 20 | 21 | model Session { 22 | id String @id @default(cuid()) 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @updatedAt 25 | user User @relation(fields: [userId], references: [id]) 26 | userId String 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/unAuthenticatedRoute.ts: -------------------------------------------------------------------------------- 1 | import { withIronSession } from "next-iron-session"; 2 | import { sessionOptions } from "./sessions"; 3 | import { GetServerSidePropsContext } from "next"; 4 | 5 | import prisma from "./prisma"; 6 | 7 | export const unAuthenticatedRoute = withIronSession( 8 | async ({ req, res }: GetServerSidePropsContext) => { 9 | const sessionID = req.session.get("sessionID"); 10 | 11 | const session = await prisma.session.findUnique({ 12 | where: { id: sessionID } 13 | }); 14 | 15 | if (session) { 16 | res.setHeader("location", "/"); 17 | res.statusCode = 302; 18 | res.end(); 19 | return { props: { session } }; 20 | } 21 | 22 | return { 23 | props: {} 24 | }; 25 | }, 26 | sessionOptions 27 | ); 28 | -------------------------------------------------------------------------------- /src/utils/authenticatedRoute.ts: -------------------------------------------------------------------------------- 1 | import { withIronSession } from "next-iron-session"; 2 | import { sessionOptions } from "./sessions"; 3 | import { GetServerSidePropsContext } from "next"; 4 | 5 | import prisma from "./prisma"; 6 | 7 | export const authenticatedRoute = withIronSession( 8 | async ({ req, res }: GetServerSidePropsContext) => { 9 | const sessionID = req.session.get("sessionID"); 10 | 11 | const session = await prisma.session.findUnique({ 12 | where: { id: sessionID } 13 | }); 14 | 15 | if (!session) { 16 | res.setHeader("location", "/login"); 17 | res.statusCode = 302; 18 | res.end(); 19 | return { props: {} }; 20 | } 21 | 22 | return { 23 | props: { session } 24 | }; 25 | }, 26 | sessionOptions 27 | ); 28 | -------------------------------------------------------------------------------- /src/pages/authenticated.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from "next"; 2 | import { authenticatedRoute } from "../utils/authenticatedRoute"; 3 | import { Session } from "@prisma/client"; 4 | import { gql, useQuery } from "@apollo/client"; 5 | import { MeQuery } from "./__generated__/authenticated.generated"; 6 | 7 | export const getServerSideProps: GetServerSideProps = authenticatedRoute; 8 | 9 | export default function Home({ session }: { session: Session }) { 10 | const query = gql` 11 | query Me { 12 | me { 13 | id 14 | email 15 | name 16 | } 17 | } 18 | `; 19 | 20 | const { loading, data, error } = useQuery(query); 21 | 22 | return ( 23 |
24 | {loading && ( 25 |
26 | Loadinggg
27 |
28 |
29 | )} 30 | 31 | {data && ( 32 |
33 | Hello {data?.me?.name}, You are logged in as {data?.me?.email} 34 |
35 | )} 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ive been working on nextjs for quite some time, and decided to make a template for a fullstack NextJs application, this template includes _Full GraphQL + TypeScript Support_ using _GiraphQL_ as the code first schema builder solution (https://giraphql.com) with _GraphQL CodeGen_ end to end setup for full type safety and uses Prisma as the ORM to talk to _PostgreSQL_ based db. It also has codegen plugins installed which will generate individual types for every file or component in your application (so queries written in the frontend will also have full type support). This is I believe quite scalable and I have set up session based auth using _next-iron-session_ which makes it super easy to manage sessions and also having a shared context between the requests. I hope this will be useful to you guys, and I will be updating it alot, so keep an eye for commits, will add more authentication and also add apollo-client in the frontend along with some demo pages styled with _TailwindCss_. 2 | Do star and fork if you find it helpful. 3 | https://github.com/kush-daga/Next-Prisma-GraphQL-Template 4 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/graphql/builder.ts: -------------------------------------------------------------------------------- 1 | import SchemaBuilder from "@giraphql/core"; 2 | import { User, Session } from "@prisma/client"; 3 | import { IncomingMessage, OutgoingMessage } from "http"; 4 | import ScopeAuthPlugin from "@giraphql/plugin-scope-auth"; 5 | import ValidationPlugin from "@giraphql/plugin-validation"; 6 | 7 | export const builder = new SchemaBuilder<{ 8 | DefaultInputFieldRequiredness: true; 9 | Context: { 10 | req: IncomingMessage; 11 | res: OutgoingMessage; 12 | user?: User | null; 13 | session?: Session | null; 14 | }; 15 | Scalars: { 16 | DateTime: { Input: Date; Output: Date }; 17 | }; 18 | AuthScopes: { 19 | user: boolean; 20 | isUnauthenticated: boolean; 21 | }; 22 | }>({ 23 | defaultInputFieldRequiredness: true, 24 | plugins: [ScopeAuthPlugin, ValidationPlugin], 25 | authScopes: async context => ({ 26 | user: !!context.user, 27 | isUnauthenticated: !context.user 28 | }) 29 | }); 30 | 31 | builder.queryType({}); 32 | builder.mutationType({}); 33 | 34 | builder.scalarType("DateTime", { 35 | serialize: date => date.toISOString(), 36 | parseValue: date => { 37 | return new Date(date); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /src/__generated__/generated.d.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | export type Exact = { [K in keyof T]: T[K] }; 3 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 4 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 5 | /** All built-in and custom scalars, mapped to their actual values */ 6 | export type Scalars = { 7 | ID: string; 8 | String: string; 9 | Boolean: boolean; 10 | Int: number; 11 | Float: number; 12 | DateTime: any; 13 | }; 14 | 15 | 16 | export type LoginInput = { 17 | email: Scalars['String']; 18 | password: Scalars['String']; 19 | }; 20 | 21 | export type Mutation = { 22 | __typename?: 'Mutation'; 23 | login: User; 24 | logout: Scalars['Boolean']; 25 | signUp: User; 26 | }; 27 | 28 | 29 | export type MutationLoginArgs = { 30 | input: LoginInput; 31 | }; 32 | 33 | 34 | export type MutationSignUpArgs = { 35 | input: SignUpInput; 36 | }; 37 | 38 | export type Query = { 39 | __typename?: 'Query'; 40 | me?: Maybe; 41 | }; 42 | 43 | export type SignUpInput = { 44 | email: Scalars['String']; 45 | name: Scalars['String']; 46 | password: Scalars['String']; 47 | }; 48 | 49 | export type User = { 50 | __typename?: 'User'; 51 | email: Scalars['String']; 52 | id: Scalars['ID']; 53 | name: Scalars['String']; 54 | }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing-prisma", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "generate": "graphql-codegen --config codegen.yml --watch" 10 | }, 11 | "dependencies": { 12 | "@apollo/client": "^3.3.16", 13 | "@giraphql/core": "^2.1.0", 14 | "@giraphql/plugin-scope-auth": "2.0.2", 15 | "@giraphql/plugin-validation": "1.2.0", 16 | "@prisma/client": "^2.21.2", 17 | "apollo-server-micro": "^2.23.0", 18 | "graphql": "^15.5.0", 19 | "graphql-helix": "^1.4.0", 20 | "next": "10.1.3", 21 | "next-iron-session": "^4.1.13", 22 | "react": "17.0.2", 23 | "react-dom": "17.0.2", 24 | "secure-password": "^4.0.0", 25 | "superjson": "^1.7.4", 26 | "zod": "^1.11.16" 27 | }, 28 | "devDependencies": { 29 | "@graphql-codegen/cli": "1.21.4", 30 | "@graphql-codegen/introspection": "1.18.2", 31 | "@graphql-codegen/near-operation-file-preset": "^1.18.0", 32 | "@graphql-codegen/typescript": "1.22.0", 33 | "@graphql-codegen/typescript-document-nodes": "1.17.11", 34 | "@graphql-codegen/typescript-graphql-files-modules": "1.18.1", 35 | "@graphql-codegen/typescript-operations": "1.17.16", 36 | "@types/node": "^15.0.1", 37 | "@types/react": "^17.0.4", 38 | "@types/secure-password": "^3.1.0", 39 | "babel-plugin-superjson-next": "^0.2.3", 40 | "prisma": "^2.21.2", 41 | "typescript": "^4.2.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /prisma/migrations/20210429215940_new_user/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `firstName` on the `User` table. All the data in the column will be lost. 5 | - You are about to drop the column `lastName` on the `User` table. All the data in the column will be lost. 6 | - You are about to drop the column `photoUrl` on the `User` table. All the data in the column will be lost. 7 | - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail. 8 | - Added the required column `name` to the `User` table without a default value. This is not possible if the table is not empty. 9 | - Added the required column `hashedPassword` to the `User` table without a default value. This is not possible if the table is not empty. 10 | 11 | */ 12 | -- AlterTable 13 | ALTER TABLE "User" DROP COLUMN "firstName", 14 | DROP COLUMN "lastName", 15 | DROP COLUMN "photoUrl", 16 | ADD COLUMN "name" TEXT NOT NULL, 17 | ADD COLUMN "hashedPassword" BYTEA NOT NULL; 18 | 19 | -- CreateTable 20 | CREATE TABLE "Session" ( 21 | "id" TEXT NOT NULL, 22 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | "updatedAt" TIMESTAMP(3) NOT NULL, 24 | "userId" TEXT NOT NULL, 25 | 26 | PRIMARY KEY ("id") 27 | ); 28 | 29 | -- CreateIndex 30 | CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); 31 | 32 | -- AddForeignKey 33 | ALTER TABLE "Session" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 34 | -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import prisma from "./prisma"; 2 | import SecurePassword from "secure-password"; 3 | 4 | const securePassword = new SecurePassword(); 5 | 6 | export const hashPassword = async (password: string) => { 7 | try { 8 | return await securePassword.hash(Buffer.from(password)); 9 | } catch (err) { 10 | throw new Error("Error in hash password" + err.message); 11 | } 12 | }; 13 | 14 | export const verifyPassword = async ( 15 | hashedPassword: Buffer, 16 | password: string 17 | ) => { 18 | try { 19 | return await securePassword.verify(Buffer.from(password), hashedPassword); 20 | } catch (error) { 21 | console.log("ERROR IN VERIFY PASS", error); 22 | return SecurePassword.INVALID; 23 | } 24 | }; 25 | 26 | export const authenticateUser = async (email: string, password: string) => { 27 | const user = await prisma.user.findUnique({ 28 | where: { 29 | email: email 30 | } 31 | }); 32 | 33 | if (!user || !user.hashedPassword) { 34 | throw new Error("Email not found"); 35 | } 36 | let isVerified; 37 | try { 38 | isVerified = await verifyPassword(user.hashedPassword, password); 39 | } catch (err) { 40 | console.error("ERROR" + err.message); 41 | } 42 | 43 | switch (isVerified) { 44 | case SecurePassword.VALID: 45 | break; 46 | case SecurePassword.VALID_NEEDS_REHASH: 47 | console.log("here"); 48 | const newHash = await hashPassword(password); 49 | await prisma.user.update({ 50 | where: { id: user.id }, 51 | data: { 52 | hashedPassword: newHash 53 | } 54 | }); 55 | break; 56 | default: 57 | throw new Error("Invalid Password"); 58 | } 59 | 60 | return user; 61 | }; 62 | -------------------------------------------------------------------------------- /src/utils/sessions.ts: -------------------------------------------------------------------------------- 1 | import { Session, User } from "@prisma/client"; 2 | import { applySession, SessionOptions } from "next-iron-session"; 3 | import { IncomingMessage } from "http"; 4 | import { GetServerSidePropsContext } from "next"; 5 | 6 | import prisma from "./prisma"; 7 | 8 | export const sessionOptions: SessionOptions = { 9 | password: [{ id: 1, password: process.env.COOKIE_SECRET as string }], 10 | cookieName: "template-repo", 11 | cookieOptions: { 12 | secure: process.env.NODE_ENV === "production", 13 | httpOnly: true, 14 | sameSite: "strict" 15 | } 16 | }; 17 | 18 | export const createSession = async (req: any, user: User) => { 19 | const session = await prisma.session.create({ 20 | data: { 21 | userId: user.id 22 | } 23 | }); 24 | 25 | await req.session.set("sessionID", session.id); 26 | await req.session.save(); 27 | return session; 28 | }; 29 | 30 | interface PrismaSession extends Session { 31 | user: User; 32 | } 33 | 34 | const sessionCache = new WeakMap(); 35 | 36 | export const resolveSession = async ({ 37 | req, 38 | res 39 | }: Pick) => { 40 | if (sessionCache.has(req)) { 41 | console.log("in cache"); 42 | return sessionCache.get(req); 43 | } 44 | 45 | await applySession(req, res, sessionOptions); 46 | 47 | let session: PrismaSession | null = null; 48 | 49 | const sessionID = req.session.get("sessionID"); 50 | 51 | if (sessionID) { 52 | session = await prisma.session.findUnique({ 53 | where: { id: sessionID }, 54 | include: { 55 | user: true 56 | } 57 | }); 58 | } 59 | 60 | sessionCache.set(req, session); 61 | 62 | return session; 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/__generated__/Test.generated.tsx: -------------------------------------------------------------------------------- 1 | import * as Types from '../../__generated__/generated.d'; 2 | 3 | export type Maybe = T | null; 4 | export type Exact = { [K in keyof T]: T[K] }; 5 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 6 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 7 | /** All built-in and custom scalars, mapped to their actual values */ 8 | export type Scalars = { 9 | ID: string; 10 | String: string; 11 | Boolean: boolean; 12 | Int: number; 13 | Float: number; 14 | DateTime: any; 15 | }; 16 | 17 | 18 | export type LoginInput = { 19 | email: Scalars['String']; 20 | password: Scalars['String']; 21 | }; 22 | 23 | export type Mutation = { 24 | __typename?: 'Mutation'; 25 | login: User; 26 | logout: Scalars['Boolean']; 27 | signUp: User; 28 | }; 29 | 30 | 31 | export type MutationLoginArgs = { 32 | input: LoginInput; 33 | }; 34 | 35 | 36 | export type MutationSignUpArgs = { 37 | input: SignUpInput; 38 | }; 39 | 40 | export type Query = { 41 | __typename?: 'Query'; 42 | me?: Maybe; 43 | }; 44 | 45 | export type SignUpInput = { 46 | email: Scalars['String']; 47 | name: Scalars['String']; 48 | password: Scalars['String']; 49 | }; 50 | 51 | export type User = { 52 | __typename?: 'User'; 53 | email: Scalars['String']; 54 | id: Scalars['ID']; 55 | name: Scalars['String']; 56 | }; 57 | 58 | export type UserQueryVariables = Types.Exact<{ [key: string]: never; }>; 59 | 60 | 61 | export type UserQuery = ( 62 | { __typename?: 'Query' } 63 | & { me?: Types.Maybe<( 64 | { __typename?: 'User' } 65 | & Pick 66 | )> } 67 | ); 68 | -------------------------------------------------------------------------------- /src/pages/__generated__/authenticated.generated.tsx: -------------------------------------------------------------------------------- 1 | import * as Types from '../../__generated__/generated.d'; 2 | 3 | export type Maybe = T | null; 4 | export type Exact = { [K in keyof T]: T[K] }; 5 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 6 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 7 | /** All built-in and custom scalars, mapped to their actual values */ 8 | export type Scalars = { 9 | ID: string; 10 | String: string; 11 | Boolean: boolean; 12 | Int: number; 13 | Float: number; 14 | DateTime: any; 15 | }; 16 | 17 | 18 | export type LoginInput = { 19 | email: Scalars['String']; 20 | password: Scalars['String']; 21 | }; 22 | 23 | export type Mutation = { 24 | __typename?: 'Mutation'; 25 | login: User; 26 | logout: Scalars['Boolean']; 27 | signUp: User; 28 | }; 29 | 30 | 31 | export type MutationLoginArgs = { 32 | input: LoginInput; 33 | }; 34 | 35 | 36 | export type MutationSignUpArgs = { 37 | input: SignUpInput; 38 | }; 39 | 40 | export type Query = { 41 | __typename?: 'Query'; 42 | me?: Maybe; 43 | }; 44 | 45 | export type SignUpInput = { 46 | email: Scalars['String']; 47 | name: Scalars['String']; 48 | password: Scalars['String']; 49 | }; 50 | 51 | export type User = { 52 | __typename?: 'User'; 53 | email: Scalars['String']; 54 | id: Scalars['ID']; 55 | name: Scalars['String']; 56 | }; 57 | 58 | export type MeQueryVariables = Types.Exact<{ [key: string]: never; }>; 59 | 60 | 61 | export type MeQuery = ( 62 | { __typename?: 'Query' } 63 | & { me?: Types.Maybe<( 64 | { __typename?: 'User' } 65 | & Pick 66 | )> } 67 | ); 68 | -------------------------------------------------------------------------------- /src/graphql/resolvers/AuthResolver.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../utils/prisma"; 2 | 3 | import { hashPassword, authenticateUser } from "../../utils/auth"; 4 | import { createSession } from "../../utils/sessions"; 5 | import { builder } from "../builder"; 6 | import { UserObject } from "./UserResolver"; 7 | 8 | builder.queryFields(t => ({ 9 | me: t.field({ 10 | type: UserObject, 11 | authScopes: { 12 | user: true 13 | }, 14 | nullable: true, 15 | resolve: (_root, _args, { user, session }) => { 16 | return user; 17 | } 18 | }) 19 | })); 20 | 21 | builder.mutationField("logout", t => 22 | t.boolean({ 23 | authScopes: { 24 | user: true 25 | }, 26 | resolve: async (_root, _args, { session }) => { 27 | await prisma.session.delete({ where: { id: session!.id } }); 28 | return true; 29 | } 30 | }) 31 | ); 32 | 33 | const SignUpInput = builder.inputType("SignUpInput", { 34 | fields: t => ({ 35 | name: t.string({ required: true }), 36 | email: t.string({ required: true }), 37 | password: t.string({ required: true }) 38 | }) 39 | }); 40 | 41 | builder.mutationField("signUp", t => 42 | t.field({ 43 | type: UserObject, 44 | args: { 45 | input: t.arg({ type: SignUpInput }) 46 | }, 47 | authScopes: { 48 | isUnauthenticated: true 49 | }, 50 | resolve: async (_root, { input }, { req }) => { 51 | const user = await prisma.user.create({ 52 | data: { 53 | email: input.email, 54 | name: input.name, 55 | hashedPassword: await hashPassword(input.password) 56 | } 57 | }); 58 | await createSession(req, user); 59 | return user; 60 | } 61 | }) 62 | ); 63 | 64 | const LoginInput = builder.inputType("LoginInput", { 65 | fields: t => ({ 66 | email: t.string({ required: true }), 67 | password: t.string({ required: true }) 68 | }) 69 | }); 70 | builder.mutationField("login", t => 71 | t.field({ 72 | type: UserObject, 73 | args: { 74 | input: t.arg({ type: LoginInput }) 75 | }, 76 | authScopes: { 77 | isUnauthenticated: true 78 | }, 79 | resolve: async (_root, { input }, { req }) => { 80 | console.log("CREATING USER"); 81 | try { 82 | const user = await authenticateUser(input.email, input.password); 83 | await createSession(req, user); 84 | return user; 85 | } catch (err) { 86 | throw new Error(err); 87 | } 88 | } 89 | }) 90 | ); 91 | -------------------------------------------------------------------------------- /src/pages/api/graphql.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getGraphQLParameters, 3 | processRequest, 4 | renderGraphiQL, 5 | shouldRenderGraphiQL 6 | } from "graphql-helix"; 7 | import { NextApiHandler } from "next/types"; 8 | import { IncomingHttpHeaders } from "node:http"; 9 | import schema from "../../graphql/index"; 10 | import { resolveSession } from "../../utils/sessions"; 11 | 12 | export default (async (req, res) => { 13 | const session = await resolveSession({ req, res }); 14 | 15 | interface GraphQLRequest { 16 | body?: any; 17 | headers: IncomingHttpHeaders; 18 | method: string; 19 | query: any; 20 | } 21 | const request: GraphQLRequest = { 22 | body: req.body, 23 | headers: req.headers, 24 | method: req.method ?? "GET", 25 | query: req.query 26 | }; 27 | 28 | if (shouldRenderGraphiQL(request)) { 29 | res.send(renderGraphiQL({ endpoint: "/api/graphql" })); 30 | } else { 31 | const { operationName, query, variables } = getGraphQLParameters(request); 32 | 33 | const result = await processRequest({ 34 | operationName, 35 | query, 36 | variables, 37 | request, 38 | schema, 39 | contextFactory: () => { 40 | return { 41 | req, 42 | res, 43 | user: session?.user, 44 | session: session 45 | }; 46 | } 47 | }); 48 | 49 | if (result.type === "RESPONSE") { 50 | result.headers.forEach(({ name, value }) => res.setHeader(name, value)); 51 | res.status(result.status); 52 | res.json(result.payload); 53 | } else if (result.type === "MULTIPART_RESPONSE") { 54 | res.writeHead(200, { 55 | Connection: "keep-alive", 56 | "Content-Type": 'multipart/mixed; boundary="-"', 57 | "Transfer-Encoding": "chunked" 58 | }); 59 | 60 | req.on("close", () => { 61 | result.unsubscribe(); 62 | }); 63 | 64 | res.write("---"); 65 | 66 | await result.subscribe(result => { 67 | const chunk = Buffer.from(JSON.stringify(result), "utf8"); 68 | const data = [ 69 | "", 70 | "Content-Type: application/json; charset=utf-8", 71 | "Content-Length: " + String(chunk.length), 72 | "", 73 | chunk 74 | ]; 75 | 76 | if (result.hasNext) { 77 | data.push("---"); 78 | } 79 | 80 | res.write(data.join("\r\n")); 81 | }); 82 | 83 | res.write("\r\n-----\r\n"); 84 | res.end(); 85 | } else { 86 | res.writeHead(200, { 87 | "Content-Type": "text/event-stream", 88 | Connection: "keep-alive", 89 | "Cache-Control": "no-cache" 90 | }); 91 | 92 | req.on("close", () => { 93 | result.unsubscribe(); 94 | }); 95 | 96 | await result.subscribe(result => { 97 | res.write(`data: ${JSON.stringify(result)}\n\n`); 98 | }); 99 | } 100 | } 101 | }) as NextApiHandler; 102 | -------------------------------------------------------------------------------- /graphql.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "__schema": { 3 | "queryType": { 4 | "name": "Query" 5 | }, 6 | "mutationType": { 7 | "name": "Mutation" 8 | }, 9 | "subscriptionType": null, 10 | "types": [ 11 | { 12 | "kind": "SCALAR", 13 | "name": "DateTime", 14 | "description": null, 15 | "fields": null, 16 | "inputFields": null, 17 | "interfaces": null, 18 | "enumValues": null, 19 | "possibleTypes": null 20 | }, 21 | { 22 | "kind": "INPUT_OBJECT", 23 | "name": "LoginInput", 24 | "description": null, 25 | "fields": null, 26 | "inputFields": [ 27 | { 28 | "name": "email", 29 | "description": null, 30 | "type": { 31 | "kind": "NON_NULL", 32 | "name": null, 33 | "ofType": { 34 | "kind": "SCALAR", 35 | "name": "String", 36 | "ofType": null 37 | } 38 | }, 39 | "defaultValue": null, 40 | "isDeprecated": false, 41 | "deprecationReason": null 42 | }, 43 | { 44 | "name": "password", 45 | "description": null, 46 | "type": { 47 | "kind": "NON_NULL", 48 | "name": null, 49 | "ofType": { 50 | "kind": "SCALAR", 51 | "name": "String", 52 | "ofType": null 53 | } 54 | }, 55 | "defaultValue": null, 56 | "isDeprecated": false, 57 | "deprecationReason": null 58 | } 59 | ], 60 | "interfaces": null, 61 | "enumValues": null, 62 | "possibleTypes": null 63 | }, 64 | { 65 | "kind": "SCALAR", 66 | "name": "String", 67 | "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", 68 | "fields": null, 69 | "inputFields": null, 70 | "interfaces": null, 71 | "enumValues": null, 72 | "possibleTypes": null 73 | }, 74 | { 75 | "kind": "OBJECT", 76 | "name": "Mutation", 77 | "description": null, 78 | "fields": [ 79 | { 80 | "name": "login", 81 | "description": null, 82 | "args": [ 83 | { 84 | "name": "input", 85 | "description": null, 86 | "type": { 87 | "kind": "NON_NULL", 88 | "name": null, 89 | "ofType": { 90 | "kind": "INPUT_OBJECT", 91 | "name": "LoginInput", 92 | "ofType": null 93 | } 94 | }, 95 | "defaultValue": null, 96 | "isDeprecated": false, 97 | "deprecationReason": null 98 | } 99 | ], 100 | "type": { 101 | "kind": "NON_NULL", 102 | "name": null, 103 | "ofType": { 104 | "kind": "OBJECT", 105 | "name": "User", 106 | "ofType": null 107 | } 108 | }, 109 | "isDeprecated": false, 110 | "deprecationReason": null 111 | }, 112 | { 113 | "name": "logout", 114 | "description": null, 115 | "args": [], 116 | "type": { 117 | "kind": "NON_NULL", 118 | "name": null, 119 | "ofType": { 120 | "kind": "SCALAR", 121 | "name": "Boolean", 122 | "ofType": null 123 | } 124 | }, 125 | "isDeprecated": false, 126 | "deprecationReason": null 127 | }, 128 | { 129 | "name": "signUp", 130 | "description": null, 131 | "args": [ 132 | { 133 | "name": "input", 134 | "description": null, 135 | "type": { 136 | "kind": "NON_NULL", 137 | "name": null, 138 | "ofType": { 139 | "kind": "INPUT_OBJECT", 140 | "name": "SignUpInput", 141 | "ofType": null 142 | } 143 | }, 144 | "defaultValue": null, 145 | "isDeprecated": false, 146 | "deprecationReason": null 147 | } 148 | ], 149 | "type": { 150 | "kind": "NON_NULL", 151 | "name": null, 152 | "ofType": { 153 | "kind": "OBJECT", 154 | "name": "User", 155 | "ofType": null 156 | } 157 | }, 158 | "isDeprecated": false, 159 | "deprecationReason": null 160 | } 161 | ], 162 | "inputFields": null, 163 | "interfaces": [], 164 | "enumValues": null, 165 | "possibleTypes": null 166 | }, 167 | { 168 | "kind": "SCALAR", 169 | "name": "Boolean", 170 | "description": "The `Boolean` scalar type represents `true` or `false`.", 171 | "fields": null, 172 | "inputFields": null, 173 | "interfaces": null, 174 | "enumValues": null, 175 | "possibleTypes": null 176 | }, 177 | { 178 | "kind": "OBJECT", 179 | "name": "Query", 180 | "description": null, 181 | "fields": [ 182 | { 183 | "name": "me", 184 | "description": null, 185 | "args": [], 186 | "type": { 187 | "kind": "OBJECT", 188 | "name": "User", 189 | "ofType": null 190 | }, 191 | "isDeprecated": false, 192 | "deprecationReason": null 193 | } 194 | ], 195 | "inputFields": null, 196 | "interfaces": [], 197 | "enumValues": null, 198 | "possibleTypes": null 199 | }, 200 | { 201 | "kind": "INPUT_OBJECT", 202 | "name": "SignUpInput", 203 | "description": null, 204 | "fields": null, 205 | "inputFields": [ 206 | { 207 | "name": "email", 208 | "description": null, 209 | "type": { 210 | "kind": "NON_NULL", 211 | "name": null, 212 | "ofType": { 213 | "kind": "SCALAR", 214 | "name": "String", 215 | "ofType": null 216 | } 217 | }, 218 | "defaultValue": null, 219 | "isDeprecated": false, 220 | "deprecationReason": null 221 | }, 222 | { 223 | "name": "name", 224 | "description": null, 225 | "type": { 226 | "kind": "NON_NULL", 227 | "name": null, 228 | "ofType": { 229 | "kind": "SCALAR", 230 | "name": "String", 231 | "ofType": null 232 | } 233 | }, 234 | "defaultValue": null, 235 | "isDeprecated": false, 236 | "deprecationReason": null 237 | }, 238 | { 239 | "name": "password", 240 | "description": null, 241 | "type": { 242 | "kind": "NON_NULL", 243 | "name": null, 244 | "ofType": { 245 | "kind": "SCALAR", 246 | "name": "String", 247 | "ofType": null 248 | } 249 | }, 250 | "defaultValue": null, 251 | "isDeprecated": false, 252 | "deprecationReason": null 253 | } 254 | ], 255 | "interfaces": null, 256 | "enumValues": null, 257 | "possibleTypes": null 258 | }, 259 | { 260 | "kind": "OBJECT", 261 | "name": "User", 262 | "description": null, 263 | "fields": [ 264 | { 265 | "name": "email", 266 | "description": null, 267 | "args": [], 268 | "type": { 269 | "kind": "NON_NULL", 270 | "name": null, 271 | "ofType": { 272 | "kind": "SCALAR", 273 | "name": "String", 274 | "ofType": null 275 | } 276 | }, 277 | "isDeprecated": false, 278 | "deprecationReason": null 279 | }, 280 | { 281 | "name": "id", 282 | "description": null, 283 | "args": [], 284 | "type": { 285 | "kind": "NON_NULL", 286 | "name": null, 287 | "ofType": { 288 | "kind": "SCALAR", 289 | "name": "ID", 290 | "ofType": null 291 | } 292 | }, 293 | "isDeprecated": false, 294 | "deprecationReason": null 295 | }, 296 | { 297 | "name": "name", 298 | "description": null, 299 | "args": [], 300 | "type": { 301 | "kind": "NON_NULL", 302 | "name": null, 303 | "ofType": { 304 | "kind": "SCALAR", 305 | "name": "String", 306 | "ofType": null 307 | } 308 | }, 309 | "isDeprecated": false, 310 | "deprecationReason": null 311 | } 312 | ], 313 | "inputFields": null, 314 | "interfaces": [], 315 | "enumValues": null, 316 | "possibleTypes": null 317 | }, 318 | { 319 | "kind": "SCALAR", 320 | "name": "ID", 321 | "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", 322 | "fields": null, 323 | "inputFields": null, 324 | "interfaces": null, 325 | "enumValues": null, 326 | "possibleTypes": null 327 | }, 328 | { 329 | "kind": "OBJECT", 330 | "name": "__Schema", 331 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", 332 | "fields": [ 333 | { 334 | "name": "description", 335 | "description": null, 336 | "args": [], 337 | "type": { 338 | "kind": "SCALAR", 339 | "name": "String", 340 | "ofType": null 341 | }, 342 | "isDeprecated": false, 343 | "deprecationReason": null 344 | }, 345 | { 346 | "name": "types", 347 | "description": "A list of all types supported by this server.", 348 | "args": [], 349 | "type": { 350 | "kind": "NON_NULL", 351 | "name": null, 352 | "ofType": { 353 | "kind": "LIST", 354 | "name": null, 355 | "ofType": { 356 | "kind": "NON_NULL", 357 | "name": null, 358 | "ofType": { 359 | "kind": "OBJECT", 360 | "name": "__Type", 361 | "ofType": null 362 | } 363 | } 364 | } 365 | }, 366 | "isDeprecated": false, 367 | "deprecationReason": null 368 | }, 369 | { 370 | "name": "queryType", 371 | "description": "The type that query operations will be rooted at.", 372 | "args": [], 373 | "type": { 374 | "kind": "NON_NULL", 375 | "name": null, 376 | "ofType": { 377 | "kind": "OBJECT", 378 | "name": "__Type", 379 | "ofType": null 380 | } 381 | }, 382 | "isDeprecated": false, 383 | "deprecationReason": null 384 | }, 385 | { 386 | "name": "mutationType", 387 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 388 | "args": [], 389 | "type": { 390 | "kind": "OBJECT", 391 | "name": "__Type", 392 | "ofType": null 393 | }, 394 | "isDeprecated": false, 395 | "deprecationReason": null 396 | }, 397 | { 398 | "name": "subscriptionType", 399 | "description": "If this server support subscription, the type that subscription operations will be rooted at.", 400 | "args": [], 401 | "type": { 402 | "kind": "OBJECT", 403 | "name": "__Type", 404 | "ofType": null 405 | }, 406 | "isDeprecated": false, 407 | "deprecationReason": null 408 | }, 409 | { 410 | "name": "directives", 411 | "description": "A list of all directives supported by this server.", 412 | "args": [], 413 | "type": { 414 | "kind": "NON_NULL", 415 | "name": null, 416 | "ofType": { 417 | "kind": "LIST", 418 | "name": null, 419 | "ofType": { 420 | "kind": "NON_NULL", 421 | "name": null, 422 | "ofType": { 423 | "kind": "OBJECT", 424 | "name": "__Directive", 425 | "ofType": null 426 | } 427 | } 428 | } 429 | }, 430 | "isDeprecated": false, 431 | "deprecationReason": null 432 | } 433 | ], 434 | "inputFields": null, 435 | "interfaces": [], 436 | "enumValues": null, 437 | "possibleTypes": null 438 | }, 439 | { 440 | "kind": "OBJECT", 441 | "name": "__Type", 442 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByUrl`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", 443 | "fields": [ 444 | { 445 | "name": "kind", 446 | "description": null, 447 | "args": [], 448 | "type": { 449 | "kind": "NON_NULL", 450 | "name": null, 451 | "ofType": { 452 | "kind": "ENUM", 453 | "name": "__TypeKind", 454 | "ofType": null 455 | } 456 | }, 457 | "isDeprecated": false, 458 | "deprecationReason": null 459 | }, 460 | { 461 | "name": "name", 462 | "description": null, 463 | "args": [], 464 | "type": { 465 | "kind": "SCALAR", 466 | "name": "String", 467 | "ofType": null 468 | }, 469 | "isDeprecated": false, 470 | "deprecationReason": null 471 | }, 472 | { 473 | "name": "description", 474 | "description": null, 475 | "args": [], 476 | "type": { 477 | "kind": "SCALAR", 478 | "name": "String", 479 | "ofType": null 480 | }, 481 | "isDeprecated": false, 482 | "deprecationReason": null 483 | }, 484 | { 485 | "name": "specifiedByUrl", 486 | "description": null, 487 | "args": [], 488 | "type": { 489 | "kind": "SCALAR", 490 | "name": "String", 491 | "ofType": null 492 | }, 493 | "isDeprecated": false, 494 | "deprecationReason": null 495 | }, 496 | { 497 | "name": "fields", 498 | "description": null, 499 | "args": [ 500 | { 501 | "name": "includeDeprecated", 502 | "description": null, 503 | "type": { 504 | "kind": "SCALAR", 505 | "name": "Boolean", 506 | "ofType": null 507 | }, 508 | "defaultValue": "false", 509 | "isDeprecated": false, 510 | "deprecationReason": null 511 | } 512 | ], 513 | "type": { 514 | "kind": "LIST", 515 | "name": null, 516 | "ofType": { 517 | "kind": "NON_NULL", 518 | "name": null, 519 | "ofType": { 520 | "kind": "OBJECT", 521 | "name": "__Field", 522 | "ofType": null 523 | } 524 | } 525 | }, 526 | "isDeprecated": false, 527 | "deprecationReason": null 528 | }, 529 | { 530 | "name": "interfaces", 531 | "description": null, 532 | "args": [], 533 | "type": { 534 | "kind": "LIST", 535 | "name": null, 536 | "ofType": { 537 | "kind": "NON_NULL", 538 | "name": null, 539 | "ofType": { 540 | "kind": "OBJECT", 541 | "name": "__Type", 542 | "ofType": null 543 | } 544 | } 545 | }, 546 | "isDeprecated": false, 547 | "deprecationReason": null 548 | }, 549 | { 550 | "name": "possibleTypes", 551 | "description": null, 552 | "args": [], 553 | "type": { 554 | "kind": "LIST", 555 | "name": null, 556 | "ofType": { 557 | "kind": "NON_NULL", 558 | "name": null, 559 | "ofType": { 560 | "kind": "OBJECT", 561 | "name": "__Type", 562 | "ofType": null 563 | } 564 | } 565 | }, 566 | "isDeprecated": false, 567 | "deprecationReason": null 568 | }, 569 | { 570 | "name": "enumValues", 571 | "description": null, 572 | "args": [ 573 | { 574 | "name": "includeDeprecated", 575 | "description": null, 576 | "type": { 577 | "kind": "SCALAR", 578 | "name": "Boolean", 579 | "ofType": null 580 | }, 581 | "defaultValue": "false", 582 | "isDeprecated": false, 583 | "deprecationReason": null 584 | } 585 | ], 586 | "type": { 587 | "kind": "LIST", 588 | "name": null, 589 | "ofType": { 590 | "kind": "NON_NULL", 591 | "name": null, 592 | "ofType": { 593 | "kind": "OBJECT", 594 | "name": "__EnumValue", 595 | "ofType": null 596 | } 597 | } 598 | }, 599 | "isDeprecated": false, 600 | "deprecationReason": null 601 | }, 602 | { 603 | "name": "inputFields", 604 | "description": null, 605 | "args": [ 606 | { 607 | "name": "includeDeprecated", 608 | "description": null, 609 | "type": { 610 | "kind": "SCALAR", 611 | "name": "Boolean", 612 | "ofType": null 613 | }, 614 | "defaultValue": "false", 615 | "isDeprecated": false, 616 | "deprecationReason": null 617 | } 618 | ], 619 | "type": { 620 | "kind": "LIST", 621 | "name": null, 622 | "ofType": { 623 | "kind": "NON_NULL", 624 | "name": null, 625 | "ofType": { 626 | "kind": "OBJECT", 627 | "name": "__InputValue", 628 | "ofType": null 629 | } 630 | } 631 | }, 632 | "isDeprecated": false, 633 | "deprecationReason": null 634 | }, 635 | { 636 | "name": "ofType", 637 | "description": null, 638 | "args": [], 639 | "type": { 640 | "kind": "OBJECT", 641 | "name": "__Type", 642 | "ofType": null 643 | }, 644 | "isDeprecated": false, 645 | "deprecationReason": null 646 | } 647 | ], 648 | "inputFields": null, 649 | "interfaces": [], 650 | "enumValues": null, 651 | "possibleTypes": null 652 | }, 653 | { 654 | "kind": "ENUM", 655 | "name": "__TypeKind", 656 | "description": "An enum describing what kind of type a given `__Type` is.", 657 | "fields": null, 658 | "inputFields": null, 659 | "interfaces": null, 660 | "enumValues": [ 661 | { 662 | "name": "SCALAR", 663 | "description": "Indicates this type is a scalar.", 664 | "isDeprecated": false, 665 | "deprecationReason": null 666 | }, 667 | { 668 | "name": "OBJECT", 669 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 670 | "isDeprecated": false, 671 | "deprecationReason": null 672 | }, 673 | { 674 | "name": "INTERFACE", 675 | "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", 676 | "isDeprecated": false, 677 | "deprecationReason": null 678 | }, 679 | { 680 | "name": "UNION", 681 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 682 | "isDeprecated": false, 683 | "deprecationReason": null 684 | }, 685 | { 686 | "name": "ENUM", 687 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 688 | "isDeprecated": false, 689 | "deprecationReason": null 690 | }, 691 | { 692 | "name": "INPUT_OBJECT", 693 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 694 | "isDeprecated": false, 695 | "deprecationReason": null 696 | }, 697 | { 698 | "name": "LIST", 699 | "description": "Indicates this type is a list. `ofType` is a valid field.", 700 | "isDeprecated": false, 701 | "deprecationReason": null 702 | }, 703 | { 704 | "name": "NON_NULL", 705 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 706 | "isDeprecated": false, 707 | "deprecationReason": null 708 | } 709 | ], 710 | "possibleTypes": null 711 | }, 712 | { 713 | "kind": "OBJECT", 714 | "name": "__Field", 715 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", 716 | "fields": [ 717 | { 718 | "name": "name", 719 | "description": null, 720 | "args": [], 721 | "type": { 722 | "kind": "NON_NULL", 723 | "name": null, 724 | "ofType": { 725 | "kind": "SCALAR", 726 | "name": "String", 727 | "ofType": null 728 | } 729 | }, 730 | "isDeprecated": false, 731 | "deprecationReason": null 732 | }, 733 | { 734 | "name": "description", 735 | "description": null, 736 | "args": [], 737 | "type": { 738 | "kind": "SCALAR", 739 | "name": "String", 740 | "ofType": null 741 | }, 742 | "isDeprecated": false, 743 | "deprecationReason": null 744 | }, 745 | { 746 | "name": "args", 747 | "description": null, 748 | "args": [ 749 | { 750 | "name": "includeDeprecated", 751 | "description": null, 752 | "type": { 753 | "kind": "SCALAR", 754 | "name": "Boolean", 755 | "ofType": null 756 | }, 757 | "defaultValue": "false", 758 | "isDeprecated": false, 759 | "deprecationReason": null 760 | } 761 | ], 762 | "type": { 763 | "kind": "NON_NULL", 764 | "name": null, 765 | "ofType": { 766 | "kind": "LIST", 767 | "name": null, 768 | "ofType": { 769 | "kind": "NON_NULL", 770 | "name": null, 771 | "ofType": { 772 | "kind": "OBJECT", 773 | "name": "__InputValue", 774 | "ofType": null 775 | } 776 | } 777 | } 778 | }, 779 | "isDeprecated": false, 780 | "deprecationReason": null 781 | }, 782 | { 783 | "name": "type", 784 | "description": null, 785 | "args": [], 786 | "type": { 787 | "kind": "NON_NULL", 788 | "name": null, 789 | "ofType": { 790 | "kind": "OBJECT", 791 | "name": "__Type", 792 | "ofType": null 793 | } 794 | }, 795 | "isDeprecated": false, 796 | "deprecationReason": null 797 | }, 798 | { 799 | "name": "isDeprecated", 800 | "description": null, 801 | "args": [], 802 | "type": { 803 | "kind": "NON_NULL", 804 | "name": null, 805 | "ofType": { 806 | "kind": "SCALAR", 807 | "name": "Boolean", 808 | "ofType": null 809 | } 810 | }, 811 | "isDeprecated": false, 812 | "deprecationReason": null 813 | }, 814 | { 815 | "name": "deprecationReason", 816 | "description": null, 817 | "args": [], 818 | "type": { 819 | "kind": "SCALAR", 820 | "name": "String", 821 | "ofType": null 822 | }, 823 | "isDeprecated": false, 824 | "deprecationReason": null 825 | } 826 | ], 827 | "inputFields": null, 828 | "interfaces": [], 829 | "enumValues": null, 830 | "possibleTypes": null 831 | }, 832 | { 833 | "kind": "OBJECT", 834 | "name": "__InputValue", 835 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", 836 | "fields": [ 837 | { 838 | "name": "name", 839 | "description": null, 840 | "args": [], 841 | "type": { 842 | "kind": "NON_NULL", 843 | "name": null, 844 | "ofType": { 845 | "kind": "SCALAR", 846 | "name": "String", 847 | "ofType": null 848 | } 849 | }, 850 | "isDeprecated": false, 851 | "deprecationReason": null 852 | }, 853 | { 854 | "name": "description", 855 | "description": null, 856 | "args": [], 857 | "type": { 858 | "kind": "SCALAR", 859 | "name": "String", 860 | "ofType": null 861 | }, 862 | "isDeprecated": false, 863 | "deprecationReason": null 864 | }, 865 | { 866 | "name": "type", 867 | "description": null, 868 | "args": [], 869 | "type": { 870 | "kind": "NON_NULL", 871 | "name": null, 872 | "ofType": { 873 | "kind": "OBJECT", 874 | "name": "__Type", 875 | "ofType": null 876 | } 877 | }, 878 | "isDeprecated": false, 879 | "deprecationReason": null 880 | }, 881 | { 882 | "name": "defaultValue", 883 | "description": "A GraphQL-formatted string representing the default value for this input value.", 884 | "args": [], 885 | "type": { 886 | "kind": "SCALAR", 887 | "name": "String", 888 | "ofType": null 889 | }, 890 | "isDeprecated": false, 891 | "deprecationReason": null 892 | }, 893 | { 894 | "name": "isDeprecated", 895 | "description": null, 896 | "args": [], 897 | "type": { 898 | "kind": "NON_NULL", 899 | "name": null, 900 | "ofType": { 901 | "kind": "SCALAR", 902 | "name": "Boolean", 903 | "ofType": null 904 | } 905 | }, 906 | "isDeprecated": false, 907 | "deprecationReason": null 908 | }, 909 | { 910 | "name": "deprecationReason", 911 | "description": null, 912 | "args": [], 913 | "type": { 914 | "kind": "SCALAR", 915 | "name": "String", 916 | "ofType": null 917 | }, 918 | "isDeprecated": false, 919 | "deprecationReason": null 920 | } 921 | ], 922 | "inputFields": null, 923 | "interfaces": [], 924 | "enumValues": null, 925 | "possibleTypes": null 926 | }, 927 | { 928 | "kind": "OBJECT", 929 | "name": "__EnumValue", 930 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", 931 | "fields": [ 932 | { 933 | "name": "name", 934 | "description": null, 935 | "args": [], 936 | "type": { 937 | "kind": "NON_NULL", 938 | "name": null, 939 | "ofType": { 940 | "kind": "SCALAR", 941 | "name": "String", 942 | "ofType": null 943 | } 944 | }, 945 | "isDeprecated": false, 946 | "deprecationReason": null 947 | }, 948 | { 949 | "name": "description", 950 | "description": null, 951 | "args": [], 952 | "type": { 953 | "kind": "SCALAR", 954 | "name": "String", 955 | "ofType": null 956 | }, 957 | "isDeprecated": false, 958 | "deprecationReason": null 959 | }, 960 | { 961 | "name": "isDeprecated", 962 | "description": null, 963 | "args": [], 964 | "type": { 965 | "kind": "NON_NULL", 966 | "name": null, 967 | "ofType": { 968 | "kind": "SCALAR", 969 | "name": "Boolean", 970 | "ofType": null 971 | } 972 | }, 973 | "isDeprecated": false, 974 | "deprecationReason": null 975 | }, 976 | { 977 | "name": "deprecationReason", 978 | "description": null, 979 | "args": [], 980 | "type": { 981 | "kind": "SCALAR", 982 | "name": "String", 983 | "ofType": null 984 | }, 985 | "isDeprecated": false, 986 | "deprecationReason": null 987 | } 988 | ], 989 | "inputFields": null, 990 | "interfaces": [], 991 | "enumValues": null, 992 | "possibleTypes": null 993 | }, 994 | { 995 | "kind": "OBJECT", 996 | "name": "__Directive", 997 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", 998 | "fields": [ 999 | { 1000 | "name": "name", 1001 | "description": null, 1002 | "args": [], 1003 | "type": { 1004 | "kind": "NON_NULL", 1005 | "name": null, 1006 | "ofType": { 1007 | "kind": "SCALAR", 1008 | "name": "String", 1009 | "ofType": null 1010 | } 1011 | }, 1012 | "isDeprecated": false, 1013 | "deprecationReason": null 1014 | }, 1015 | { 1016 | "name": "description", 1017 | "description": null, 1018 | "args": [], 1019 | "type": { 1020 | "kind": "SCALAR", 1021 | "name": "String", 1022 | "ofType": null 1023 | }, 1024 | "isDeprecated": false, 1025 | "deprecationReason": null 1026 | }, 1027 | { 1028 | "name": "isRepeatable", 1029 | "description": null, 1030 | "args": [], 1031 | "type": { 1032 | "kind": "NON_NULL", 1033 | "name": null, 1034 | "ofType": { 1035 | "kind": "SCALAR", 1036 | "name": "Boolean", 1037 | "ofType": null 1038 | } 1039 | }, 1040 | "isDeprecated": false, 1041 | "deprecationReason": null 1042 | }, 1043 | { 1044 | "name": "locations", 1045 | "description": null, 1046 | "args": [], 1047 | "type": { 1048 | "kind": "NON_NULL", 1049 | "name": null, 1050 | "ofType": { 1051 | "kind": "LIST", 1052 | "name": null, 1053 | "ofType": { 1054 | "kind": "NON_NULL", 1055 | "name": null, 1056 | "ofType": { 1057 | "kind": "ENUM", 1058 | "name": "__DirectiveLocation", 1059 | "ofType": null 1060 | } 1061 | } 1062 | } 1063 | }, 1064 | "isDeprecated": false, 1065 | "deprecationReason": null 1066 | }, 1067 | { 1068 | "name": "args", 1069 | "description": null, 1070 | "args": [], 1071 | "type": { 1072 | "kind": "NON_NULL", 1073 | "name": null, 1074 | "ofType": { 1075 | "kind": "LIST", 1076 | "name": null, 1077 | "ofType": { 1078 | "kind": "NON_NULL", 1079 | "name": null, 1080 | "ofType": { 1081 | "kind": "OBJECT", 1082 | "name": "__InputValue", 1083 | "ofType": null 1084 | } 1085 | } 1086 | } 1087 | }, 1088 | "isDeprecated": false, 1089 | "deprecationReason": null 1090 | } 1091 | ], 1092 | "inputFields": null, 1093 | "interfaces": [], 1094 | "enumValues": null, 1095 | "possibleTypes": null 1096 | }, 1097 | { 1098 | "kind": "ENUM", 1099 | "name": "__DirectiveLocation", 1100 | "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", 1101 | "fields": null, 1102 | "inputFields": null, 1103 | "interfaces": null, 1104 | "enumValues": [ 1105 | { 1106 | "name": "QUERY", 1107 | "description": "Location adjacent to a query operation.", 1108 | "isDeprecated": false, 1109 | "deprecationReason": null 1110 | }, 1111 | { 1112 | "name": "MUTATION", 1113 | "description": "Location adjacent to a mutation operation.", 1114 | "isDeprecated": false, 1115 | "deprecationReason": null 1116 | }, 1117 | { 1118 | "name": "SUBSCRIPTION", 1119 | "description": "Location adjacent to a subscription operation.", 1120 | "isDeprecated": false, 1121 | "deprecationReason": null 1122 | }, 1123 | { 1124 | "name": "FIELD", 1125 | "description": "Location adjacent to a field.", 1126 | "isDeprecated": false, 1127 | "deprecationReason": null 1128 | }, 1129 | { 1130 | "name": "FRAGMENT_DEFINITION", 1131 | "description": "Location adjacent to a fragment definition.", 1132 | "isDeprecated": false, 1133 | "deprecationReason": null 1134 | }, 1135 | { 1136 | "name": "FRAGMENT_SPREAD", 1137 | "description": "Location adjacent to a fragment spread.", 1138 | "isDeprecated": false, 1139 | "deprecationReason": null 1140 | }, 1141 | { 1142 | "name": "INLINE_FRAGMENT", 1143 | "description": "Location adjacent to an inline fragment.", 1144 | "isDeprecated": false, 1145 | "deprecationReason": null 1146 | }, 1147 | { 1148 | "name": "VARIABLE_DEFINITION", 1149 | "description": "Location adjacent to a variable definition.", 1150 | "isDeprecated": false, 1151 | "deprecationReason": null 1152 | }, 1153 | { 1154 | "name": "SCHEMA", 1155 | "description": "Location adjacent to a schema definition.", 1156 | "isDeprecated": false, 1157 | "deprecationReason": null 1158 | }, 1159 | { 1160 | "name": "SCALAR", 1161 | "description": "Location adjacent to a scalar definition.", 1162 | "isDeprecated": false, 1163 | "deprecationReason": null 1164 | }, 1165 | { 1166 | "name": "OBJECT", 1167 | "description": "Location adjacent to an object type definition.", 1168 | "isDeprecated": false, 1169 | "deprecationReason": null 1170 | }, 1171 | { 1172 | "name": "FIELD_DEFINITION", 1173 | "description": "Location adjacent to a field definition.", 1174 | "isDeprecated": false, 1175 | "deprecationReason": null 1176 | }, 1177 | { 1178 | "name": "ARGUMENT_DEFINITION", 1179 | "description": "Location adjacent to an argument definition.", 1180 | "isDeprecated": false, 1181 | "deprecationReason": null 1182 | }, 1183 | { 1184 | "name": "INTERFACE", 1185 | "description": "Location adjacent to an interface definition.", 1186 | "isDeprecated": false, 1187 | "deprecationReason": null 1188 | }, 1189 | { 1190 | "name": "UNION", 1191 | "description": "Location adjacent to a union definition.", 1192 | "isDeprecated": false, 1193 | "deprecationReason": null 1194 | }, 1195 | { 1196 | "name": "ENUM", 1197 | "description": "Location adjacent to an enum definition.", 1198 | "isDeprecated": false, 1199 | "deprecationReason": null 1200 | }, 1201 | { 1202 | "name": "ENUM_VALUE", 1203 | "description": "Location adjacent to an enum value definition.", 1204 | "isDeprecated": false, 1205 | "deprecationReason": null 1206 | }, 1207 | { 1208 | "name": "INPUT_OBJECT", 1209 | "description": "Location adjacent to an input object type definition.", 1210 | "isDeprecated": false, 1211 | "deprecationReason": null 1212 | }, 1213 | { 1214 | "name": "INPUT_FIELD_DEFINITION", 1215 | "description": "Location adjacent to an input object field definition.", 1216 | "isDeprecated": false, 1217 | "deprecationReason": null 1218 | } 1219 | ], 1220 | "possibleTypes": null 1221 | } 1222 | ], 1223 | "directives": [ 1224 | { 1225 | "name": "include", 1226 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 1227 | "isRepeatable": false, 1228 | "locations": [ 1229 | "FIELD", 1230 | "FRAGMENT_SPREAD", 1231 | "INLINE_FRAGMENT" 1232 | ], 1233 | "args": [ 1234 | { 1235 | "name": "if", 1236 | "description": "Included when true.", 1237 | "type": { 1238 | "kind": "NON_NULL", 1239 | "name": null, 1240 | "ofType": { 1241 | "kind": "SCALAR", 1242 | "name": "Boolean", 1243 | "ofType": null 1244 | } 1245 | }, 1246 | "defaultValue": null, 1247 | "isDeprecated": false, 1248 | "deprecationReason": null 1249 | } 1250 | ] 1251 | }, 1252 | { 1253 | "name": "skip", 1254 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 1255 | "isRepeatable": false, 1256 | "locations": [ 1257 | "FIELD", 1258 | "FRAGMENT_SPREAD", 1259 | "INLINE_FRAGMENT" 1260 | ], 1261 | "args": [ 1262 | { 1263 | "name": "if", 1264 | "description": "Skipped when true.", 1265 | "type": { 1266 | "kind": "NON_NULL", 1267 | "name": null, 1268 | "ofType": { 1269 | "kind": "SCALAR", 1270 | "name": "Boolean", 1271 | "ofType": null 1272 | } 1273 | }, 1274 | "defaultValue": null, 1275 | "isDeprecated": false, 1276 | "deprecationReason": null 1277 | } 1278 | ] 1279 | }, 1280 | { 1281 | "name": "deprecated", 1282 | "description": "Marks an element of a GraphQL schema as no longer supported.", 1283 | "isRepeatable": false, 1284 | "locations": [ 1285 | "FIELD_DEFINITION", 1286 | "ARGUMENT_DEFINITION", 1287 | "INPUT_FIELD_DEFINITION", 1288 | "ENUM_VALUE" 1289 | ], 1290 | "args": [ 1291 | { 1292 | "name": "reason", 1293 | "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", 1294 | "type": { 1295 | "kind": "SCALAR", 1296 | "name": "String", 1297 | "ofType": null 1298 | }, 1299 | "defaultValue": "\"No longer supported\"", 1300 | "isDeprecated": false, 1301 | "deprecationReason": null 1302 | } 1303 | ] 1304 | }, 1305 | { 1306 | "name": "specifiedBy", 1307 | "description": "Exposes a URL that specifies the behaviour of this scalar.", 1308 | "isRepeatable": false, 1309 | "locations": [ 1310 | "SCALAR" 1311 | ], 1312 | "args": [ 1313 | { 1314 | "name": "url", 1315 | "description": "The URL that specifies the behaviour of this scalar.", 1316 | "type": { 1317 | "kind": "NON_NULL", 1318 | "name": null, 1319 | "ofType": { 1320 | "kind": "SCALAR", 1321 | "name": "String", 1322 | "ofType": null 1323 | } 1324 | }, 1325 | "defaultValue": null, 1326 | "isDeprecated": false, 1327 | "deprecationReason": null 1328 | } 1329 | ] 1330 | } 1331 | ] 1332 | } 1333 | } --------------------------------------------------------------------------------