├── .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 |
--------------------------------------------------------------------------------