├── .eslintrc.json ├── .githooks └── pre-commit ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── Dockerfile ├── README.md ├── codegen.yml ├── graphql └── schema.graphql ├── next-env.d.ts ├── package-lock.json ├── package.json ├── src ├── components │ └── seo │ │ └── DefaultSeo.tsx ├── context │ └── modal.tsx ├── env │ ├── .env.dev │ ├── .env.local │ ├── .env.prod │ └── index.ts ├── graphqlClient │ ├── apollo.ts │ ├── mutation │ │ └── saveUser.graphql │ └── query │ │ └── user.graphql ├── graphqlServer │ ├── errors │ │ └── condition.ts │ ├── index.ts │ ├── plugins │ │ └── logging.ts │ └── resolvers │ │ ├── context.ts │ │ ├── datasources │ │ └── index.ts │ │ ├── directives │ │ ├── authorization.ts │ │ └── index.ts │ │ ├── entity │ │ └── user.ts │ │ ├── index.ts │ │ ├── loaders │ │ └── user.ts │ │ ├── mutation │ │ └── index.ts │ │ ├── query │ │ └── index.ts │ │ └── scalars │ │ └── dateTime.ts ├── helper │ └── array.ts ├── hooks │ └── useCustomToast.ts ├── lib │ └── logger.ts ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── _error.tsx │ ├── api │ │ └── graphql.tsx │ └── index.tsx └── types │ ├── generated │ ├── clientGraphql.tsx │ └── serverGraphql.d.ts │ └── server │ └── context.d.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:@typescript-eslint/eslint-recommended", 7 | "plugin:prettier/recommended" 8 | ], 9 | "plugins": ["@typescript-eslint", "react"], 10 | "parser": "@typescript-eslint/parser", 11 | "env": { 12 | "browser": true, 13 | "node": true, 14 | "es6": true 15 | }, 16 | "parserOptions": { 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true 20 | } 21 | }, 22 | "rules": { 23 | "react/display-name": "off", 24 | "react/prop-types": "off", 25 | "react/react-in-jsx-scope": "off", 26 | "@typescript-eslint/no-explicit-any": "off", 27 | "@typescript-eslint/explicit-module-boundary-types": "off", 28 | "@typescript-eslint/no-non-null-assertion": "off", 29 | "@typescript-eslint/no-var-requires": "off", 30 | "no-case-declarations": "off" 31 | }, 32 | "ignorePatterns": ["src/types/generated/**/*"] 33 | } 34 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npm run pre-commit 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # next.js build output 5 | .next 6 | 7 | # production server output 8 | dist 9 | 10 | # Firebase 11 | .firebase 12 | *.log 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "semi": false, 6 | "htmlWhitespaceSensitivity": "ignore", 7 | "printWidth": 140, 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "eslint.workingDirectories": ["./"] 4 | 5 | } 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | WORKDIR /usr/src/app 3 | 4 | ENV TZ Asia/Tokyo 5 | ENV NODE_ENV production 6 | 7 | COPY package*.json ./ 8 | RUN npm set-script prepare "" 9 | RUN npm install 10 | 11 | COPY . . 12 | 13 | EXPOSE 80 14 | 15 | CMD ["npm", "run", "start"] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js x Apollo Server/Client x Chakra UI 2 | 3 | ## セットアップ 4 | ``` 5 | $ npm install 6 | ``` 7 | 8 | ## ローカルで動かす 9 | ``` 10 | $ npm run dev 11 | ``` 12 | 13 | ## 環境変数 14 | - `env/.env.[name]` にパブリックなものは置く 15 | - secret的なもの(=サーバーサイドでしか使ってはいけないもの)は実行環境に埋め込む 16 | - localだけ例外的に.env.localで管理 17 | - 好みに応じて.gitignoreに追加する 18 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "./graphql/schema.graphql" 3 | documents: src/graphqlClient/**/*.graphql 4 | generates: 5 | src/types/generated/serverGraphql.d.ts: 6 | plugins: 7 | - "typescript" 8 | - "typescript-resolvers" 9 | config: 10 | useIndexSignature: true 11 | enumsAsTypes: true 12 | contextType: ../server/context#CustomContext 13 | scalars: 14 | DateTime: Date 15 | src/types/generated/clientGraphql.tsx: 16 | plugins: 17 | - "typescript" 18 | - "typescript-operations" 19 | - "typescript-react-apollo" 20 | config: 21 | enumsAsTypes: true 22 | withHooks: true 23 | withComponent: false 24 | withHOC: false 25 | maybeValue: T | undefined 26 | -------------------------------------------------------------------------------- /graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | # Scalars 2 | 3 | scalar DateTime 4 | 5 | 6 | ## directives 7 | 8 | 9 | directive @authorization( 10 | requires: UserRole = member 11 | ) on OBJECT | FIELD_DEFINITION | QUERY | MUTATION 12 | 13 | enum UserRole { 14 | member # 例えば、ログインしているユーザー 15 | } 16 | 17 | 18 | type User { 19 | id: ID! 20 | name: String 21 | createdAt: DateTime! 22 | } 23 | 24 | 25 | ## Queries 26 | 27 | 28 | type Query { 29 | user(id: ID!): User 30 | } 31 | 32 | 33 | ## mutations 34 | 35 | 36 | type Mutation { 37 | saveUser(name: String): User! @authorization(requires: member) 38 | } 39 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-apollo", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "prepare": "git config --local core.hooksPath .githooks", 6 | "pre-commit": "lint-staged", 7 | "clean": "rm -rf .next && rm -rf dist", 8 | "dev": "env-cmd -f src/env/.env.local next -p 3000", 9 | "build": "npm run clean && next build && npm run prisma:codegen && npm run copy-static", 10 | "build:withEnv": "env-cmd -f src/env/.env.$APP_ENV npm run build", 11 | "gql:codegen": "graphql-codegen --config codegen.yml", 12 | "start": "next start -p 80", 13 | "start:localDocker": "docker build -t nextjs-apollo-sample . && docker run -p 80:80 nextjs-apollo-sample", 14 | "copy-static": "copyfiles -u 1 \"public/**/*\" \".next/static\"", 15 | "lint": "eslint --ext .ts,.tsx .", 16 | "lint:fix": "eslint --ext .ts,.tsx . --fix" 17 | }, 18 | "dependencies": { 19 | "@apollo/client": "^3.6.2", 20 | "@chakra-ui/react": "^1.8.8", 21 | "@emotion/react": "^11.9.0", 22 | "@emotion/styled": "^11.8.1", 23 | "@graphql-tools/schema": "^8.2.0", 24 | "@graphql-tools/utils": "^8.2.2", 25 | "apollo-datasource": "^3.3.1", 26 | "apollo-server-micro": "^3.7.0", 27 | "dataloader": "^2.1.0", 28 | "framer-motion": "^6.3.3", 29 | "graphql": "^16.4.0", 30 | "micro": "^9.3.4", 31 | "next": "^12.1.6", 32 | "next-seo": "^5.4.0", 33 | "react": "^18.1.0", 34 | "react-dom": "^18.1.0", 35 | "react-hook-form": "^7.30.0", 36 | "uuid": "^8.3.2" 37 | }, 38 | "devDependencies": { 39 | "@graphql-codegen/cli": "2.6.2", 40 | "@graphql-codegen/introspection": "2.1.1", 41 | "@graphql-codegen/typescript": "2.4.10", 42 | "@graphql-codegen/typescript-operations": "2.3.7", 43 | "@graphql-codegen/typescript-react-apollo": "3.2.13", 44 | "@graphql-codegen/typescript-resolvers": "2.6.3", 45 | "@types/node": "^17.0.31", 46 | "@types/react": "^18.0.8", 47 | "@types/react-dom": "^18.0.3", 48 | "@types/uuid": "^8.3.4", 49 | "@typescript-eslint/eslint-plugin": "^5.22.0", 50 | "@typescript-eslint/parser": "^5.22.0", 51 | "copyfiles": "^2.4.1", 52 | "env-cmd": "^10.1.0", 53 | "eslint": "^8.14.0", 54 | "eslint-config-prettier": "^8.5.0", 55 | "eslint-plugin-prettier": "^4.0.0", 56 | "eslint-plugin-react": "^7.29.4", 57 | "lint-staged": "^12.4.1", 58 | "prettier": "^2.6.2", 59 | "prettier-plugin-organize-imports": "^2.3.4", 60 | "ts-node": "^10.7.0", 61 | "typescript": "^4.6.4" 62 | }, 63 | "lint-staged": { 64 | "./**/*.{js,jsx,ts,tsx}": [ 65 | "npm run lint:fix" 66 | ] 67 | }, 68 | "volta": { 69 | "node": "16.15.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/seo/DefaultSeo.tsx: -------------------------------------------------------------------------------- 1 | import { DefaultSeo as DS } from 'next-seo' 2 | import { FC } from 'react' 3 | 4 | export const DefaultSeo: FC = () => { 5 | const serviceName = 'Next.js Apollo Sample' 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /src/context/modal.tsx: -------------------------------------------------------------------------------- 1 | import { useDisclosure } from '@chakra-ui/hooks' 2 | import { Modal, ModalBody, ModalContent, ModalOverlay } from '@chakra-ui/modal' 3 | import { createContext, FC, ReactNode, useCallback, useContext, useState } from 'react' 4 | 5 | const stub = (): never => { 6 | throw new Error() 7 | } 8 | 9 | const initialState: State & Dispatch = { 10 | isOpen: false, 11 | showModal: stub, 12 | hideModal: stub, 13 | } 14 | 15 | type State = { 16 | isOpen: boolean 17 | body?: ReactNode 18 | } 19 | 20 | type Dispatch = { 21 | showModal: (body?: State['body']) => void 22 | hideModal: () => void 23 | } 24 | 25 | const StateContext = createContext({ ...initialState }) 26 | const DispatchContext = createContext({ ...initialState }) 27 | 28 | export const ModalProviderContainer: FC<{ children: ReactNode }> = ({ children }) => { 29 | const state = useCore() 30 | return ( 31 | 32 | 33 | {children} 34 | 35 | 36 | {state.state.body && {state.state.body}} 37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | const useCore = (): { state: State; dispatcher: Dispatch } => { 44 | const [state, setState] = useState({ ...initialState }) 45 | const { isOpen, onOpen, onClose } = useDisclosure() 46 | 47 | const showModal: Dispatch['showModal'] = useCallback(body => { 48 | setState({ ...state, body }) 49 | onOpen() 50 | }, []) 51 | 52 | const hideModal: Dispatch['hideModal'] = useCallback(() => { 53 | onClose() 54 | }, []) 55 | 56 | return { 57 | state: { ...state, isOpen }, 58 | dispatcher: { showModal, hideModal }, 59 | } 60 | } 61 | 62 | export const useModalState = () => useContext(StateContext) 63 | export const useModalDispatcher = () => useContext(DispatchContext) 64 | -------------------------------------------------------------------------------- /src/env/.env.dev: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_ENV=dev 2 | -------------------------------------------------------------------------------- /src/env/.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_ENV=local 2 | NEXT_PUBLIC_HOST=http://localhost:3000 3 | -------------------------------------------------------------------------------- /src/env/.env.prod: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_ENV=prod 2 | -------------------------------------------------------------------------------- /src/env/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | interface PublicEnv { 3 | env: 'local' | 'dev' | 'prod' 4 | host: string 5 | } 6 | 7 | export const publicEnv: PublicEnv = { 8 | env: process.env.NEXT_PUBLIC_APP_ENV! as any, 9 | host: process.env.NEXT_PUBLIC_HOST!, 10 | } 11 | -------------------------------------------------------------------------------- /src/graphqlClient/apollo.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client' 2 | import { setContext } from '@apollo/client/link/context' 3 | import { publicEnv } from 'env' 4 | 5 | const httpLink = createHttpLink({ 6 | uri: publicEnv.host + '/api/graphql', 7 | }) 8 | 9 | const authLink = setContext(async (_, { headers }) => { 10 | let idToken: string | null 11 | try { 12 | // TODO: get and set id token 13 | idToken = 'sample' 14 | } catch { 15 | idToken = null 16 | } 17 | 18 | if (publicEnv.env !== 'prod') console.log(`idToken: ${idToken}`) 19 | 20 | return { 21 | headers: { 22 | ...headers, 23 | authorization: idToken ? `Bearer ${idToken}` : '', 24 | }, 25 | } 26 | }) 27 | 28 | const _client = new ApolloClient({ 29 | link: authLink.concat(httpLink), 30 | cache: new InMemoryCache({}), 31 | }) 32 | 33 | export const apolloClient = _client 34 | -------------------------------------------------------------------------------- /src/graphqlClient/mutation/saveUser.graphql: -------------------------------------------------------------------------------- 1 | mutation saveUser($name: String) { 2 | saveUser(name: $name) { 3 | id 4 | name 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/graphqlClient/query/user.graphql: -------------------------------------------------------------------------------- 1 | query user($id: ID!) { 2 | user(id: $id) { 3 | id 4 | name 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/graphqlServer/errors/condition.ts: -------------------------------------------------------------------------------- 1 | import { ApolloError } from 'apollo-server-micro' 2 | 3 | export class ConditionError extends ApolloError { 4 | constructor(message: string) { 5 | super(message, 'INVALID_CONDITION') 6 | 7 | Object.defineProperty(this, 'name', { value: 'ConditionError' }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/graphqlServer/index.ts: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema } from '@graphql-tools/schema' 2 | import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core' 3 | import { ApolloServer, gql } from 'apollo-server-micro' 4 | import { publicEnv } from 'env' 5 | import * as fs from 'fs' 6 | import * as path from 'path' 7 | import { LoggingPlugin } from './plugins/logging' 8 | import { resolvers } from './resolvers' 9 | import { ensureContext } from './resolvers/context' 10 | import { dataSources } from './resolvers/datasources' 11 | import { setupDirectives } from './resolvers/directives' 12 | 13 | export const initializeApolloServer = () => { 14 | // nodeを実行している環境からのパス 15 | const typeDefs = gql(fs.readFileSync(path.resolve(process.cwd(), './graphql/schema.graphql')).toString()) 16 | const canDebug = publicEnv.env !== 'prod' 17 | 18 | let schema = makeExecutableSchema({ typeDefs, resolvers }) 19 | schema = setupDirectives(schema) 20 | 21 | const server = new ApolloServer({ 22 | debug: canDebug, 23 | introspection: canDebug, 24 | schema, 25 | context: ensureContext, 26 | dataSources, 27 | plugins: [new LoggingPlugin(), ApolloServerPluginLandingPageGraphQLPlayground()], 28 | }) 29 | 30 | return server 31 | } 32 | -------------------------------------------------------------------------------- /src/graphqlServer/plugins/logging.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServerPlugin, GraphQLRequestContext, GraphQLRequestListener } from 'apollo-server-plugin-base' 2 | import { Logger } from 'lib/logger' 3 | import { CustomContext } from 'types/server/context' 4 | import { v4 } from 'uuid' 5 | 6 | type Context = CustomContext 7 | 8 | export class LoggingPlugin implements ApolloServerPlugin { 9 | async requestDidStart(context: GraphQLRequestContext): Promise> { 10 | const requestID = v4() 11 | 12 | Logger.info({ 13 | requestID, 14 | message: 'request_start', 15 | operationName: context.operationName ?? context.request.operationName, 16 | variables: context.request.variables, 17 | }) 18 | 19 | return { 20 | didEncounterErrors: async ({ errors }) => { 21 | // FIXME: とりあえず全件出力しているので適宜修正 22 | errors.forEach(error => { 23 | Logger.error( 24 | { 25 | requestID, 26 | message: 'request_end_with_error', 27 | context: { 28 | ...context.context, 29 | }, 30 | }, 31 | error 32 | ) 33 | }) 34 | }, 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/graphqlServer/resolvers/context.ts: -------------------------------------------------------------------------------- 1 | import { ContextFunction } from 'apollo-server-core' 2 | import { IncomingMessage, ServerResponse } from 'http' 3 | import { PartialCustomContext } from 'types/server/context' 4 | 5 | export type NextContext = { req: IncomingMessage; res: ServerResponse } 6 | 7 | export const ensureContext: ContextFunction = async (args): Promise => { 8 | if ((args as any)?._ensured === true) { 9 | // 型をつけるのが難しすぎるので仕方なくanyにキャストして対応 10 | return args as any 11 | } 12 | 13 | const context: PartialCustomContext = { 14 | _ensured: true, 15 | } 16 | 17 | const authorization = args.req.headers.authorization 18 | if (authorization) { 19 | // TODO: verify id token 20 | const verified = false 21 | if (verified) { 22 | context.userID = 'sub' 23 | } 24 | } 25 | 26 | return context 27 | } 28 | -------------------------------------------------------------------------------- /src/graphqlServer/resolvers/datasources/index.ts: -------------------------------------------------------------------------------- 1 | export const dataSources = () => ({}) 2 | -------------------------------------------------------------------------------- /src/graphqlServer/resolvers/directives/authorization.ts: -------------------------------------------------------------------------------- 1 | import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils' 2 | import { AuthenticationError } from 'apollo-server-micro' 3 | import { defaultFieldResolver, GraphQLSchema } from 'graphql' 4 | import { UserRole } from 'types/generated/serverGraphql' 5 | import { PartialCustomContext } from 'types/server/context' 6 | 7 | const directiveName = 'authorization' 8 | 9 | export const authorizationDirectiveTransformer = (schema: GraphQLSchema) => { 10 | const typeDirectiveArgumentMaps: Record = {} 11 | 12 | return mapSchema(schema, { 13 | [MapperKind.OBJECT_TYPE]: objectType => { 14 | const authDirective = getDirective(schema, objectType, directiveName)?.[0] 15 | if (authDirective) { 16 | typeDirectiveArgumentMaps[objectType.name] = authDirective 17 | } 18 | return undefined 19 | }, 20 | [MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => { 21 | const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0] ?? typeDirectiveArgumentMaps[typeName] 22 | 23 | if (authDirective) { 24 | const requiredRole: UserRole | undefined = authDirective.requires 25 | 26 | if (requiredRole) { 27 | const { resolve = defaultFieldResolver } = fieldConfig 28 | 29 | fieldConfig.resolve = async (source, args, context: PartialCustomContext, info) => { 30 | switch (requiredRole) { 31 | case 'member': 32 | if (!context.userID) { 33 | throw new AuthenticationError('ログインが必要です。') 34 | } 35 | break 36 | } 37 | 38 | return resolve(source, args, context, info) 39 | } 40 | 41 | return fieldConfig 42 | } 43 | } 44 | }, 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /src/graphqlServer/resolvers/directives/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql' 2 | import { authorizationDirectiveTransformer } from './authorization' 3 | 4 | export const setupDirectives = (schema: GraphQLSchema) => { 5 | const s = authorizationDirectiveTransformer(schema) 6 | // directiveが追加されたらsを順番に渡していって更新していく 7 | return s 8 | } 9 | -------------------------------------------------------------------------------- /src/graphqlServer/resolvers/entity/user.ts: -------------------------------------------------------------------------------- 1 | import { UserResolvers } from 'types/generated/serverGraphql' 2 | 3 | export const User: UserResolvers = { 4 | name: parent => `${parent.id} name`, 5 | createdAt: () => new Date(), 6 | } 7 | -------------------------------------------------------------------------------- /src/graphqlServer/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import { Resolvers } from 'types/generated/serverGraphql' 2 | import { User } from './entity/user' 3 | import { mutationResolvers } from './mutation' 4 | import { queryResolvers } from './query' 5 | import { DateTimeType } from './scalars/dateTime' 6 | 7 | export const resolvers: Resolvers = { 8 | DateTime: new DateTimeType(), 9 | Query: queryResolvers, 10 | Mutation: mutationResolvers, 11 | 12 | // Entity 13 | 14 | User, 15 | } 16 | -------------------------------------------------------------------------------- /src/graphqlServer/resolvers/loaders/user.ts: -------------------------------------------------------------------------------- 1 | import DataLoader from 'dataloader' 2 | import { User } from 'types/generated/serverGraphql' 3 | 4 | export const userLoader = new DataLoader( 5 | async userIDs => { 6 | return Promise.resolve(userIDs.map(id => ({ id, name: `${id} name` }))) 7 | }, 8 | { cache: false } 9 | ) 10 | -------------------------------------------------------------------------------- /src/graphqlServer/resolvers/mutation/index.ts: -------------------------------------------------------------------------------- 1 | import { MutationResolvers } from 'types/generated/serverGraphql' 2 | 3 | export const mutationResolvers: MutationResolvers = { 4 | saveUser: (_, args, context) => ({ id: context.userID ?? 'none', name: args.name }), 5 | } 6 | -------------------------------------------------------------------------------- /src/graphqlServer/resolvers/query/index.ts: -------------------------------------------------------------------------------- 1 | import { QueryResolvers } from 'types/generated/serverGraphql' 2 | 3 | export const queryResolvers: QueryResolvers = { 4 | user: (_, args) => ({ id: args.id }), 5 | } 6 | -------------------------------------------------------------------------------- /src/graphqlServer/resolvers/scalars/dateTime.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql' 2 | 3 | export class DateTimeType extends GraphQLScalarType { 4 | constructor() { 5 | super({ 6 | name: 'DateTime', 7 | description: 'ISO8601 formatted string(ex: `2014-10-10T13:50:40+09:00`)', 8 | serialize(value) { 9 | if (value instanceof Date) return value.toISOString() 10 | throw new Error(`incompatible type(${typeof value}) returned in serialize of DateTime`) 11 | }, 12 | parseValue(value) { 13 | if (typeof value !== 'string') throw new Error(`incompatible type (${typeof value}) in parse of DateTime`) 14 | return new Date(value) // Convert incoming string to Date 15 | }, 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/helper/array.ts: -------------------------------------------------------------------------------- 1 | export const makeArrayWritable = (array: readonly T[]): T[] => { 2 | return array as T[] 3 | } 4 | 5 | export function splitArray(array: T[], size: number) { 6 | const list: T[][] = [] 7 | let i = 0 8 | // eslint-disable-next-line no-constant-condition 9 | while (true) { 10 | const sliced = array.slice(i * size, size * (i + 1)) 11 | if (sliced.length > 0) { 12 | list.push(sliced) 13 | i++ 14 | } else { 15 | break 16 | } 17 | } 18 | return list 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/useCustomToast.ts: -------------------------------------------------------------------------------- 1 | import { useToast, UseToastOptions } from '@chakra-ui/toast' 2 | 3 | export const useCustomToast = () => { 4 | const toast = useToast() 5 | 6 | return (status: UseToastOptions['status'], title: string, options?: UseToastOptions) => { 7 | toast({ 8 | position: 'top', 9 | duration: status === 'error' ? 5000 : 3000, 10 | status, 11 | title, 12 | isClosable: true, 13 | ...options, 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/logger.ts: -------------------------------------------------------------------------------- 1 | import { publicEnv } from 'env' 2 | 3 | const LogLevel = { 4 | debug: 'debug', 5 | info: 'info', 6 | warn: 'warn', 7 | error: 'error', 8 | } as const 9 | type LogLevel = typeof LogLevel[keyof typeof LogLevel] 10 | 11 | type Payload = { 12 | [key: string]: any 13 | } 14 | 15 | export class Logger { 16 | private static output(level: LogLevel, payload: Payload, error?: Error) { 17 | const entry: Payload = { 18 | ...payload, 19 | severity: level, 20 | } 21 | 22 | if (error) { 23 | entry.errorMessage = error.message 24 | entry.errorName = error.name 25 | entry.errorStacktrace = error.stack 26 | } 27 | 28 | console[level](JSON.stringify(entry)) 29 | } 30 | 31 | static debug(payload: Payload, error?: Error) { 32 | if (publicEnv.env === 'local') { 33 | this.output('debug', payload, error) 34 | } 35 | } 36 | 37 | static info(payload: Payload, error?: Error) { 38 | this.output('info', payload, error) 39 | } 40 | 41 | static warn(payload: Payload, error?: Error) { 42 | this.output('warn', payload, error) 43 | } 44 | 45 | static error(payload: Payload, error?: Error) { 46 | this.output('error', payload, error) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import Link from 'next/link' 3 | 4 | const NotFoundPage: NextPage = () => { 5 | return ( 6 |
7 |

ページが見つかりませんでした

8 | トップページへ 9 |
10 | ) 11 | } 12 | 13 | export default NotFoundPage 14 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloProvider } from '@apollo/client' 2 | import { ChakraProvider } from '@chakra-ui/react' 3 | import { DefaultSeo } from 'components/seo/DefaultSeo' 4 | import { ModalProviderContainer } from 'context/modal' 5 | import { apolloClient } from 'graphqlClient/apollo' 6 | import { AppProps } from 'next/app' 7 | import Head from 'next/head' 8 | import { FC, ReactNode } from 'react' 9 | 10 | const Providers: FC<{ children: ReactNode }> = ({ children }) => { 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ) 18 | } 19 | 20 | const App = ({ Component, pageProps }: AppProps) => { 21 | return ( 22 | <> 23 | 24 | Next.js Apollo Sample 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default App 37 | -------------------------------------------------------------------------------- /src/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next' 2 | import Link from 'next/link' 3 | 4 | type Props = { 5 | statusCode: number 6 | message?: string 7 | } 8 | 9 | const ErrorPage: NextPage = (props: Props) => { 10 | const generateMessage = () => { 11 | switch (props.statusCode) { 12 | case 401: 13 | return 'アクセス権限がありません' 14 | case 404: 15 | return 'ページが見つかりませんでした' 16 | default: 17 | break 18 | } 19 | } 20 | return ( 21 |
22 |

{generateMessage()}

23 | トップページへ 24 |
25 | ) 26 | } 27 | 28 | export default ErrorPage 29 | -------------------------------------------------------------------------------- /src/pages/api/graphql.tsx: -------------------------------------------------------------------------------- 1 | import { initializeApolloServer } from 'graphqlServer' 2 | import { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | export const config = { api: { bodyParser: false } } 5 | 6 | const server = initializeApolloServer() 7 | const startServer = server.start() 8 | 9 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 10 | await startServer 11 | await server.createHandler({ 12 | path: '/api/graphql', 13 | })(req, res) 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Heading, Link, Spacer, VStack } from '@chakra-ui/layout' 2 | import { NextPage } from 'next' 3 | import NextLink from 'next/link' 4 | import React from 'react' 5 | import { useUserQuery } from 'types/generated/clientGraphql' 6 | 7 | const Page: NextPage = () => { 8 | const { data } = useUserQuery({ variables: { id: 'sampleID' } }) 9 | 10 | return ( 11 | 12 | 13 | Hello Next.js Apollo Sample! 14 | 15 | 16 | 17 | 18 | リンク集 19 | 20 | 21 | GraphQL Playground 22 | 23 | 24 | 25 | 26 | {data && GraphQLから取得したデータ: {JSON.stringify(data.user)}} 27 | 28 | 29 | ) 30 | } 31 | 32 | export default Page 33 | -------------------------------------------------------------------------------- /src/types/generated/clientGraphql.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | import * as Apollo from '@apollo/client'; 3 | export type Maybe = T | undefined; 4 | export type InputMaybe = T | undefined; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 7 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 8 | const defaultOptions = {} as const; 9 | /** All built-in and custom scalars, mapped to their actual values */ 10 | export type Scalars = { 11 | ID: string; 12 | String: string; 13 | Boolean: boolean; 14 | Int: number; 15 | Float: number; 16 | DateTime: any; 17 | }; 18 | 19 | export type Mutation = { 20 | __typename?: 'Mutation'; 21 | saveUser: User; 22 | }; 23 | 24 | 25 | export type MutationSaveUserArgs = { 26 | name?: InputMaybe; 27 | }; 28 | 29 | export type Query = { 30 | __typename?: 'Query'; 31 | user?: Maybe; 32 | }; 33 | 34 | 35 | export type QueryUserArgs = { 36 | id: Scalars['ID']; 37 | }; 38 | 39 | export type User = { 40 | __typename?: 'User'; 41 | createdAt: Scalars['DateTime']; 42 | id: Scalars['ID']; 43 | name?: Maybe; 44 | }; 45 | 46 | export type UserRole = 47 | | 'member'; 48 | 49 | export type SaveUserMutationVariables = Exact<{ 50 | name?: InputMaybe; 51 | }>; 52 | 53 | 54 | export type SaveUserMutation = { __typename?: 'Mutation', saveUser: { __typename?: 'User', id: string, name?: string | undefined } }; 55 | 56 | export type UserQueryVariables = Exact<{ 57 | id: Scalars['ID']; 58 | }>; 59 | 60 | 61 | export type UserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, name?: string | undefined } | undefined }; 62 | 63 | 64 | export const SaveUserDocument = gql` 65 | mutation saveUser($name: String) { 66 | saveUser(name: $name) { 67 | id 68 | name 69 | } 70 | } 71 | `; 72 | export type SaveUserMutationFn = Apollo.MutationFunction; 73 | 74 | /** 75 | * __useSaveUserMutation__ 76 | * 77 | * To run a mutation, you first call `useSaveUserMutation` within a React component and pass it any options that fit your needs. 78 | * When your component renders, `useSaveUserMutation` returns a tuple that includes: 79 | * - A mutate function that you can call at any time to execute the mutation 80 | * - An object with fields that represent the current status of the mutation's execution 81 | * 82 | * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; 83 | * 84 | * @example 85 | * const [saveUserMutation, { data, loading, error }] = useSaveUserMutation({ 86 | * variables: { 87 | * name: // value for 'name' 88 | * }, 89 | * }); 90 | */ 91 | export function useSaveUserMutation(baseOptions?: Apollo.MutationHookOptions) { 92 | const options = {...defaultOptions, ...baseOptions} 93 | return Apollo.useMutation(SaveUserDocument, options); 94 | } 95 | export type SaveUserMutationHookResult = ReturnType; 96 | export type SaveUserMutationResult = Apollo.MutationResult; 97 | export type SaveUserMutationOptions = Apollo.BaseMutationOptions; 98 | export const UserDocument = gql` 99 | query user($id: ID!) { 100 | user(id: $id) { 101 | id 102 | name 103 | } 104 | } 105 | `; 106 | 107 | /** 108 | * __useUserQuery__ 109 | * 110 | * To run a query within a React component, call `useUserQuery` and pass it any options that fit your needs. 111 | * When your component renders, `useUserQuery` returns an object from Apollo Client that contains loading, error, and data properties 112 | * you can use to render your UI. 113 | * 114 | * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; 115 | * 116 | * @example 117 | * const { data, loading, error } = useUserQuery({ 118 | * variables: { 119 | * id: // value for 'id' 120 | * }, 121 | * }); 122 | */ 123 | export function useUserQuery(baseOptions: Apollo.QueryHookOptions) { 124 | const options = {...defaultOptions, ...baseOptions} 125 | return Apollo.useQuery(UserDocument, options); 126 | } 127 | export function useUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { 128 | const options = {...defaultOptions, ...baseOptions} 129 | return Apollo.useLazyQuery(UserDocument, options); 130 | } 131 | export type UserQueryHookResult = ReturnType; 132 | export type UserLazyQueryHookResult = ReturnType; 133 | export type UserQueryResult = Apollo.QueryResult; -------------------------------------------------------------------------------- /src/types/generated/serverGraphql.d.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; 2 | import { CustomContext } from '../server/context'; 3 | export type Maybe = T | null; 4 | export type InputMaybe = Maybe; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 7 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 8 | export type RequireFields = Omit & { [P in K]-?: NonNullable }; 9 | /** All built-in and custom scalars, mapped to their actual values */ 10 | export type Scalars = { 11 | ID: string; 12 | String: string; 13 | Boolean: boolean; 14 | Int: number; 15 | Float: number; 16 | DateTime: Date; 17 | }; 18 | 19 | export type Mutation = { 20 | __typename?: 'Mutation'; 21 | saveUser: User; 22 | }; 23 | 24 | 25 | export type MutationSaveUserArgs = { 26 | name?: InputMaybe; 27 | }; 28 | 29 | export type Query = { 30 | __typename?: 'Query'; 31 | user?: Maybe; 32 | }; 33 | 34 | 35 | export type QueryUserArgs = { 36 | id: Scalars['ID']; 37 | }; 38 | 39 | export type User = { 40 | __typename?: 'User'; 41 | createdAt: Scalars['DateTime']; 42 | id: Scalars['ID']; 43 | name?: Maybe; 44 | }; 45 | 46 | export type UserRole = 47 | | 'member'; 48 | 49 | export type WithIndex = TObject & Record; 50 | export type ResolversObject = WithIndex; 51 | 52 | export type ResolverTypeWrapper = Promise | T; 53 | 54 | 55 | export type ResolverWithResolve = { 56 | resolve: ResolverFn; 57 | }; 58 | export type Resolver = ResolverFn | ResolverWithResolve; 59 | 60 | export type ResolverFn = ( 61 | parent: TParent, 62 | args: TArgs, 63 | context: TContext, 64 | info: GraphQLResolveInfo 65 | ) => Promise | TResult; 66 | 67 | export type SubscriptionSubscribeFn = ( 68 | parent: TParent, 69 | args: TArgs, 70 | context: TContext, 71 | info: GraphQLResolveInfo 72 | ) => AsyncIterable | Promise>; 73 | 74 | export type SubscriptionResolveFn = ( 75 | parent: TParent, 76 | args: TArgs, 77 | context: TContext, 78 | info: GraphQLResolveInfo 79 | ) => TResult | Promise; 80 | 81 | export interface SubscriptionSubscriberObject { 82 | subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; 83 | resolve?: SubscriptionResolveFn; 84 | } 85 | 86 | export interface SubscriptionResolverObject { 87 | subscribe: SubscriptionSubscribeFn; 88 | resolve: SubscriptionResolveFn; 89 | } 90 | 91 | export type SubscriptionObject = 92 | | SubscriptionSubscriberObject 93 | | SubscriptionResolverObject; 94 | 95 | export type SubscriptionResolver = 96 | | ((...args: any[]) => SubscriptionObject) 97 | | SubscriptionObject; 98 | 99 | export type TypeResolveFn = ( 100 | parent: TParent, 101 | context: TContext, 102 | info: GraphQLResolveInfo 103 | ) => Maybe | Promise>; 104 | 105 | export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; 106 | 107 | export type NextResolverFn = () => Promise; 108 | 109 | export type DirectiveResolverFn = ( 110 | next: NextResolverFn, 111 | parent: TParent, 112 | args: TArgs, 113 | context: TContext, 114 | info: GraphQLResolveInfo 115 | ) => TResult | Promise; 116 | 117 | /** Mapping between all available schema types and the resolvers types */ 118 | export type ResolversTypes = ResolversObject<{ 119 | Boolean: ResolverTypeWrapper; 120 | DateTime: ResolverTypeWrapper; 121 | ID: ResolverTypeWrapper; 122 | Mutation: ResolverTypeWrapper<{}>; 123 | Query: ResolverTypeWrapper<{}>; 124 | String: ResolverTypeWrapper; 125 | User: ResolverTypeWrapper; 126 | UserRole: UserRole; 127 | }>; 128 | 129 | /** Mapping between all available schema types and the resolvers parents */ 130 | export type ResolversParentTypes = ResolversObject<{ 131 | Boolean: Scalars['Boolean']; 132 | DateTime: Scalars['DateTime']; 133 | ID: Scalars['ID']; 134 | Mutation: {}; 135 | Query: {}; 136 | String: Scalars['String']; 137 | User: User; 138 | }>; 139 | 140 | export type AuthorizationDirectiveArgs = { 141 | requires?: Maybe; 142 | }; 143 | 144 | export type AuthorizationDirectiveResolver = DirectiveResolverFn; 145 | 146 | export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig { 147 | name: 'DateTime'; 148 | } 149 | 150 | export type MutationResolvers = ResolversObject<{ 151 | saveUser?: Resolver>; 152 | }>; 153 | 154 | export type QueryResolvers = ResolversObject<{ 155 | user?: Resolver, ParentType, ContextType, RequireFields>; 156 | }>; 157 | 158 | export type UserResolvers = ResolversObject<{ 159 | createdAt?: Resolver; 160 | id?: Resolver; 161 | name?: Resolver, ParentType, ContextType>; 162 | __isTypeOf?: IsTypeOfResolverFn; 163 | }>; 164 | 165 | export type Resolvers = ResolversObject<{ 166 | DateTime?: GraphQLScalarType; 167 | Mutation?: MutationResolvers; 168 | Query?: QueryResolvers; 169 | User?: UserResolvers; 170 | }>; 171 | 172 | export type DirectiveResolvers = ResolversObject<{ 173 | authorization?: AuthorizationDirectiveResolver; 174 | }>; 175 | -------------------------------------------------------------------------------- /src/types/server/context.d.ts: -------------------------------------------------------------------------------- 1 | import { dataSources } from 'graphqlServer/resolvers/datasources' 2 | 3 | export type PartialCustomContext = { 4 | _ensured?: boolean 5 | userID?: string 6 | } 7 | 8 | export type CustomContext = PartialCustomContext & { 9 | dataSources: ReturnType 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "lib": [ 5 | "dom", 6 | "ES2020" 7 | ], 8 | "module": "commonjs", 9 | "target": "ES2020", 10 | "baseUrl": "./src", 11 | "moduleResolution": "node", 12 | "strict": true, 13 | "noEmit": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "isolatedModules": true, 20 | "removeComments": false, 21 | "preserveConstEnums": true, 22 | "sourceMap": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "resolveJsonModule": true, 25 | "allowJs": true, 26 | "incremental": true 27 | }, 28 | "exclude": [ 29 | "dist", 30 | ".next", 31 | "out", 32 | "scripts", 33 | "node_modules" 34 | ], 35 | "include": [ 36 | "next-env.d.ts", 37 | "**/*.ts", 38 | "**/*.tsx" 39 | ] 40 | } 41 | --------------------------------------------------------------------------------