├── .gitignore ├── README.md ├── cheatsheet ├── client │ ├── .babelrc │ ├── .env.example │ ├── .graphqlconfig.yml │ ├── codegen.yml │ ├── index.js │ ├── next.config.js │ ├── package.json │ ├── serverless.yml │ ├── src │ │ ├── apollo │ │ │ └── index.ts │ │ ├── assets │ │ │ └── favicon.png │ │ ├── generated │ │ │ ├── graphql.tsx │ │ │ └── schema.graphql │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ └── index.tsx │ │ ├── queries │ │ │ ├── createTask.graphql │ │ │ ├── deleteTask.graphql │ │ │ ├── getTasks.graphql │ │ │ └── updateTask.graphql │ │ ├── serverless │ │ │ └── main.js │ │ ├── store │ │ │ └── index.ts │ │ └── types │ │ │ ├── graphql.d.ts │ │ │ ├── images.d.ts │ │ │ └── vendors.d.ts │ ├── tsconfig.json │ ├── tslint.json │ ├── webpack.config.js │ └── yarn.lock └── server │ ├── .env.development │ ├── .env.example │ ├── .env.production │ ├── .vscode │ └── settings.json │ ├── datamodel.prisma │ ├── package.json │ ├── prisma.yml │ ├── serverless.yml │ ├── src │ ├── app.ts │ ├── generated │ │ ├── nexus-prisma │ │ │ ├── datamodel-info.ts │ │ │ ├── index.ts │ │ │ └── nexus-prisma.ts │ │ ├── nexus.ts │ │ ├── prisma │ │ │ ├── index.ts │ │ │ └── prisma-schema.ts │ │ └── schema.graphql │ ├── resolvers │ │ ├── Mutation.ts │ │ ├── Query.ts │ │ ├── index.ts │ │ └── task │ │ │ ├── Mutation.ts │ │ │ ├── Query.ts │ │ │ └── index.ts │ ├── server.ts │ └── serverless.ts │ ├── tsconfig.json │ ├── tslint.json │ ├── webpack.config.js │ └── yarn.lock ├── documents ├── 1-graphql │ ├── README.md │ └── images │ │ ├── diagram-1.png │ │ └── screenshot-1.png ├── 2-serverless │ ├── README.md │ └── images │ │ ├── diagram-1.png │ │ ├── screenshot-1.png │ │ ├── screenshot-10.png │ │ ├── screenshot-11.png │ │ ├── screenshot-12.png │ │ ├── screenshot-13.png │ │ ├── screenshot-14.png │ │ ├── screenshot-15.png │ │ ├── screenshot-16.png │ │ ├── screenshot-2.png │ │ ├── screenshot-3.png │ │ ├── screenshot-4.png │ │ ├── screenshot-5.png │ │ ├── screenshot-6.png │ │ ├── screenshot-7.png │ │ ├── screenshot-8.png │ │ └── screenshot-9.png ├── 3-prisma-on-aws │ ├── README.md │ └── images │ │ ├── diagram-1.png │ │ ├── screenshot-1.png │ │ ├── screenshot-10.png │ │ ├── screenshot-11.png │ │ ├── screenshot-12.png │ │ ├── screenshot-13.png │ │ ├── screenshot-14.png │ │ ├── screenshot-2.png │ │ ├── screenshot-3.png │ │ ├── screenshot-4.png │ │ ├── screenshot-5.png │ │ ├── screenshot-6.png │ │ ├── screenshot-7.png │ │ ├── screenshot-8.png │ │ └── screenshot-9.png ├── 4-prisma │ ├── README.md │ └── images │ │ ├── diagram-1.png │ │ ├── diagram-2.png │ │ ├── diagram-3.png │ │ ├── diagram-4.png │ │ ├── screenshot-1.png │ │ ├── screenshot-2.png │ │ ├── screenshot-3.png │ │ ├── screenshot-4.png │ │ ├── screenshot-5.png │ │ ├── screenshot-6.png │ │ └── screenshot-7.png ├── 5-react-graphql │ ├── README.md │ └── images │ │ └── screenshot-1.png └── 6-delete │ ├── README.md │ └── images │ ├── screenshot-1.png │ ├── screenshot-2.png │ ├── screenshot-3.png │ ├── screenshot-4.png │ ├── screenshot-5.png │ ├── screenshot-6.png │ └── screenshot-7.png ├── full-architecture-3.png ├── starters ├── client │ ├── .babelrc │ ├── .env.example │ ├── .graphqlconfig.yml │ ├── codegen.yml │ ├── index.js │ ├── next.config.js │ ├── package.json │ ├── serverless.yml │ ├── src │ │ ├── apollo │ │ │ └── index.ts │ │ ├── assets │ │ │ └── favicon.png │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ └── index.tsx │ │ ├── serverless │ │ │ └── main.js │ │ ├── store │ │ │ └── index.ts │ │ └── types │ │ │ ├── graphql.d.ts │ │ │ ├── images.d.ts │ │ │ └── vendors.d.ts │ ├── tsconfig.json │ ├── tslint.json │ ├── webpack.config.js │ └── yarn.lock └── server │ ├── .env.example │ ├── .vscode │ └── settings.json │ ├── datamodel.prisma │ ├── package.json │ ├── prisma.yml │ ├── serverless.yml │ ├── src │ ├── app.ts │ ├── resolvers │ │ ├── Mutation.ts │ │ ├── Query.ts │ │ └── index.ts │ ├── server.ts │ └── serverless.ts │ ├── tsconfig.json │ ├── tslint.json │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── yarn.lock └── templates ├── prisma.aurora.serverless.yml └── prisma.mysql.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | yarn-error.log 5 | 6 | dist 7 | .serverless 8 | 9 | .env 10 | .env.development 11 | .env.production 12 | 13 | generated 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 서버리스 GraphQL 워크샵 🔨 2 | 서버리스 GraphQL 워크샵에 오신 여러분을 환영합니다! 3 | 4 | 이 워크숍은 기존에 GraphQL에 대한 배경이 없는 분들을 위해 준비되었습니다. GraphQL 백엔드에 대한 가장 기초적인 핵심 개념을 담았으며, 실제 Production에서 사용되는 도구들을 소개하며 Production 레벨에서의 GraphQL이 가진 핵심 가치가 무엇인지 배우고, 따라할 수 있는 시간을 가집니다. 5 | 6 | ![](./full-architecture-3.png) 7 | 8 | > 본 워크샵은 AWSKRUG 서버리스 모임이 주최합니다. [참가신청](https://www.meetup.com/ko-KR/awskrug/events/262459287/) 9 | > 10 | > 🖋 Written with 🔥 by [Tony](https://github.com/tonyfromundefined) 11 | 12 | ## 이런 기술 스택이 사용되었어요 🧐 13 | 14 | ### 언어 및 환경 15 | - [TypeScript](https://www.typescriptlang.org) 👏 16 | - [Node.js](https://nodejs.org/en/about/) 17 | - [Babel](https://babeljs.io) 18 | - [Webpack](https://webpack.js.org/) 19 | - [Dotenv](https://github.com/motdotla/dotenv) 20 | 21 | ### API 서버 22 | - [Express.js](https://expressjs.com/ko/) 23 | - [Apollo Server](https://www.apollographql.com/docs/apollo-server/) 24 | - [Prisma Nexus](https://nexus.js.org/) 👏 25 | - [Serverless Framework](https://serverless.com) 26 | - [AWS API Gateway](https://aws.amazon.com/ko/api-gateway/) 27 | - [AWS Lambda](https://aws.amazon.com/ko/lambda/) 28 | - [AWS S3](https://aws.amazon.com/ko/s3/) 29 | 30 | ### Data Loader + ORM 31 | - [Docker](https://www.docker.com/) 32 | - [Prisma](https://www.prisma.io/) 👏 33 | - [AWS Fargate](https://aws.amazon.com/ko/fargate/) 👏 34 | 35 | ### Database 36 | - [MySQL](https://www.mysql.com/) 37 | - [AWS Aurora Serverless](https://aws.amazon.com/ko/rds/aurora/serverless/) 38 | - [AWS RDS](https://aws.amazon.com/ko/rds/) 39 | 40 | ### Web Client 41 | - [React.js](https://reactjs.org/) 42 | - [Next.js](https://nextjs.org/) 43 | - [Apollo Client](https://github.com/apollographql/apollo-client) 44 | - [React Apollo](https://github.com/apollographql/react-apollo) 45 | - [React Apollo Hooks](https://github.com/trojanowski/react-apollo-hooks) 46 | - [GraphQL Code Generator](https://graphql-code-generator.com) 👏 47 | - [MobX 5](https://github.com/mobxjs/mobx) 48 | 49 | ### 기타 50 | - [AWS CloudFormation](https://aws.amazon.com/ko/cloudformation/) 51 | 52 | 53 | ## 미리 준비해주세요 54 | ### 0. 본 Github Repository를 본인의 컴퓨터에 복사해주세요 55 | 중간중간 실습에 필요한 파일들이 업로드 되어있습니다. 세션 시작 전 미리 다운 받아주세요. 56 | - `.zip` 파일로 [다운로드](https://github.com/tonyfromundefined/serverless-graphql-workshop/archive/master.zip) 57 | - 또는 58 | ```bash 59 | $ git clone https://github.com/tonyfromundefined/serverless-graphql-workshop 60 | ``` 61 | 62 | ### 1. PC 또는 Mac 63 | 본 세션은 코딩 과정이 포함되어 있습니다. 또한 CLI(Command Line Interface) 조작이 꼭 필요합니다. 모바일 환경(iPhone, iPad, Android)에서는 진행이 불가능하니 꼭 PC/Mac 환경에서 진행하세요. 64 | 65 | ### 2. AWS 계정 66 | - AWS 계정 만들기 [이동](https://aws.amazon.com/ko/) 67 | 68 | 본 가이드는 한명이 하나의 AWS 계정을 사용한다고 가정합니다. AWS API Gateway, Lambda, ECS, RDS, S3, CloudWatch에 접근할 수 있어야 하며, 다른 사람과 계정을 공유하게 되면 특정 리소스에 대해 충돌이 발생 할 가능성이 있으므로 권장하지 않습니다. 69 | 70 | #### [중요] 본 워크샵에서 사용하는 'AWS Fargate' 서비스는 **과금**됩니다. 실습이 끝나고 바로 삭제하세요. 71 | 72 | **AWS Fargate를 제외한** 본 워크샵의 일환으로 시작하는 모든 리소스는 AWS 계정이 12개월 미만인 경우, 제공하는 AWS 프리티어로 충분히 가능합니다. 단, 사용량이 프리티어를 넘어서는 경우, 과금 될 수도 있습니다. 따라서, 새로운 실습용 계정을 만드시길 권장합니다. 자세한 내용은 [AWS 프리 티어 페이지](https://aws.amazon.com/free/)를 참조하세요. 73 | 74 | ### 3. 웹 브라우저 75 | - Chrome 최신 버전 [다운로드](https://www.google.com/chrome/) 76 | 77 | > Internet Explorer는 AWS Web Console에서 문제가 발생할 수 있습니다. 78 | 79 | ### 4. 텍스트 에디터 80 | - VS Code [다운로드](https://code.visualstudio.com/) 81 | 82 | 본 실습 세션에는 실제 코딩이 포함됩니다. 세션 발표자는 VS Code를 사용하니, 코딩에 익숙하지 않으신 분은 따라하기 쉽도록 환경을 동일하게 설정해주세요. 83 | 84 | ### 5. Node.js 85 | - Node.js 최신 버전 [다운로드](https://nodejs.org/en/) 86 | 87 | 88 | ## 자 그럼 이제 시작해볼까요? 89 | 1. [GraphQL 살펴보기](/documents/1-graphql/README.md) 90 | 1. GraphQL이란? 91 | 2. GraphQL Type 시스템과 `Query`, `Mutation` Type 92 | 3. Nexus로 시작하는 *Code-First* GraphQL 개발 93 | 4. GraphQL Playground 94 | 5. `Task` 타입과 쿼리, 뮤테이션 만들기 95 | 2. [Serverless로 GraphQL API 배포하기](/documents/2-serverless/README.md) 96 | 1. IAM 사용자 생성하기 97 | 2. Serverless Framework을 사용해 Node.js 프로젝트 배포하기 98 | 3. [AWS에 Prisma 배포하기 (CloudFormation)](/documents/3-prisma-on-aws/README.md) 99 | 4. [Prisma 사용하기](/documents/4-prisma/README.md) 100 | 1. Prisma란? 101 | 2. Prisma 시작하기 102 | 3. Prisma Client 사용해보기 103 | 4. `nexus-prisma`를 사용해, Prisma 연결하기 104 | 5. [React.js에서 GraphQL API 사용하기](/documents/5-react-graphql/README.md) 105 | 6. [삭제하기](/documents/6-delete/README.md) 106 | 1. API 배포 삭제하기 107 | 2. CloudFormation Stack 삭제하기 108 | 3. IAM 사용자 삭제하기 109 | 110 | ## 할 일 111 | Feature Request를 원하시면, 새 이슈를 생성해주세요. 또한, Pull Request는 언제나 환영입니다.🙏 112 | 113 | - [x] macOS 114 | - [x] Windows 115 | - [x] CMD 116 | - [x] Powershell 117 | - [ ] WSL Bash 118 | 119 | --- 120 | 121 | ### Cheatsheets 122 | - [완성된 서버 프로젝트](/cheatsheet/server) 123 | - [완성된 클라이언트 프로젝트](/cheatsheet/client) 124 | -------------------------------------------------------------------------------- /cheatsheet/client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel", 4 | "@zeit/next-typescript/babel" 5 | ], 6 | "plugins": [ 7 | ["@babel/plugin-proposal-decorators", { 8 | "legacy": true 9 | }], 10 | ["@babel/plugin-proposal-class-properties", { 11 | "loose": true 12 | }], 13 | ["module-resolver", { 14 | "root": ["./src"], 15 | "alias": { 16 | "~": "./src", 17 | "~~": "./src/services" 18 | } 19 | }] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /cheatsheet/client/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_APP_STAGE = "example" 2 | NEXT_APP_GRAPHQL_ENDPOINT = "https://fyeitajxaa.execute-api.ap-northeast-1.amazonaws.com/dev/graphql" 3 | NEXT_APP_VERSION = "0.0.1" 4 | -------------------------------------------------------------------------------- /cheatsheet/client/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | app: 3 | schemaPath: ./src/generated/schema.graphql 4 | includes: ["**/*.graphql"] 5 | extensions: 6 | endpoints: 7 | default: ${env:NEXT_APP_GRAPHQL_ENDPOINT} 8 | -------------------------------------------------------------------------------- /cheatsheet/client/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ./src/generated/schema.graphql 3 | documents: '**/*.graphql' 4 | generates: 5 | src/generated/graphql.tsx: 6 | plugins: 7 | - typescript 8 | - typescript-operations 9 | - typescript-react-apollo 10 | config: 11 | typesPrefix: I 12 | withHOC: true 13 | withHooks: true 14 | withComponent: true 15 | hooksImportFrom: react-apollo-hooks 16 | -------------------------------------------------------------------------------- /cheatsheet/client/index.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser') 2 | const cookieParser = require('cookie-parser') 3 | const express = require('express') 4 | const next = require('next') 5 | const path = require('path') 6 | 7 | const config = require('./next.config') 8 | 9 | const IS_PROD = process.env.NODE_ENV === 'production' 10 | const PORT = IS_PROD ? 80 : 3000 11 | 12 | main() 13 | 14 | async function main() { 15 | const server = await createServer() 16 | 17 | server.listen(PORT) 18 | } 19 | 20 | async function createServer() { 21 | const app = next({ 22 | config, 23 | dev: !IS_PROD, 24 | dir: path.resolve(__dirname, './src'), 25 | }) 26 | 27 | await app.prepare() 28 | 29 | const server = express() 30 | 31 | server.use(bodyParser.json()) 32 | server.use(bodyParser.urlencoded({ extended: true })) 33 | server.use(cookieParser()) 34 | 35 | server.use((req, res) => app.render(req, res, req._parsedUrl.pathname, req.query)) 36 | 37 | return server 38 | } 39 | -------------------------------------------------------------------------------- /cheatsheet/client/next.config.js: -------------------------------------------------------------------------------- 1 | const withOptimizedImages = require('next-optimized-images') 2 | const withTypescript = require('@zeit/next-typescript') 3 | 4 | module.exports = ( 5 | withOptimizedImages( 6 | withTypescript({ 7 | target: 'serverless', 8 | distDir: '../dist', 9 | imagesName: '[hash].[ext]', 10 | }) 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /cheatsheet/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/tonyfromundefined/next-starter", 6 | "author": "Tony Won ", 7 | "license": "MIT", 8 | "private": false, 9 | "scripts": { 10 | "dev": "dotenv -e .env.development -- node ./index.js", 11 | "prebuild": "rimraf ./dist", 12 | "build": "next build ./src", 13 | "postbuild": "parallel-webpack", 14 | "deploy:dev": "sls deploy --stage dev", 15 | "deploy:stage": "NODE_ENV=production sls deploy --stage stage", 16 | "deploy:prod": "NODE_ENV=production sls deploy --stage prod", 17 | "lint": "tslint --project '.'", 18 | "pregenerate": "graphql --dotenv .env.development get-schema", 19 | "generate": "graphql-codegen --config codegen.yml" 20 | }, 21 | "dependencies": { 22 | "apollo-cache-inmemory": "^1.6.2", 23 | "apollo-client": "^2.6.2", 24 | "apollo-link-http": "^1.5.14", 25 | "aws-serverless-express": "^3.3.6", 26 | "body-parser": "^1.19.0", 27 | "cookie-parser": "^1.4.4", 28 | "encoding": "^0.1.12", 29 | "express": "^4.17.1", 30 | "graphql": "^14.3.1", 31 | "isomorphic-unfetch": "^3.0.0", 32 | "lodash": "^4.17.11", 33 | "mobx": "^5.10.1", 34 | "mobx-react-lite": "^1.4.0", 35 | "next": "^8.1.0", 36 | "nocache": "^2.1.0", 37 | "react": "^16.8.6", 38 | "react-apollo": "^2.5.6", 39 | "react-apollo-hooks": "^0.4.5", 40 | "react-dom": "^16.8.6" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.4.5", 44 | "@babel/plugin-proposal-class-properties": "^7.4.4", 45 | "@babel/plugin-proposal-decorators": "^7.4.4", 46 | "@graphql-codegen/cli": "1.1.3", 47 | "@graphql-codegen/typescript": "1.1.3", 48 | "@graphql-codegen/typescript-operations": "1.1.3", 49 | "@graphql-codegen/typescript-react-apollo": "1.1.3", 50 | "@types/express-serve-static-core": "^4.16.7", 51 | "@types/lodash": "^4.14.134", 52 | "@types/next": "^8.0.5", 53 | "@types/react": "^16.8.19", 54 | "@types/react-dom": "^16.8.4", 55 | "@zeit/next-typescript": "^1.1.1", 56 | "babel-plugin-module-resolver": "^3.2.0", 57 | "dotenv-cli": "^2.0.0", 58 | "graphql-cli": "^3.0.11", 59 | "imagemin-mozjpeg": "^8.0.0", 60 | "imagemin-optipng": "^7.0.0", 61 | "imagemin-svgo": "^7.0.0", 62 | "next-optimized-images": "^2.5.1", 63 | "parallel-webpack": "^2.4.0", 64 | "rimraf": "^2.6.3", 65 | "serverless": "^1.44.1", 66 | "serverless-apigw-binary": "^0.4.4", 67 | "serverless-dotenv-plugin": "^2.1.1", 68 | "serverless-s3-sync": "^1.8.0", 69 | "tslint": "^5.17.0", 70 | "tslint-react": "^4.0.0", 71 | "typescript": "^3.5.1", 72 | "webpack": "^4.33.0", 73 | "webpack-cli": "^3.3.3" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /cheatsheet/client/serverless.yml: -------------------------------------------------------------------------------- 1 | service: next-starter # 1. Edit service name 2 | 3 | plugins: 4 | - serverless-s3-sync 5 | - serverless-apigw-binary 6 | - serverless-dotenv-plugin 7 | 8 | package: 9 | individually: true 10 | excludeDevDependencies: false 11 | 12 | provider: 13 | name: aws 14 | runtime: nodejs10.x 15 | stage: ${opt:stage, 'dev'} 16 | region: us-east-1 # 2. Edit AWS region name 17 | 18 | custom: 19 | ####################################### 20 | # Unique ID included in resource names. 21 | # Replace it with a random value for every first distribution. 22 | # https://www.random.org/strings/?num=1&len=6&digits=on&loweralpha=on&unique=on&format=html&rnd=new 23 | stackId: '0uelbz' # 3. Update Random Stack ID 24 | ####################################### 25 | 26 | buckets: 27 | ASSETS_BUCKET_NAME: ${self:service}-${self:custom.stackId}-${self:provider.stage}-assets 28 | s3Sync: 29 | - bucketName: ${self:custom.buckets.ASSETS_BUCKET_NAME} 30 | localDir: dist 31 | apigwBinary: 32 | types: 33 | - '*/*' 34 | 35 | functions: 36 | main: 37 | name: ${self:service}-${self:custom.stackId}-${self:provider.stage}-main 38 | handler: dist/serverless/bundles/main.handler 39 | memorySize: 2048 40 | timeout: 10 41 | environment: 42 | NODE_ENV: production 43 | package: 44 | include: 45 | - dist/serverless/bundles/main.js 46 | exclude: 47 | - '**' 48 | events: 49 | - http: 50 | path: / 51 | method: any 52 | - http: 53 | path: /{proxy+} 54 | method: any 55 | - http: 56 | path: /_next/{proxy+} 57 | method: any 58 | integration: http-proxy 59 | request: 60 | uri: https://${self:custom.buckets.ASSETS_BUCKET_NAME}.s3.${self:provider.region}.amazonaws.com/{proxy} 61 | parameters: 62 | paths: 63 | proxy: true 64 | 65 | # 4. If you implement more than 1 entry, add entries. 66 | # hello: 67 | # name: ${self:service}-${self:custom.stackId}-${self:provider.stage}-hello 68 | # handler: dist/serverless/bundles/hello.handler 69 | # memorySize: 2048 70 | # timeout: 10 71 | # environment: 72 | # NODE_ENV: production 73 | # package: 74 | # include: 75 | # - dist/serverless/bundles/hello.js 76 | # exclude: 77 | # - '**' 78 | # events: 79 | # - http: 80 | # path: / 81 | # method: any 82 | # - http: 83 | # path: /{proxy+} 84 | # method: any 85 | 86 | 87 | resources: 88 | Resources: 89 | ClientAssetsBucket: 90 | Type: AWS::S3::Bucket 91 | Properties: 92 | BucketName: ${self:custom.buckets.ASSETS_BUCKET_NAME} 93 | CorsConfiguration: 94 | CorsRules: 95 | - 96 | AllowedOrigins: 97 | - '*' 98 | AllowedHeaders: 99 | - '*' 100 | AllowedMethods: 101 | - GET 102 | - HEAD 103 | - PUT 104 | - POST 105 | - DELETE 106 | MaxAge: 3000 107 | ExposedHeaders: 108 | - x-amz-server-side-encryption 109 | - x-amz-request-id 110 | - x-amz-id-2 111 | 112 | ClientAssetsBucketPolicy: 113 | Type: AWS::S3::BucketPolicy 114 | Properties: 115 | Bucket: 116 | Ref: ClientAssetsBucket 117 | PolicyDocument: 118 | Version: '2012-10-17' 119 | Statement: [ 120 | { 121 | Action: ['s3:GetObject'], 122 | Effect: 'Allow', 123 | Resource: { 124 | Fn::Join: ['', ['arn:aws:s3:::', { Ref: 'ClientAssetsBucket' }, '/*']], 125 | }, 126 | Principal: '*' 127 | }, 128 | ] 129 | -------------------------------------------------------------------------------- /cheatsheet/client/src/apollo/index.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryCache, NormalizedCacheObject } from 'apollo-cache-inmemory' 2 | import { ApolloClient } from 'apollo-client' 3 | import { createHttpLink } from 'apollo-link-http' 4 | import fetch from 'isomorphic-unfetch' 5 | import { Store } from '../store' 6 | 7 | let apolloClient: ApolloClient 8 | 9 | const isServer = typeof window === 'undefined' 10 | 11 | export function createApolloClient(store: Store, state?: any) { 12 | if (apolloClient) { 13 | return apolloClient 14 | 15 | } else { 16 | const link = createHttpLink({ 17 | fetch, 18 | uri: store.environments.NEXT_APP_GRAPHQL_ENDPOINT, 19 | }) 20 | 21 | const cache = new InMemoryCache() 22 | 23 | cache.restore(state || {}) 24 | 25 | if (isServer) { 26 | return new ApolloClient({ 27 | cache, 28 | link, 29 | ssrMode: true, 30 | }) 31 | 32 | } else { 33 | return apolloClient = new ApolloClient({ 34 | cache, 35 | connectToDevTools: true, 36 | link, 37 | }) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cheatsheet/client/src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/cheatsheet/client/src/assets/favicon.png -------------------------------------------------------------------------------- /cheatsheet/client/src/generated/graphql.tsx: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | /** All built-in and custom scalars, mapped to their actual values */ 3 | export type Scalars = { 4 | ID: string; 5 | String: string; 6 | Boolean: boolean; 7 | Int: number; 8 | Float: number; 9 | }; 10 | 11 | export type IMutation = { 12 | ping: Scalars["String"]; 13 | createTask: ITask; 14 | updateTask?: Maybe; 15 | deleteTask?: Maybe; 16 | }; 17 | 18 | export type IMutationCreateTaskArgs = { 19 | data: ITaskCreateInput; 20 | }; 21 | 22 | export type IMutationUpdateTaskArgs = { 23 | data: ITaskUpdateInput; 24 | where: ITaskWhereUniqueInput; 25 | }; 26 | 27 | export type IMutationDeleteTaskArgs = { 28 | where: ITaskWhereUniqueInput; 29 | }; 30 | 31 | export type IQuery = { 32 | stage: Scalars["String"]; 33 | task?: Maybe; 34 | tasks: Array; 35 | }; 36 | 37 | export type IQueryTaskArgs = { 38 | where: ITaskWhereUniqueInput; 39 | }; 40 | 41 | export type IQueryTasksArgs = { 42 | where?: Maybe; 43 | orderBy?: Maybe; 44 | skip?: Maybe; 45 | after?: Maybe; 46 | before?: Maybe; 47 | first?: Maybe; 48 | last?: Maybe; 49 | }; 50 | 51 | export type ITask = { 52 | id: Scalars["ID"]; 53 | content: Scalars["String"]; 54 | isDone: Scalars["Boolean"]; 55 | }; 56 | 57 | export type ITaskCreateInput = { 58 | id?: Maybe; 59 | content: Scalars["String"]; 60 | isDone: Scalars["Boolean"]; 61 | }; 62 | 63 | export enum ITaskOrderByInput { 64 | IdAsc = "id_ASC", 65 | IdDesc = "id_DESC", 66 | ContentAsc = "content_ASC", 67 | ContentDesc = "content_DESC", 68 | IsDoneAsc = "isDone_ASC", 69 | IsDoneDesc = "isDone_DESC", 70 | CreatedAtAsc = "createdAt_ASC", 71 | CreatedAtDesc = "createdAt_DESC", 72 | UpdatedAtAsc = "updatedAt_ASC", 73 | UpdatedAtDesc = "updatedAt_DESC" 74 | } 75 | 76 | export type ITaskUpdateInput = { 77 | content?: Maybe; 78 | isDone?: Maybe; 79 | }; 80 | 81 | export type ITaskWhereInput = { 82 | id?: Maybe; 83 | id_not?: Maybe; 84 | id_in?: Maybe>; 85 | id_not_in?: Maybe>; 86 | id_lt?: Maybe; 87 | id_lte?: Maybe; 88 | id_gt?: Maybe; 89 | id_gte?: Maybe; 90 | id_contains?: Maybe; 91 | id_not_contains?: Maybe; 92 | id_starts_with?: Maybe; 93 | id_not_starts_with?: Maybe; 94 | id_ends_with?: Maybe; 95 | id_not_ends_with?: Maybe; 96 | content?: Maybe; 97 | content_not?: Maybe; 98 | content_in?: Maybe>; 99 | content_not_in?: Maybe>; 100 | content_lt?: Maybe; 101 | content_lte?: Maybe; 102 | content_gt?: Maybe; 103 | content_gte?: Maybe; 104 | content_contains?: Maybe; 105 | content_not_contains?: Maybe; 106 | content_starts_with?: Maybe; 107 | content_not_starts_with?: Maybe; 108 | content_ends_with?: Maybe; 109 | content_not_ends_with?: Maybe; 110 | isDone?: Maybe; 111 | isDone_not?: Maybe; 112 | AND?: Maybe>; 113 | OR?: Maybe>; 114 | NOT?: Maybe>; 115 | }; 116 | 117 | export type ITaskWhereUniqueInput = { 118 | id?: Maybe; 119 | }; 120 | export type ICreateTaskMutationVariables = { 121 | data: ITaskCreateInput; 122 | }; 123 | 124 | export type ICreateTaskMutation = { __typename?: "Mutation" } & { 125 | createTask: { __typename?: "Task" } & Pick; 126 | }; 127 | 128 | export type IDeleteTaskMutationVariables = { 129 | where: ITaskWhereUniqueInput; 130 | }; 131 | 132 | export type IDeleteTaskMutation = { __typename?: "Mutation" } & { 133 | deleteTask: Maybe<{ __typename?: "Task" } & Pick>; 134 | }; 135 | 136 | export type IGetTasksQueryVariables = {}; 137 | 138 | export type IGetTasksQuery = { __typename?: "Query" } & { 139 | tasks: Array< 140 | { __typename?: "Task" } & Pick 141 | >; 142 | }; 143 | 144 | export type IUpdateTaskMutationVariables = { 145 | data: ITaskUpdateInput; 146 | where: ITaskWhereUniqueInput; 147 | }; 148 | 149 | export type IUpdateTaskMutation = { __typename?: "Mutation" } & { 150 | updateTask: Maybe<{ __typename?: "Task" } & Pick>; 151 | }; 152 | 153 | import gql from "graphql-tag"; 154 | import * as React from "react"; 155 | import * as ReactApollo from "react-apollo"; 156 | import * as ReactApolloHooks from "react-apollo-hooks"; 157 | export type Omit = Pick>; 158 | 159 | export const CreateTaskDocument = gql` 160 | mutation createTask($data: TaskCreateInput!) { 161 | createTask(data: $data) { 162 | id 163 | } 164 | } 165 | `; 166 | export type ICreateTaskMutationFn = ReactApollo.MutationFn< 167 | ICreateTaskMutation, 168 | ICreateTaskMutationVariables 169 | >; 170 | 171 | export const CreateTaskComponent = ( 172 | props: Omit< 173 | Omit< 174 | ReactApollo.MutationProps< 175 | ICreateTaskMutation, 176 | ICreateTaskMutationVariables 177 | >, 178 | "mutation" 179 | >, 180 | "variables" 181 | > & { variables?: ICreateTaskMutationVariables } 182 | ) => ( 183 | 184 | mutation={CreateTaskDocument} 185 | {...props} 186 | /> 187 | ); 188 | 189 | export type ICreateTaskProps = Partial< 190 | ReactApollo.MutateProps 191 | > & 192 | TChildProps; 193 | export function withCreateTask( 194 | operationOptions?: ReactApollo.OperationOption< 195 | TProps, 196 | ICreateTaskMutation, 197 | ICreateTaskMutationVariables, 198 | ICreateTaskProps 199 | > 200 | ) { 201 | return ReactApollo.withMutation< 202 | TProps, 203 | ICreateTaskMutation, 204 | ICreateTaskMutationVariables, 205 | ICreateTaskProps 206 | >(CreateTaskDocument, { 207 | alias: "withCreateTask", 208 | ...operationOptions 209 | }); 210 | } 211 | 212 | export function useCreateTaskMutation( 213 | baseOptions?: ReactApolloHooks.MutationHookOptions< 214 | ICreateTaskMutation, 215 | ICreateTaskMutationVariables 216 | > 217 | ) { 218 | return ReactApolloHooks.useMutation< 219 | ICreateTaskMutation, 220 | ICreateTaskMutationVariables 221 | >(CreateTaskDocument, baseOptions); 222 | } 223 | export const DeleteTaskDocument = gql` 224 | mutation deleteTask($where: TaskWhereUniqueInput!) { 225 | deleteTask(where: $where) { 226 | id 227 | } 228 | } 229 | `; 230 | export type IDeleteTaskMutationFn = ReactApollo.MutationFn< 231 | IDeleteTaskMutation, 232 | IDeleteTaskMutationVariables 233 | >; 234 | 235 | export const DeleteTaskComponent = ( 236 | props: Omit< 237 | Omit< 238 | ReactApollo.MutationProps< 239 | IDeleteTaskMutation, 240 | IDeleteTaskMutationVariables 241 | >, 242 | "mutation" 243 | >, 244 | "variables" 245 | > & { variables?: IDeleteTaskMutationVariables } 246 | ) => ( 247 | 248 | mutation={DeleteTaskDocument} 249 | {...props} 250 | /> 251 | ); 252 | 253 | export type IDeleteTaskProps = Partial< 254 | ReactApollo.MutateProps 255 | > & 256 | TChildProps; 257 | export function withDeleteTask( 258 | operationOptions?: ReactApollo.OperationOption< 259 | TProps, 260 | IDeleteTaskMutation, 261 | IDeleteTaskMutationVariables, 262 | IDeleteTaskProps 263 | > 264 | ) { 265 | return ReactApollo.withMutation< 266 | TProps, 267 | IDeleteTaskMutation, 268 | IDeleteTaskMutationVariables, 269 | IDeleteTaskProps 270 | >(DeleteTaskDocument, { 271 | alias: "withDeleteTask", 272 | ...operationOptions 273 | }); 274 | } 275 | 276 | export function useDeleteTaskMutation( 277 | baseOptions?: ReactApolloHooks.MutationHookOptions< 278 | IDeleteTaskMutation, 279 | IDeleteTaskMutationVariables 280 | > 281 | ) { 282 | return ReactApolloHooks.useMutation< 283 | IDeleteTaskMutation, 284 | IDeleteTaskMutationVariables 285 | >(DeleteTaskDocument, baseOptions); 286 | } 287 | export const GetTasksDocument = gql` 288 | query getTasks { 289 | tasks { 290 | id 291 | content 292 | isDone 293 | } 294 | } 295 | `; 296 | 297 | export const GetTasksComponent = ( 298 | props: Omit< 299 | Omit< 300 | ReactApollo.QueryProps, 301 | "query" 302 | >, 303 | "variables" 304 | > & { variables?: IGetTasksQueryVariables } 305 | ) => ( 306 | 307 | query={GetTasksDocument} 308 | {...props} 309 | /> 310 | ); 311 | 312 | export type IGetTasksProps = Partial< 313 | ReactApollo.DataProps 314 | > & 315 | TChildProps; 316 | export function withGetTasks( 317 | operationOptions?: ReactApollo.OperationOption< 318 | TProps, 319 | IGetTasksQuery, 320 | IGetTasksQueryVariables, 321 | IGetTasksProps 322 | > 323 | ) { 324 | return ReactApollo.withQuery< 325 | TProps, 326 | IGetTasksQuery, 327 | IGetTasksQueryVariables, 328 | IGetTasksProps 329 | >(GetTasksDocument, { 330 | alias: "withGetTasks", 331 | ...operationOptions 332 | }); 333 | } 334 | 335 | export function useGetTasksQuery( 336 | baseOptions?: ReactApolloHooks.QueryHookOptions 337 | ) { 338 | return ReactApolloHooks.useQuery( 339 | GetTasksDocument, 340 | baseOptions 341 | ); 342 | } 343 | export const UpdateTaskDocument = gql` 344 | mutation updateTask($data: TaskUpdateInput!, $where: TaskWhereUniqueInput!) { 345 | updateTask(data: $data, where: $where) { 346 | id 347 | } 348 | } 349 | `; 350 | export type IUpdateTaskMutationFn = ReactApollo.MutationFn< 351 | IUpdateTaskMutation, 352 | IUpdateTaskMutationVariables 353 | >; 354 | 355 | export const UpdateTaskComponent = ( 356 | props: Omit< 357 | Omit< 358 | ReactApollo.MutationProps< 359 | IUpdateTaskMutation, 360 | IUpdateTaskMutationVariables 361 | >, 362 | "mutation" 363 | >, 364 | "variables" 365 | > & { variables?: IUpdateTaskMutationVariables } 366 | ) => ( 367 | 368 | mutation={UpdateTaskDocument} 369 | {...props} 370 | /> 371 | ); 372 | 373 | export type IUpdateTaskProps = Partial< 374 | ReactApollo.MutateProps 375 | > & 376 | TChildProps; 377 | export function withUpdateTask( 378 | operationOptions?: ReactApollo.OperationOption< 379 | TProps, 380 | IUpdateTaskMutation, 381 | IUpdateTaskMutationVariables, 382 | IUpdateTaskProps 383 | > 384 | ) { 385 | return ReactApollo.withMutation< 386 | TProps, 387 | IUpdateTaskMutation, 388 | IUpdateTaskMutationVariables, 389 | IUpdateTaskProps 390 | >(UpdateTaskDocument, { 391 | alias: "withUpdateTask", 392 | ...operationOptions 393 | }); 394 | } 395 | 396 | export function useUpdateTaskMutation( 397 | baseOptions?: ReactApolloHooks.MutationHookOptions< 398 | IUpdateTaskMutation, 399 | IUpdateTaskMutationVariables 400 | > 401 | ) { 402 | return ReactApolloHooks.useMutation< 403 | IUpdateTaskMutation, 404 | IUpdateTaskMutationVariables 405 | >(UpdateTaskDocument, baseOptions); 406 | } 407 | -------------------------------------------------------------------------------- /cheatsheet/client/src/generated/schema.graphql: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | ping: String! 3 | createTask(data: TaskCreateInput!): Task! 4 | updateTask(data: TaskUpdateInput!, where: TaskWhereUniqueInput!): Task 5 | deleteTask(where: TaskWhereUniqueInput!): Task 6 | } 7 | 8 | type Query { 9 | stage: String! 10 | task(where: TaskWhereUniqueInput!): Task 11 | tasks(where: TaskWhereInput, orderBy: TaskOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Task!]! 12 | } 13 | 14 | type Task { 15 | id: ID! 16 | content: String! 17 | isDone: Boolean! 18 | } 19 | 20 | input TaskCreateInput { 21 | id: ID 22 | content: String! 23 | isDone: Boolean! 24 | } 25 | 26 | enum TaskOrderByInput { 27 | id_ASC 28 | id_DESC 29 | content_ASC 30 | content_DESC 31 | isDone_ASC 32 | isDone_DESC 33 | createdAt_ASC 34 | createdAt_DESC 35 | updatedAt_ASC 36 | updatedAt_DESC 37 | } 38 | 39 | input TaskUpdateInput { 40 | content: String 41 | isDone: Boolean 42 | } 43 | 44 | input TaskWhereInput { 45 | id: ID 46 | id_not: ID 47 | id_in: [ID!] 48 | id_not_in: [ID!] 49 | id_lt: ID 50 | id_lte: ID 51 | id_gt: ID 52 | id_gte: ID 53 | id_contains: ID 54 | id_not_contains: ID 55 | id_starts_with: ID 56 | id_not_starts_with: ID 57 | id_ends_with: ID 58 | id_not_ends_with: ID 59 | content: String 60 | content_not: String 61 | content_in: [String!] 62 | content_not_in: [String!] 63 | content_lt: String 64 | content_lte: String 65 | content_gt: String 66 | content_gte: String 67 | content_contains: String 68 | content_not_contains: String 69 | content_starts_with: String 70 | content_not_starts_with: String 71 | content_ends_with: String 72 | content_not_ends_with: String 73 | isDone: Boolean 74 | isDone_not: Boolean 75 | AND: [TaskWhereInput!] 76 | OR: [TaskWhereInput!] 77 | NOT: [TaskWhereInput!] 78 | } 79 | 80 | input TaskWhereUniqueInput { 81 | id: ID 82 | } 83 | 84 | -------------------------------------------------------------------------------- /cheatsheet/client/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import ApolloClient from 'apollo-client' 2 | import pickBy from 'lodash/pickBy' 3 | import { Container, default as NextApp } from 'next/app' 4 | import Head from 'next/head' 5 | import React, { Fragment } from 'react' 6 | import { ApolloProvider as ApolloHookProvider, getMarkupFromTree } from 'react-apollo-hooks' 7 | import { renderToString as renderFunction } from 'react-dom/server' 8 | import { createApolloClient } from '~/apollo' 9 | import FaviconImage from '~/assets/favicon.png?url' 10 | import { createStore, IEnvironments, Store, StoreProvider } from '~/store' 11 | 12 | const isServer = typeof window === 'undefined' 13 | 14 | const environments = isServer ? extractNextEnvironments(process.env) : undefined 15 | 16 | export default class extends React.Component { 17 | static async getInitialProps(appContext: any) { 18 | const appProps = await App.getInitialProps(appContext) 19 | 20 | const { Component, router } = appContext 21 | 22 | const store = createStore({ 23 | environments, 24 | }) 25 | 26 | const apollo = createApolloClient(store) 27 | 28 | appContext.ctx.store = store 29 | 30 | if (isServer) { 31 | try { 32 | await Promise.all([ 33 | store.nextServerInit(appContext.ctx.req, appContext.ctx.res), 34 | getMarkupFromTree({ 35 | tree: ( 36 | 43 | ), 44 | renderFunction, 45 | }), 46 | ]) 47 | 48 | } catch (error) { 49 | // tslint:disable-next-line:no-console 50 | console.error('[Error 29948] Pre-operation required for SSR failed') 51 | } 52 | 53 | Head.rewind() 54 | } 55 | 56 | return { 57 | apolloState: apollo.cache.extract(), 58 | store, 59 | ...appProps, 60 | } 61 | } 62 | 63 | apolloClient: ApolloClient 64 | store: Store 65 | 66 | constructor(props: any) { 67 | super(props) 68 | this.store = createStore(props.store) 69 | this.apolloClient = createApolloClient(this.store, props.apolloState) 70 | } 71 | 72 | render() { 73 | return ( 74 | 79 | ) 80 | } 81 | } 82 | 83 | class App extends NextApp { 84 | render() { 85 | const { Component, pageProps } = this.props 86 | 87 | return ( 88 | 89 | 90 | Hello, AWSKRUG 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ) 102 | } 103 | } 104 | 105 | function extractNextEnvironments(environments: IEnvironments): IEnvironments { 106 | return pickBy(environments, (_value, key) => key.indexOf('NEXT_APP') !== -1) 107 | } 108 | -------------------------------------------------------------------------------- /cheatsheet/client/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Head, Main, NextScript } from 'next/document' 2 | 3 | export default class extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cheatsheet/client/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { 3 | IGetTasksQuery, 4 | useCreateTaskMutation, 5 | useDeleteTaskMutation, 6 | useGetTasksQuery, 7 | useUpdateTaskMutation, 8 | } from '~/generated/graphql' 9 | 10 | export default function PageIndex() { 11 | const [content, setContent] = useState('') 12 | 13 | const { error, loading, data, refetch } = useGetTasksQuery() 14 | const createTask = useCreateTaskMutation() 15 | 16 | const onInputChange = (event: React.ChangeEvent) => { 17 | setContent(event.target.value) 18 | } 19 | 20 | const onAddButtonClick = async () => { 21 | await createTask({ 22 | variables: { 23 | data: { 24 | content, 25 | isDone: false, 26 | }, 27 | }, 28 | }) 29 | await refetch() 30 | } 31 | 32 | return ( 33 |
34 |

📝 할 일 목록

35 |
36 | 37 | 38 |
39 | 45 |
46 | AWSKRUG 슬랙 가입하기 😎: https://slack.awskr.org 47 |
48 |
49 | ) 50 | } 51 | 52 | interface ITasksProps { 53 | error: any 54 | loading: boolean 55 | data?: IGetTasksQuery 56 | refetch: () => any 57 | } 58 | function Tasks(props: ITasksProps) { 59 | const updateTask = useUpdateTaskMutation() 60 | const deleteTask = useDeleteTaskMutation() 61 | 62 | if (props.error) { 63 | return ( 64 |
error
65 | ) 66 | } 67 | 68 | if (props.loading || !props.data || !props.data.tasks) { 69 | return ( 70 |
loading...
71 | ) 72 | } 73 | 74 | return ( 75 |
    76 | {props.data.tasks.map((task) => { 77 | const onCompleteButtonClick = async () => { 78 | await updateTask({ 79 | variables: { 80 | data: { 81 | isDone: true, 82 | }, 83 | where: { 84 | id: task.id, 85 | }, 86 | }, 87 | }) 88 | await props.refetch() 89 | } 90 | 91 | const onDeleteButtonClick = async () => { 92 | await deleteTask({ 93 | variables: { 94 | where: { 95 | id: task.id, 96 | }, 97 | }, 98 | }) 99 | await props.refetch() 100 | } 101 | 102 | return ( 103 |
  • 104 | {task.id} 105 | {'\u00A0'}|{'\u00A0'} 106 | {task.content} 107 | {task.isDone && {'\u00A0'}✔} 108 | {'\u00A0'} 109 | 110 | 111 |
  • 112 | ) 113 | })} 114 |
115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /cheatsheet/client/src/queries/createTask.graphql: -------------------------------------------------------------------------------- 1 | mutation createTask($data: TaskCreateInput!) { 2 | createTask(data: $data) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /cheatsheet/client/src/queries/deleteTask.graphql: -------------------------------------------------------------------------------- 1 | mutation deleteTask($where: TaskWhereUniqueInput!) { 2 | deleteTask(where: $where) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /cheatsheet/client/src/queries/getTasks.graphql: -------------------------------------------------------------------------------- 1 | query getTasks { 2 | tasks { 3 | id 4 | content 5 | isDone 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cheatsheet/client/src/queries/updateTask.graphql: -------------------------------------------------------------------------------- 1 | mutation updateTask($data: TaskUpdateInput!, $where: TaskWhereUniqueInput!) { 2 | updateTask(data: $data, where: $where) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /cheatsheet/client/src/serverless/main.js: -------------------------------------------------------------------------------- 1 | const awsServerlessExpress = require('aws-serverless-express') 2 | const bodyParser = require('body-parser') 3 | const cookieParser = require('cookie-parser') 4 | const express = require('express') 5 | const nocache = require('nocache') 6 | 7 | const index = require('../../dist/serverless/pages/index') 8 | const error = require('../../dist/serverless/pages/_error') 9 | 10 | const BINARY_MIME_TYPES = [ 11 | 'application/javascript', 12 | 'application/json', 13 | 'application/octet-stream', 14 | 'application/xml', 15 | 'text/css', 16 | 'text/html', 17 | 'text/javascript', 18 | 'text/plain', 19 | 'text/text', 20 | 'text/xml' 21 | ] 22 | 23 | const app = express() 24 | 25 | app.use(nocache()) 26 | app.use(bodyParser.json()) 27 | app.use(bodyParser.urlencoded({ extended: true })) 28 | app.use(cookieParser()) 29 | 30 | app.get('/', index.render) 31 | app.get('/_error', error.render) 32 | 33 | const server = awsServerlessExpress.createServer(app, null, BINARY_MIME_TYPES) 34 | 35 | exports.handler = (event, context) => { 36 | return awsServerlessExpress.proxy(server, event, context) 37 | } 38 | -------------------------------------------------------------------------------- /cheatsheet/client/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express-serve-static-core' 2 | import { action, observable } from 'mobx' 3 | import { useStaticRendering } from 'mobx-react-lite' 4 | import { createContext, useContext } from 'react' 5 | 6 | const isServer = typeof window === 'undefined' 7 | 8 | let store: Store 9 | 10 | useStaticRendering(isServer) 11 | 12 | export interface IEnvironments { 13 | [key: string]: string | undefined 14 | } 15 | 16 | export function createStore(storeState?: Partial) { 17 | switch (true) { 18 | case isServer: 19 | return new Store(storeState) 20 | 21 | case typeof store !== 'undefined': 22 | return store 23 | 24 | default: 25 | return store = new Store(storeState) 26 | } 27 | } 28 | 29 | export class Store { 30 | @observable 31 | environments: IEnvironments 32 | 33 | constructor(storeState: Partial = {}) { 34 | this.environments = storeState.environments || {} 35 | } 36 | 37 | /** 38 | * Store Hydration 39 | * @param req Request 40 | * @param res Response 41 | */ 42 | @action 43 | async nextServerInit(req: Request, res: Response) { 44 | if (!req || !res) { 45 | return 46 | } 47 | } 48 | } 49 | 50 | export const StoreContext = createContext({} as Store) 51 | export const StoreProvider = StoreContext.Provider 52 | export const useStore = () => useContext(StoreContext) 53 | -------------------------------------------------------------------------------- /cheatsheet/client/src/types/graphql.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.graphql' { 2 | import { DocumentNode } from 'graphql' 3 | const content: DocumentNode 4 | export default content 5 | } 6 | -------------------------------------------------------------------------------- /cheatsheet/client/src/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg' { 2 | const url: string 3 | export default url 4 | } 5 | 6 | declare module '*.svg' { 7 | const url: string 8 | export default url 9 | } 10 | 11 | declare module '*.png' { 12 | const url: string 13 | export default url 14 | } 15 | 16 | declare module '*.jpg?include' { 17 | const url: string 18 | export default url 19 | } 20 | 21 | declare module '*.svg?include' { 22 | const url: string 23 | export default url 24 | } 25 | 26 | declare module '*.png?include' { 27 | const url: string 28 | export default url 29 | } 30 | 31 | declare module '*.jpg?webp' { 32 | const url: string 33 | export default url 34 | } 35 | 36 | declare module '*.svg?webp' { 37 | const url: string 38 | export default url 39 | } 40 | 41 | declare module '*.png?webp' { 42 | const url: string 43 | export default url 44 | } 45 | 46 | declare module '*.jpg?inline' { 47 | const url: string 48 | export default url 49 | } 50 | 51 | declare module '*.svg?inline' { 52 | const url: string 53 | export default url 54 | } 55 | 56 | declare module '*.png?inline' { 57 | const url: string 58 | export default url 59 | } 60 | 61 | declare module '*.jpg?url' { 62 | const url: string 63 | export default url 64 | } 65 | 66 | declare module '*.svg?url' { 67 | const url: string 68 | export default url 69 | } 70 | 71 | declare module '*.png?url' { 72 | const url: string 73 | export default url 74 | } 75 | -------------------------------------------------------------------------------- /cheatsheet/client/src/types/vendors.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'open-color' { 2 | interface IColors { 3 | 0: string 4 | 1: string 5 | 2: string 6 | 3: string 7 | 4: string 8 | 5: string 9 | 6: string 10 | 7: string 11 | 8: string 12 | 9: string 13 | } 14 | export const black: string 15 | export const blue: IColors 16 | export const cyan: IColors 17 | export const grape: IColors 18 | export const gray: IColors 19 | export const green: IColors 20 | export const indigo: IColors 21 | export const lime: IColors 22 | export const orange: IColors 23 | export const pink: IColors 24 | export const red: IColors 25 | export const teal: IColors 26 | export const violet: IColors 27 | export const white: string 28 | export const yellow: IColors 29 | } 30 | -------------------------------------------------------------------------------- /cheatsheet/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es6", 5 | "lib": [ 6 | "esnext", 7 | "dom", 8 | "dom.iterable", 9 | "scripthost", 10 | "esnext.asynciterable" 11 | ], 12 | "module": "es6", 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "jsx": "preserve", 16 | "allowSyntheticDefaultImports": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "removeComments": false, 20 | "preserveConstEnums": true, 21 | "sourceMap": true, 22 | "skipLibCheck": true, 23 | "esModuleInterop": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "baseUrl": ".", 27 | "paths": { 28 | "~~/*": ["./src/services/*"], 29 | "~/*": ["./src/*"] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cheatsheet/client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-react" 6 | ], 7 | "linterOptions": { 8 | "exclude": [ 9 | "node_modules/**/*.ts", 10 | "dist/**/*.js", 11 | "src/generated/graphql.tsx" 12 | ] 13 | }, 14 | "rules": { 15 | "semicolon": [true, "never"], 16 | "quotemark": [true, "single"], 17 | "indent": [true, "spaces", 2], 18 | "object-literal-sort-keys": false, 19 | "no-shadowed-variable": false, 20 | "max-line-length": false, 21 | "prefer-for-of": false, 22 | "variable-name": false, 23 | "no-empty": false, 24 | "jsx-boolean-value": false, 25 | "no-console": [true, "log"], 26 | "member-access": false, 27 | "max-classes-per-file": false, 28 | "jsx-no-multiline-js": false, 29 | "member-ordering": false, 30 | "no-var-requires": false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cheatsheet/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const isProd = process.env.NODE_ENV === 'production' 5 | 6 | const filenames = fs.readdirSync(path.resolve(__dirname, './src/serverless')) 7 | 8 | const entries = [] 9 | 10 | for (const filename of filenames) { 11 | entries.push({ 12 | [filename.split('.')[0]]: path.resolve(__dirname, './src/serverless', filename), 13 | }) 14 | } 15 | 16 | module.exports = entries.map((entry) => ({ 17 | mode: isProd ? 'production' : 'development', 18 | entry, 19 | output: { 20 | path: path.resolve(__dirname, './dist/serverless/bundles'), 21 | filename: `[name].js`, 22 | libraryTarget: 'commonjs', 23 | }, 24 | target: 'node', 25 | optimization: { 26 | minimize: false, 27 | }, 28 | stats: 'errors-only', 29 | })) 30 | -------------------------------------------------------------------------------- /cheatsheet/server/.env.development: -------------------------------------------------------------------------------- 1 | STAGE="development" 2 | IS_PLAYGROUND_ENABLED="1" 3 | IS_TRACING_ENABLED="1" 4 | 5 | PRISMA_ENDPOINT="http://prism-loadb-6rq7nvnnj8r6-2132101941.ap-northeast-2.elb.amazonaws.com/serverless/dev" 6 | PRISMA_MANAGEMENT_API_SECRET="serverless" 7 | -------------------------------------------------------------------------------- /cheatsheet/server/.env.example: -------------------------------------------------------------------------------- 1 | STAGE="example" 2 | IS_PLAYGROUND_ENABLED="1" 3 | IS_TRACING_ENABLED="1" 4 | 5 | PRISMA_ENDPOINT="{endpoint}/{service}/{stage}" 6 | PRISMA_MANAGEMENT_API_SECRET="{managementApiSecret}" 7 | -------------------------------------------------------------------------------- /cheatsheet/server/.env.production: -------------------------------------------------------------------------------- 1 | STAGE="production" 2 | IS_PLAYGROUND_ENABLED="1" 3 | IS_TRACING_ENABLED="1" 4 | 5 | PRISMA_ENDPOINT="http://prism-loadb-6rq7nvnnj8r6-2132101941.ap-northeast-2.elb.amazonaws.com/serverless/prod" 6 | PRISMA_MANAGEMENT_API_SECRET="serverless" 7 | -------------------------------------------------------------------------------- /cheatsheet/server/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /cheatsheet/server/datamodel.prisma: -------------------------------------------------------------------------------- 1 | type Task { 2 | id: ID! @id 3 | content: String! 4 | isDone: Boolean! 5 | } 6 | -------------------------------------------------------------------------------- /cheatsheet/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-graphql-workshop-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/serverless-graphql-workshop", 6 | "author": "tonyfromundefined", 7 | "license": "MIT", 8 | "private": false, 9 | "scripts": { 10 | "predev": "rimraf ./dist", 11 | "dev": "dotenv -e ./.env.development -- ts-node-dev --inspect -- ./src/server.ts", 12 | "prebuild": "rimraf ./dist", 13 | "build": "webpack", 14 | "start": "dotenv -e ./.env.production -- node ./dist/server.js", 15 | "deploy:dev": "sls deploy --stage dev", 16 | "deploy:prod": "NODE_ENV=production sls deploy --stage prod", 17 | "undeploy:dev": "sls remove --stage dev", 18 | "undeploy:prod": "sls remove --stage prod", 19 | "prisma:deploy:dev": "prisma deploy -e .env.development", 20 | "prisma:deploy:prod": "prisma deploy -e .env.production", 21 | "prisma:token:dev": "prisma token -e .env.development", 22 | "prisma:token:prod": "prisma token -e .env.production", 23 | "prisma:admin:dev": "prisma admin -e .env.development", 24 | "prisma:admin:prod": "prisma admin -e .env.production" 25 | }, 26 | "dependencies": { 27 | "apollo-server-express": "^2.6.3", 28 | "aws-serverless-express": "^3.3.6", 29 | "cors": "^2.8.5", 30 | "express": "^4.17.1", 31 | "nexus": "^0.12.0-beta.6", 32 | "nexus-prisma": "^0.3.7", 33 | "prisma-client-lib": "^1.34.0", 34 | "short-uuid": "^3.1.1" 35 | }, 36 | "devDependencies": { 37 | "@types/aws-serverless-express": "^3.3.1", 38 | "@types/dotenv": "^6.1.1", 39 | "@types/graphql": "^14.2.1", 40 | "dotenv-cli": "^2.0.0", 41 | "fork-ts-checker-webpack-plugin": "^1.3.7", 42 | "nexus-prisma-generate": "^0.3.7", 43 | "rimraf": "^2.6.3", 44 | "serverless": "^1.45.1", 45 | "serverless-apigw-binary": "^0.4.4", 46 | "serverless-dotenv-plugin": "^2.1.1", 47 | "ts-loader": "^6.0.2", 48 | "ts-node-dev": "^1.0.0-pre.40", 49 | "tslint": "^5.17.0", 50 | "typescript": "3.4", 51 | "webpack": "^4.34.0", 52 | "webpack-cli": "^3.3.4", 53 | "webpackbar": "^3.2.0" 54 | }, 55 | "resolutions": { 56 | "nexus-prisma-generate/**/graphql": "0.13.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cheatsheet/server/prisma.yml: -------------------------------------------------------------------------------- 1 | endpoint: ${env:PRISMA_ENDPOINT} 2 | datamodel: datamodel.prisma 3 | secret: 03ef7cac 4 | 5 | generate: 6 | - generator: typescript-client 7 | output: ./src/generated/prisma/ 8 | 9 | hooks: 10 | post-deploy: 11 | - nexus-prisma-generate --output ./src/generated/nexus-prisma 12 | -------------------------------------------------------------------------------- /cheatsheet/server/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-graphql-workshop 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs8.10 6 | stage: ${opt:stage, 'dev'} 7 | region: ap-northeast-1 8 | profile: SERVERLESS_WORKSHOP 9 | 10 | package: 11 | individually: true 12 | excludeDevDependencies: false 13 | 14 | functions: 15 | main: 16 | name: ${self:service}-${self:provider.stage} 17 | handler: dist/serverless.handler 18 | memorySize: 1024 19 | timeout: 10 20 | environment: 21 | NODE_ENV: production 22 | package: 23 | include: 24 | - dist/serverless.js 25 | exclude: 26 | - '**' 27 | events: 28 | - http: 29 | path: / 30 | method: any 31 | - http: 32 | path: /{proxy+} 33 | method: any 34 | 35 | plugins: 36 | - serverless-apigw-binary 37 | - serverless-dotenv-plugin 38 | 39 | custom: 40 | apigwBinary: 41 | types: 42 | - '*/*' 43 | -------------------------------------------------------------------------------- /cheatsheet/server/src/app.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server-express' 2 | import cors from 'cors' 3 | import express from 'express' 4 | import { makePrismaSchema } from 'nexus-prisma' 5 | import path from 'path' 6 | import datamodelInfo from './generated/nexus-prisma' 7 | import { prisma } from './generated/prisma' 8 | 9 | import * as types from './resolvers' 10 | 11 | const playground = !!Number(process.env.IS_PLAYGROUND_ENABLED || '0') 12 | const tracing = !!Number(process.env.IS_TRACING_ENABLED || '0') 13 | 14 | const app = express() 15 | 16 | app.use(cors()) 17 | 18 | app.get('/', (_req, res) => { 19 | return res.json('ok') 20 | }) 21 | 22 | const server = new ApolloServer({ 23 | schema: makePrismaSchema({ 24 | types, 25 | prisma: { 26 | client: prisma, 27 | datamodelInfo, 28 | }, 29 | outputs: { 30 | schema: path.resolve('./src/generated', 'schema.graphql'), 31 | typegen: path.resolve('./src/generated', 'nexus.ts'), 32 | }, 33 | }), 34 | introspection: playground, 35 | playground, 36 | tracing, 37 | }) 38 | 39 | server.applyMiddleware({ 40 | app, 41 | }) 42 | 43 | export default app 44 | -------------------------------------------------------------------------------- /cheatsheet/server/src/generated/nexus-prisma/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by nexus-prisma@0.3.7 3 | * Do not make changes to this file directly 4 | */ 5 | 6 | export { default } from './datamodel-info' 7 | 8 | -------------------------------------------------------------------------------- /cheatsheet/server/src/generated/nexus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by GraphQL Nexus 3 | * Do not make changes to this file directly 4 | */ 5 | 6 | 7 | 8 | 9 | 10 | 11 | declare global { 12 | interface NexusGen extends NexusGenTypes {} 13 | } 14 | 15 | export interface NexusGenInputs { 16 | TaskCreateInput: { // input type 17 | content: string; // String! 18 | id?: string | null; // ID 19 | isDone: boolean; // Boolean! 20 | } 21 | TaskUpdateInput: { // input type 22 | content?: string | null; // String 23 | isDone?: boolean | null; // Boolean 24 | } 25 | TaskWhereInput: { // input type 26 | AND?: NexusGenInputs['TaskWhereInput'][] | null; // [TaskWhereInput!] 27 | content?: string | null; // String 28 | content_contains?: string | null; // String 29 | content_ends_with?: string | null; // String 30 | content_gt?: string | null; // String 31 | content_gte?: string | null; // String 32 | content_in?: string[] | null; // [String!] 33 | content_lt?: string | null; // String 34 | content_lte?: string | null; // String 35 | content_not?: string | null; // String 36 | content_not_contains?: string | null; // String 37 | content_not_ends_with?: string | null; // String 38 | content_not_in?: string[] | null; // [String!] 39 | content_not_starts_with?: string | null; // String 40 | content_starts_with?: string | null; // String 41 | id?: string | null; // ID 42 | id_contains?: string | null; // ID 43 | id_ends_with?: string | null; // ID 44 | id_gt?: string | null; // ID 45 | id_gte?: string | null; // ID 46 | id_in?: string[] | null; // [ID!] 47 | id_lt?: string | null; // ID 48 | id_lte?: string | null; // ID 49 | id_not?: string | null; // ID 50 | id_not_contains?: string | null; // ID 51 | id_not_ends_with?: string | null; // ID 52 | id_not_in?: string[] | null; // [ID!] 53 | id_not_starts_with?: string | null; // ID 54 | id_starts_with?: string | null; // ID 55 | isDone?: boolean | null; // Boolean 56 | isDone_not?: boolean | null; // Boolean 57 | NOT?: NexusGenInputs['TaskWhereInput'][] | null; // [TaskWhereInput!] 58 | OR?: NexusGenInputs['TaskWhereInput'][] | null; // [TaskWhereInput!] 59 | } 60 | TaskWhereUniqueInput: { // input type 61 | id?: string | null; // ID 62 | } 63 | } 64 | 65 | export interface NexusGenEnums { 66 | TaskOrderByInput: "content_ASC" | "content_DESC" | "createdAt_ASC" | "createdAt_DESC" | "id_ASC" | "id_DESC" | "isDone_ASC" | "isDone_DESC" | "updatedAt_ASC" | "updatedAt_DESC" 67 | } 68 | 69 | export interface NexusGenRootTypes { 70 | Mutation: {}; 71 | Query: {}; 72 | Task: { // root type 73 | content: string; // String! 74 | id: string; // ID! 75 | isDone: boolean; // Boolean! 76 | } 77 | String: string; 78 | Int: number; 79 | Float: number; 80 | Boolean: boolean; 81 | ID: string; 82 | } 83 | 84 | export interface NexusGenAllTypes extends NexusGenRootTypes { 85 | TaskCreateInput: NexusGenInputs['TaskCreateInput']; 86 | TaskUpdateInput: NexusGenInputs['TaskUpdateInput']; 87 | TaskWhereInput: NexusGenInputs['TaskWhereInput']; 88 | TaskWhereUniqueInput: NexusGenInputs['TaskWhereUniqueInput']; 89 | TaskOrderByInput: NexusGenEnums['TaskOrderByInput']; 90 | } 91 | 92 | export interface NexusGenFieldTypes { 93 | Mutation: { // field return type 94 | createTask: NexusGenRootTypes['Task']; // Task! 95 | deleteTask: NexusGenRootTypes['Task'] | null; // Task 96 | ping: string; // String! 97 | updateTask: NexusGenRootTypes['Task'] | null; // Task 98 | } 99 | Query: { // field return type 100 | stage: string; // String! 101 | task: NexusGenRootTypes['Task'] | null; // Task 102 | tasks: NexusGenRootTypes['Task'][]; // [Task!]! 103 | } 104 | Task: { // field return type 105 | content: string; // String! 106 | id: string; // ID! 107 | isDone: boolean; // Boolean! 108 | } 109 | } 110 | 111 | export interface NexusGenArgTypes { 112 | Mutation: { 113 | createTask: { // args 114 | data: NexusGenInputs['TaskCreateInput']; // TaskCreateInput! 115 | } 116 | deleteTask: { // args 117 | where: NexusGenInputs['TaskWhereUniqueInput']; // TaskWhereUniqueInput! 118 | } 119 | updateTask: { // args 120 | data: NexusGenInputs['TaskUpdateInput']; // TaskUpdateInput! 121 | where: NexusGenInputs['TaskWhereUniqueInput']; // TaskWhereUniqueInput! 122 | } 123 | } 124 | Query: { 125 | task: { // args 126 | where: NexusGenInputs['TaskWhereUniqueInput']; // TaskWhereUniqueInput! 127 | } 128 | tasks: { // args 129 | after?: string | null; // String 130 | before?: string | null; // String 131 | first?: number | null; // Int 132 | last?: number | null; // Int 133 | orderBy?: NexusGenEnums['TaskOrderByInput'] | null; // TaskOrderByInput 134 | skip?: number | null; // Int 135 | where?: NexusGenInputs['TaskWhereInput'] | null; // TaskWhereInput 136 | } 137 | } 138 | } 139 | 140 | export interface NexusGenAbstractResolveReturnTypes { 141 | } 142 | 143 | export interface NexusGenInheritedFields {} 144 | 145 | export type NexusGenObjectNames = "Mutation" | "Query" | "Task"; 146 | 147 | export type NexusGenInputNames = "TaskCreateInput" | "TaskUpdateInput" | "TaskWhereInput" | "TaskWhereUniqueInput"; 148 | 149 | export type NexusGenEnumNames = "TaskOrderByInput"; 150 | 151 | export type NexusGenInterfaceNames = never; 152 | 153 | export type NexusGenScalarNames = "Boolean" | "Float" | "ID" | "Int" | "String"; 154 | 155 | export type NexusGenUnionNames = never; 156 | 157 | export interface NexusGenTypes { 158 | context: any; 159 | inputTypes: NexusGenInputs; 160 | rootTypes: NexusGenRootTypes; 161 | argTypes: NexusGenArgTypes; 162 | fieldTypes: NexusGenFieldTypes; 163 | allTypes: NexusGenAllTypes; 164 | inheritedFields: NexusGenInheritedFields; 165 | objectNames: NexusGenObjectNames; 166 | inputNames: NexusGenInputNames; 167 | enumNames: NexusGenEnumNames; 168 | interfaceNames: NexusGenInterfaceNames; 169 | scalarNames: NexusGenScalarNames; 170 | unionNames: NexusGenUnionNames; 171 | allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames']; 172 | allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames']; 173 | allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] 174 | abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; 175 | abstractResolveReturn: NexusGenAbstractResolveReturnTypes; 176 | } -------------------------------------------------------------------------------- /cheatsheet/server/src/generated/prisma/index.ts: -------------------------------------------------------------------------------- 1 | // Code generated by Prisma (prisma@1.34.1). DO NOT EDIT. 2 | // Please don't change this file manually but run `prisma generate` to update it. 3 | // For more information, please read the docs: https://www.prisma.io/docs/prisma-client/ 4 | 5 | import { DocumentNode } from "graphql"; 6 | import { 7 | makePrismaClientClass, 8 | BaseClientOptions, 9 | Model 10 | } from "prisma-client-lib"; 11 | import { typeDefs } from "./prisma-schema"; 12 | 13 | export type AtLeastOne }> = Partial & 14 | U[keyof U]; 15 | 16 | export type Maybe = T | undefined | null; 17 | 18 | export interface Exists { 19 | task: (where?: TaskWhereInput) => Promise; 20 | } 21 | 22 | export interface Node {} 23 | 24 | export type FragmentableArray = Promise> & Fragmentable; 25 | 26 | export interface Fragmentable { 27 | $fragment(fragment: string | DocumentNode): Promise; 28 | } 29 | 30 | export interface Prisma { 31 | $exists: Exists; 32 | $graphql: ( 33 | query: string, 34 | variables?: { [key: string]: any } 35 | ) => Promise; 36 | 37 | /** 38 | * Queries 39 | */ 40 | 41 | task: (where: TaskWhereUniqueInput) => TaskNullablePromise; 42 | tasks: (args?: { 43 | where?: TaskWhereInput; 44 | orderBy?: TaskOrderByInput; 45 | skip?: Int; 46 | after?: String; 47 | before?: String; 48 | first?: Int; 49 | last?: Int; 50 | }) => FragmentableArray; 51 | tasksConnection: (args?: { 52 | where?: TaskWhereInput; 53 | orderBy?: TaskOrderByInput; 54 | skip?: Int; 55 | after?: String; 56 | before?: String; 57 | first?: Int; 58 | last?: Int; 59 | }) => TaskConnectionPromise; 60 | node: (args: { id: ID_Output }) => Node; 61 | 62 | /** 63 | * Mutations 64 | */ 65 | 66 | createTask: (data: TaskCreateInput) => TaskPromise; 67 | updateTask: (args: { 68 | data: TaskUpdateInput; 69 | where: TaskWhereUniqueInput; 70 | }) => TaskPromise; 71 | updateManyTasks: (args: { 72 | data: TaskUpdateManyMutationInput; 73 | where?: TaskWhereInput; 74 | }) => BatchPayloadPromise; 75 | upsertTask: (args: { 76 | where: TaskWhereUniqueInput; 77 | create: TaskCreateInput; 78 | update: TaskUpdateInput; 79 | }) => TaskPromise; 80 | deleteTask: (where: TaskWhereUniqueInput) => TaskPromise; 81 | deleteManyTasks: (where?: TaskWhereInput) => BatchPayloadPromise; 82 | 83 | /** 84 | * Subscriptions 85 | */ 86 | 87 | $subscribe: Subscription; 88 | } 89 | 90 | export interface Subscription { 91 | task: ( 92 | where?: TaskSubscriptionWhereInput 93 | ) => TaskSubscriptionPayloadSubscription; 94 | } 95 | 96 | export interface ClientConstructor { 97 | new (options?: BaseClientOptions): T; 98 | } 99 | 100 | /** 101 | * Types 102 | */ 103 | 104 | export type TaskOrderByInput = 105 | | "id_ASC" 106 | | "id_DESC" 107 | | "content_ASC" 108 | | "content_DESC" 109 | | "isDone_ASC" 110 | | "isDone_DESC"; 111 | 112 | export type MutationType = "CREATED" | "UPDATED" | "DELETED"; 113 | 114 | export interface TaskCreateInput { 115 | id?: Maybe; 116 | content: String; 117 | isDone: Boolean; 118 | } 119 | 120 | export interface TaskUpdateInput { 121 | content?: Maybe; 122 | isDone?: Maybe; 123 | } 124 | 125 | export interface TaskWhereInput { 126 | id?: Maybe; 127 | id_not?: Maybe; 128 | id_in?: Maybe; 129 | id_not_in?: Maybe; 130 | id_lt?: Maybe; 131 | id_lte?: Maybe; 132 | id_gt?: Maybe; 133 | id_gte?: Maybe; 134 | id_contains?: Maybe; 135 | id_not_contains?: Maybe; 136 | id_starts_with?: Maybe; 137 | id_not_starts_with?: Maybe; 138 | id_ends_with?: Maybe; 139 | id_not_ends_with?: Maybe; 140 | content?: Maybe; 141 | content_not?: Maybe; 142 | content_in?: Maybe; 143 | content_not_in?: Maybe; 144 | content_lt?: Maybe; 145 | content_lte?: Maybe; 146 | content_gt?: Maybe; 147 | content_gte?: Maybe; 148 | content_contains?: Maybe; 149 | content_not_contains?: Maybe; 150 | content_starts_with?: Maybe; 151 | content_not_starts_with?: Maybe; 152 | content_ends_with?: Maybe; 153 | content_not_ends_with?: Maybe; 154 | isDone?: Maybe; 155 | isDone_not?: Maybe; 156 | AND?: Maybe; 157 | OR?: Maybe; 158 | NOT?: Maybe; 159 | } 160 | 161 | export interface TaskUpdateManyMutationInput { 162 | content?: Maybe; 163 | isDone?: Maybe; 164 | } 165 | 166 | export interface TaskSubscriptionWhereInput { 167 | mutation_in?: Maybe; 168 | updatedFields_contains?: Maybe; 169 | updatedFields_contains_every?: Maybe; 170 | updatedFields_contains_some?: Maybe; 171 | node?: Maybe; 172 | AND?: Maybe; 173 | OR?: Maybe; 174 | NOT?: Maybe; 175 | } 176 | 177 | export type TaskWhereUniqueInput = AtLeastOne<{ 178 | id: Maybe; 179 | }>; 180 | 181 | export interface NodeNode { 182 | id: ID_Output; 183 | } 184 | 185 | export interface AggregateTask { 186 | count: Int; 187 | } 188 | 189 | export interface AggregateTaskPromise 190 | extends Promise, 191 | Fragmentable { 192 | count: () => Promise; 193 | } 194 | 195 | export interface AggregateTaskSubscription 196 | extends Promise>, 197 | Fragmentable { 198 | count: () => Promise>; 199 | } 200 | 201 | export interface BatchPayload { 202 | count: Long; 203 | } 204 | 205 | export interface BatchPayloadPromise 206 | extends Promise, 207 | Fragmentable { 208 | count: () => Promise; 209 | } 210 | 211 | export interface BatchPayloadSubscription 212 | extends Promise>, 213 | Fragmentable { 214 | count: () => Promise>; 215 | } 216 | 217 | export interface TaskPreviousValues { 218 | id: ID_Output; 219 | content: String; 220 | isDone: Boolean; 221 | } 222 | 223 | export interface TaskPreviousValuesPromise 224 | extends Promise, 225 | Fragmentable { 226 | id: () => Promise; 227 | content: () => Promise; 228 | isDone: () => Promise; 229 | } 230 | 231 | export interface TaskPreviousValuesSubscription 232 | extends Promise>, 233 | Fragmentable { 234 | id: () => Promise>; 235 | content: () => Promise>; 236 | isDone: () => Promise>; 237 | } 238 | 239 | export interface TaskEdge { 240 | node: Task; 241 | cursor: String; 242 | } 243 | 244 | export interface TaskEdgePromise extends Promise, Fragmentable { 245 | node: () => T; 246 | cursor: () => Promise; 247 | } 248 | 249 | export interface TaskEdgeSubscription 250 | extends Promise>, 251 | Fragmentable { 252 | node: () => T; 253 | cursor: () => Promise>; 254 | } 255 | 256 | export interface TaskSubscriptionPayload { 257 | mutation: MutationType; 258 | node: Task; 259 | updatedFields: String[]; 260 | previousValues: TaskPreviousValues; 261 | } 262 | 263 | export interface TaskSubscriptionPayloadPromise 264 | extends Promise, 265 | Fragmentable { 266 | mutation: () => Promise; 267 | node: () => T; 268 | updatedFields: () => Promise; 269 | previousValues: () => T; 270 | } 271 | 272 | export interface TaskSubscriptionPayloadSubscription 273 | extends Promise>, 274 | Fragmentable { 275 | mutation: () => Promise>; 276 | node: () => T; 277 | updatedFields: () => Promise>; 278 | previousValues: () => T; 279 | } 280 | 281 | export interface Task { 282 | id: ID_Output; 283 | content: String; 284 | isDone: Boolean; 285 | } 286 | 287 | export interface TaskPromise extends Promise, Fragmentable { 288 | id: () => Promise; 289 | content: () => Promise; 290 | isDone: () => Promise; 291 | } 292 | 293 | export interface TaskSubscription 294 | extends Promise>, 295 | Fragmentable { 296 | id: () => Promise>; 297 | content: () => Promise>; 298 | isDone: () => Promise>; 299 | } 300 | 301 | export interface TaskNullablePromise 302 | extends Promise, 303 | Fragmentable { 304 | id: () => Promise; 305 | content: () => Promise; 306 | isDone: () => Promise; 307 | } 308 | 309 | export interface TaskConnection { 310 | pageInfo: PageInfo; 311 | edges: TaskEdge[]; 312 | } 313 | 314 | export interface TaskConnectionPromise 315 | extends Promise, 316 | Fragmentable { 317 | pageInfo: () => T; 318 | edges: >() => T; 319 | aggregate: () => T; 320 | } 321 | 322 | export interface TaskConnectionSubscription 323 | extends Promise>, 324 | Fragmentable { 325 | pageInfo: () => T; 326 | edges: >>() => T; 327 | aggregate: () => T; 328 | } 329 | 330 | export interface PageInfo { 331 | hasNextPage: Boolean; 332 | hasPreviousPage: Boolean; 333 | startCursor?: String; 334 | endCursor?: String; 335 | } 336 | 337 | export interface PageInfoPromise extends Promise, Fragmentable { 338 | hasNextPage: () => Promise; 339 | hasPreviousPage: () => Promise; 340 | startCursor: () => Promise; 341 | endCursor: () => Promise; 342 | } 343 | 344 | export interface PageInfoSubscription 345 | extends Promise>, 346 | Fragmentable { 347 | hasNextPage: () => Promise>; 348 | hasPreviousPage: () => Promise>; 349 | startCursor: () => Promise>; 350 | endCursor: () => Promise>; 351 | } 352 | 353 | /* 354 | 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. 355 | */ 356 | export type String = string; 357 | 358 | export type Long = string; 359 | 360 | /* 361 | 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. 362 | */ 363 | export type ID_Input = string | number; 364 | export type ID_Output = string; 365 | 366 | /* 367 | The `Boolean` scalar type represents `true` or `false`. 368 | */ 369 | export type Boolean = boolean; 370 | 371 | /* 372 | The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. 373 | */ 374 | export type Int = number; 375 | 376 | /** 377 | * Model Metadata 378 | */ 379 | 380 | export const models: Model[] = [ 381 | { 382 | name: "Task", 383 | embedded: false 384 | } 385 | ]; 386 | 387 | /** 388 | * Type Defs 389 | */ 390 | 391 | export const Prisma = makePrismaClientClass>({ 392 | typeDefs, 393 | models, 394 | endpoint: `${process.env["PRISMA_ENDPOINT"]}`, 395 | secret: `03ef7cac` 396 | }); 397 | export const prisma = new Prisma(); 398 | -------------------------------------------------------------------------------- /cheatsheet/server/src/generated/prisma/prisma-schema.ts: -------------------------------------------------------------------------------- 1 | // Code generated by Prisma (prisma@1.34.1). DO NOT EDIT. 2 | // Please don't change this file manually but run `prisma generate` to update it. 3 | // For more information, please read the docs: https://www.prisma.io/docs/prisma-client/ 4 | 5 | export const typeDefs = /* GraphQL */ `type AggregateTask { 6 | count: Int! 7 | } 8 | 9 | type BatchPayload { 10 | count: Long! 11 | } 12 | 13 | scalar Long 14 | 15 | type Mutation { 16 | createTask(data: TaskCreateInput!): Task! 17 | updateTask(data: TaskUpdateInput!, where: TaskWhereUniqueInput!): Task 18 | updateManyTasks(data: TaskUpdateManyMutationInput!, where: TaskWhereInput): BatchPayload! 19 | upsertTask(where: TaskWhereUniqueInput!, create: TaskCreateInput!, update: TaskUpdateInput!): Task! 20 | deleteTask(where: TaskWhereUniqueInput!): Task 21 | deleteManyTasks(where: TaskWhereInput): BatchPayload! 22 | } 23 | 24 | enum MutationType { 25 | CREATED 26 | UPDATED 27 | DELETED 28 | } 29 | 30 | interface Node { 31 | id: ID! 32 | } 33 | 34 | type PageInfo { 35 | hasNextPage: Boolean! 36 | hasPreviousPage: Boolean! 37 | startCursor: String 38 | endCursor: String 39 | } 40 | 41 | type Query { 42 | task(where: TaskWhereUniqueInput!): Task 43 | tasks(where: TaskWhereInput, orderBy: TaskOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Task]! 44 | tasksConnection(where: TaskWhereInput, orderBy: TaskOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): TaskConnection! 45 | node(id: ID!): Node 46 | } 47 | 48 | type Subscription { 49 | task(where: TaskSubscriptionWhereInput): TaskSubscriptionPayload 50 | } 51 | 52 | type Task { 53 | id: ID! 54 | content: String! 55 | isDone: Boolean! 56 | } 57 | 58 | type TaskConnection { 59 | pageInfo: PageInfo! 60 | edges: [TaskEdge]! 61 | aggregate: AggregateTask! 62 | } 63 | 64 | input TaskCreateInput { 65 | id: ID 66 | content: String! 67 | isDone: Boolean! 68 | } 69 | 70 | type TaskEdge { 71 | node: Task! 72 | cursor: String! 73 | } 74 | 75 | enum TaskOrderByInput { 76 | id_ASC 77 | id_DESC 78 | content_ASC 79 | content_DESC 80 | isDone_ASC 81 | isDone_DESC 82 | } 83 | 84 | type TaskPreviousValues { 85 | id: ID! 86 | content: String! 87 | isDone: Boolean! 88 | } 89 | 90 | type TaskSubscriptionPayload { 91 | mutation: MutationType! 92 | node: Task 93 | updatedFields: [String!] 94 | previousValues: TaskPreviousValues 95 | } 96 | 97 | input TaskSubscriptionWhereInput { 98 | mutation_in: [MutationType!] 99 | updatedFields_contains: String 100 | updatedFields_contains_every: [String!] 101 | updatedFields_contains_some: [String!] 102 | node: TaskWhereInput 103 | AND: [TaskSubscriptionWhereInput!] 104 | OR: [TaskSubscriptionWhereInput!] 105 | NOT: [TaskSubscriptionWhereInput!] 106 | } 107 | 108 | input TaskUpdateInput { 109 | content: String 110 | isDone: Boolean 111 | } 112 | 113 | input TaskUpdateManyMutationInput { 114 | content: String 115 | isDone: Boolean 116 | } 117 | 118 | input TaskWhereInput { 119 | id: ID 120 | id_not: ID 121 | id_in: [ID!] 122 | id_not_in: [ID!] 123 | id_lt: ID 124 | id_lte: ID 125 | id_gt: ID 126 | id_gte: ID 127 | id_contains: ID 128 | id_not_contains: ID 129 | id_starts_with: ID 130 | id_not_starts_with: ID 131 | id_ends_with: ID 132 | id_not_ends_with: ID 133 | content: String 134 | content_not: String 135 | content_in: [String!] 136 | content_not_in: [String!] 137 | content_lt: String 138 | content_lte: String 139 | content_gt: String 140 | content_gte: String 141 | content_contains: String 142 | content_not_contains: String 143 | content_starts_with: String 144 | content_not_starts_with: String 145 | content_ends_with: String 146 | content_not_ends_with: String 147 | isDone: Boolean 148 | isDone_not: Boolean 149 | AND: [TaskWhereInput!] 150 | OR: [TaskWhereInput!] 151 | NOT: [TaskWhereInput!] 152 | } 153 | 154 | input TaskWhereUniqueInput { 155 | id: ID 156 | } 157 | ` -------------------------------------------------------------------------------- /cheatsheet/server/src/generated/schema.graphql: -------------------------------------------------------------------------------- 1 | ### This file was autogenerated by GraphQL Nexus 2 | ### Do not make changes to this file directly 3 | 4 | 5 | type Mutation { 6 | createTask(data: TaskCreateInput!): Task! 7 | deleteTask(where: TaskWhereUniqueInput!): Task 8 | ping: String! 9 | updateTask(data: TaskUpdateInput!, where: TaskWhereUniqueInput!): Task 10 | } 11 | 12 | type Query { 13 | stage: String! 14 | task(where: TaskWhereUniqueInput!): Task 15 | tasks(after: String, before: String, first: Int, last: Int, orderBy: TaskOrderByInput, skip: Int, where: TaskWhereInput): [Task!]! 16 | } 17 | 18 | type Task { 19 | content: String! 20 | id: ID! 21 | isDone: Boolean! 22 | } 23 | 24 | input TaskCreateInput { 25 | content: String! 26 | id: ID 27 | isDone: Boolean! 28 | } 29 | 30 | enum TaskOrderByInput { 31 | content_ASC 32 | content_DESC 33 | createdAt_ASC 34 | createdAt_DESC 35 | id_ASC 36 | id_DESC 37 | isDone_ASC 38 | isDone_DESC 39 | updatedAt_ASC 40 | updatedAt_DESC 41 | } 42 | 43 | input TaskUpdateInput { 44 | content: String 45 | isDone: Boolean 46 | } 47 | 48 | input TaskWhereInput { 49 | AND: [TaskWhereInput!] 50 | content: String 51 | content_contains: String 52 | content_ends_with: String 53 | content_gt: String 54 | content_gte: String 55 | content_in: [String!] 56 | content_lt: String 57 | content_lte: String 58 | content_not: String 59 | content_not_contains: String 60 | content_not_ends_with: String 61 | content_not_in: [String!] 62 | content_not_starts_with: String 63 | content_starts_with: String 64 | id: ID 65 | id_contains: ID 66 | id_ends_with: ID 67 | id_gt: ID 68 | id_gte: ID 69 | id_in: [ID!] 70 | id_lt: ID 71 | id_lte: ID 72 | id_not: ID 73 | id_not_contains: ID 74 | id_not_ends_with: ID 75 | id_not_in: [ID!] 76 | id_not_starts_with: ID 77 | id_starts_with: ID 78 | isDone: Boolean 79 | isDone_not: Boolean 80 | NOT: [TaskWhereInput!] 81 | OR: [TaskWhereInput!] 82 | } 83 | 84 | input TaskWhereUniqueInput { 85 | id: ID 86 | } 87 | -------------------------------------------------------------------------------- /cheatsheet/server/src/resolvers/Mutation.ts: -------------------------------------------------------------------------------- 1 | import { mutationType } from 'nexus' 2 | 3 | export const Mutation = mutationType({ 4 | definition(t) { 5 | t.string('ping', { 6 | resolve: (_parent, _args, _context) => { 7 | return 'pong' 8 | }, 9 | }) 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /cheatsheet/server/src/resolvers/Query.ts: -------------------------------------------------------------------------------- 1 | import { queryType } from 'nexus' 2 | 3 | export const Query = queryType({ 4 | definition(t) { 5 | t.string('stage', { 6 | resolve: (_parent, _args, _context) => { 7 | return process.env.STAGE as string 8 | }, 9 | }) 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /cheatsheet/server/src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Query' 2 | export * from './Mutation' 3 | 4 | export * from './task' 5 | -------------------------------------------------------------------------------- /cheatsheet/server/src/resolvers/task/Mutation.ts: -------------------------------------------------------------------------------- 1 | import { prismaExtendType } from 'nexus-prisma' 2 | 3 | export const TaskMutations = prismaExtendType({ 4 | type: 'Mutation', 5 | definition(t) { 6 | t.prismaFields([ 7 | 'createTask', 8 | 'updateTask', 9 | 'deleteTask', 10 | ]) 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /cheatsheet/server/src/resolvers/task/Query.ts: -------------------------------------------------------------------------------- 1 | import { prismaExtendType } from 'nexus-prisma' 2 | 3 | export const TaskQueries = prismaExtendType({ 4 | type: 'Query', 5 | definition(t) { 6 | t.prismaFields(['task', 'tasks']) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /cheatsheet/server/src/resolvers/task/index.ts: -------------------------------------------------------------------------------- 1 | import { prismaObjectType } from 'nexus-prisma' 2 | 3 | export const Task = prismaObjectType({ 4 | name: 'Task', 5 | definition(t) { 6 | t.prismaFields(['*']) 7 | 8 | // 또는 다음과 같이 원하는 필드만 노출할 수 있습니다. 9 | // t.prismaFields(['content', 'isDone']) 10 | }, 11 | }) 12 | 13 | export * from './Query' 14 | export * from './Mutation' 15 | -------------------------------------------------------------------------------- /cheatsheet/server/src/server.ts: -------------------------------------------------------------------------------- 1 | import app from './app' 2 | 3 | const isProd = process.env.NODE_ENV === 'production' 4 | const port = isProd ? 80 : 3000 5 | 6 | app.listen(port) 7 | -------------------------------------------------------------------------------- /cheatsheet/server/src/serverless.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, Context } from 'aws-lambda' 2 | import awsServerlessExpress from 'aws-serverless-express' 3 | import app from './app' 4 | 5 | const BINARY_MIME_TYPES = [ 6 | 'application/javascript', 7 | 'application/json', 8 | 'application/octet-stream', 9 | 'application/xml', 10 | 'text/css', 11 | 'text/html', 12 | 'text/javascript', 13 | 'text/plain', 14 | 'text/text', 15 | 'text/xml', 16 | ] 17 | 18 | const server = awsServerlessExpress.createServer(app, undefined, BINARY_MIME_TYPES) 19 | 20 | export function handler(event: APIGatewayProxyEvent, context: Context) { 21 | return awsServerlessExpress.proxy(server, event, context) 22 | } 23 | -------------------------------------------------------------------------------- /cheatsheet/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "esnext", 7 | ], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "removeComments": false, 14 | "preserveConstEnums": true, 15 | "sourceMap": true, 16 | "skipLibCheck": true, 17 | "esModuleInterop": true, 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "baseUrl": ".", 21 | "paths": {} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cheatsheet/server/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**/*.ts", 9 | "dist/**/*.js", 10 | "src/generated/**/*.ts" 11 | ] 12 | }, 13 | "rules": { 14 | "semicolon": [true, "never"], 15 | "quotemark": [true, "single"], 16 | "indent": [true, "spaces", 2], 17 | "object-literal-sort-keys": false, 18 | "no-shadowed-variable": false, 19 | "max-line-length": false, 20 | "prefer-for-of": false, 21 | "variable-name": false, 22 | "no-empty": false, 23 | "jsx-boolean-value": false, 24 | "no-console": [true, "log"], 25 | "member-access": false, 26 | "max-classes-per-file": false, 27 | "jsx-no-multiline-js": false, 28 | "member-ordering": false, 29 | "no-var-requires": false, 30 | "interface-name": false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cheatsheet/server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') 3 | const WebpackbarPlugin = require('webpackbar') 4 | 5 | module.exports = { 6 | mode: 'production', 7 | target: 'node', 8 | entry: { 9 | server: path.resolve(__dirname, './src/server.ts'), 10 | serverless: path.resolve(__dirname, './src/serverless.ts'), 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, './dist'), 14 | filename: '[name].js', 15 | libraryTarget: 'commonjs', 16 | }, 17 | resolve: { 18 | extensions: ['.ts', '.tsx', '.js'], 19 | alias: { 20 | '~': path.resolve(__dirname, './src'), 21 | }, 22 | }, 23 | module: { 24 | rules: [ 25 | { test: /\.mjs$/, include: /node_modules/, type: 'javascript/auto' }, 26 | { test: /\.tsx?$/, loader: 'ts-loader' }, 27 | ], 28 | }, 29 | stats: 'errors-only', 30 | optimization: { 31 | minimize: false, 32 | }, 33 | plugins: [ 34 | new WebpackbarPlugin({ 35 | name: 'Server (Production)', 36 | color: '#fa5252', 37 | }), 38 | new ForkTsCheckerWebpackPlugin(), 39 | ], 40 | } 41 | -------------------------------------------------------------------------------- /documents/1-graphql/README.md: -------------------------------------------------------------------------------- 1 | # 1. GraphQL 살펴보기 2 | 3 | ### 본 챕터의 학습 목표는 아래와 같습니다. 👏 4 | - [ ] GraphQL에 대해 이해한다 5 | - [ ] Node.js 환경에서 GraphQL 프로젝트를 시작한다 6 | - [ ] *Schema-First* vs. *Code-First* 개념에 대해 이해한다 7 | - [ ] Nexus 문법을 익힌다 8 | 9 | 10 | ## (1) GraphQL이란? 11 | GraphQL은 API 설계(Schema)와 요청(Query)을 구조화하는 일련의 약속(Interface)입니다. GraphQL을 통해서 우리는 데이터에 기반하여 API를 디자인할 수 있으며, 클라이언트에서는 정해진 쿼리 언어를 통해 API를 체계적으로 사용할 수 있습니다. 12 | - 클라이언트는 서버에 필요한 자원만 요청할 수 있습니다. 13 | - 클라이언트는 서버가 가진 많은 자원을 단 한 번의 요청으로 가져 올 수 있습니다. 14 | - 타입 시스템을 통해 개발 생산성을 비약적으로 향상 시킬 수 있습니다. 15 | - 제공되는 기본 개발자 도구를 통해 API를 쉽게 문서화하고 검색할 수 있습니다. 16 | - 버전 관리 없이 API를 점진적으로 진화 시킬 수 있습니다. 17 | 18 | 19 | ## (2) GraphQL 타입 시스템과 쿼리, 뮤테이션 타입 20 | GraphQL의 기본 타입에는 다음 5가지가 존재합니다. 21 | 22 | - `Int`: 부호가 있는 32비트 정수. 23 | - `Float`: 부호가 있는 부동소수점 값. 24 | - `String`: UTF-8 문자열. 25 | - `Boolean`: `true` 또는 `false`. 26 | - `ID`: `ID` 스칼라 타입은 객체를 다시 요청하거나 캐시의 키로써 자주 사용되는 고유 식별자를 나타냅니다. `ID` 타입은 String 과 같은 방법으로 직렬화되지만, `ID`로 정의하는 것은 사람이 읽을 수 있도록 하는 의도가 아니라는 것을 의미합니다. 27 | 28 | #### 살펴보기 29 | 30 | `User`와 `Post`라는 타입을 스키마에 선언해볼까요? 31 | 32 | ```graphql 33 | type User { 34 | id: ID! 35 | username: String! 36 | posts: [Post!]! 37 | } 38 | 39 | type Post { 40 | id: ID! 41 | title: String! 42 | content: String! 43 | author: User! 44 | } 45 | ``` 46 | 47 | > 참고: `!`는 데이터 값에 `null`이 포함 될 수 없음을 나타냅니다. 기본적으로 `!`는 모두 붙여준다고 생각하는 것이 좋습니다. 48 | 49 | API 설계 단에서 다음과 같이 API에 필요한 타입들을 정의할 수 있습니다. 자 이제 타입을 만들었으니, 해당 타입을 가지는 데이터를 가져올 수 있도록 만들어주어야겠죠? 50 | 51 | ### 쿼리와 뮤테이션 52 | 쿼리와 뮤테이션은 기본적으로 선언되어야 하는 타입입니다. 53 | 54 | - 쿼리는 데이터를 가져오는데 사용합니다. 55 | - `ID` 속성을 통해 Client 내부에서 Cache를 구현할 수 있습니다 56 | - 뮤테이션은 데이터를 조작하는데 사용합니다. 57 | - 생성 58 | - 수정 59 | - 삭제 60 | - ... 61 | 62 | 이 두가지 타입을 통해 데이터를 가져오고, 수정할 수 있도록 `Query`, `Mutation` 타입 내에 속성들을 선언해보겠습니다. 63 | 64 | #### 살펴보기 65 | ```graphql 66 | type User { 67 | id: ID! 68 | username: String! 69 | posts: [Post!]! 70 | } 71 | 72 | type Post { 73 | id: ID! 74 | title: String! 75 | content: String! 76 | author: User! 77 | } 78 | 79 | type Query { 80 | user(id: ID!): User! 81 | post(id: ID!): Post! 82 | } 83 | 84 | type Mutation { 85 | createUser(username: String!): User! 86 | updateUser(id: ID!, username: String): User! 87 | deleteUser(id: ID!): User! 88 | createPost(title: String!, content: String!, authorId: String!): Post! 89 | updatePost(id: ID!, title: String, content: String): Post! 90 | deletePost(id: ID!): Post! 91 | } 92 | ``` 93 | 94 | 다음과 같이 `Query`와 `Mutation`에 속성 값으로 필요한 쿼리, 뮤테이션 들을 만들어주었습니다. 95 | 96 | ### 리졸버 97 | 이렇게 멋지게 선언해 준 GraphQL 스키마를 어떻게 구동시킬 수 있을까요? 바로 리졸버가 그 역할을 해줍니다. 리졸버는 타입 내 속성과 1:1로 일치시켜 구현이 필요합니다. 리졸버는 `parent`, `args`를 기본 argument로 하는 함수의 형태입니다. 98 | 99 | - `parent`는 상위 리졸버에서 `return` 한 값을 나타냅니다 100 | - `args`는 쿼리문 내에서 넣어준 매개변수를 나타냅니다. 101 | - `context`는 요청 하나를 타고 공유되는 전역 상태입니다. (로그인 세션 정보등을 저장) 102 | 103 | #### 살펴보기 104 | ```typescript 105 | const User = { 106 | id: (parent, args, context) => { 107 | return parent.id 108 | }, 109 | username: (parent, args, context) => { 110 | return parent.username 111 | }, 112 | posts: (parent, args, context) => { 113 | /* parent.id를 통해 DB에서 Post를 가져옵니다 */ 114 | return posts 115 | }, 116 | } 117 | 118 | const Post = { 119 | id: (parent, args, context) => { 120 | return parent.id 121 | }, 122 | title: (parent, args, context) => { 123 | return parent.title 124 | }, 125 | content: (parent, args, context) => { 126 | return parent.content 127 | }, 128 | author: (parent, args, context) => { 129 | /* parent.authorId를 통해 DB에서 해당 User를 가져옵니다 */ 130 | return user 131 | }, 132 | } 133 | 134 | const Query = { 135 | user: (parent, args, context) => { 136 | /* args.id를 통해 DB에서 해당 User를 가져옵니다 */ 137 | return user 138 | }, 139 | post: (parent, args, context) => { 140 | /* args.id를 통해 DB에서 해당 Post를 가져옵니다 */ 141 | return post 142 | }, 143 | } 144 | 145 | const Mutation = { 146 | createUser: (parent, args, context) => { 147 | /* 새 유저를 생성해서 DB에 삽입합니다 */ 148 | return user 149 | }, 150 | updateUser: (parent, args, context) => { 151 | /* args.id를 통해 DB에서 기존 유저를 불러와 값을 수정 한 뒤 DB에 삽입합니다 */ 152 | return user 153 | }, 154 | deleteUser: (parent, args, context) => { 155 | /* args.id를 통해 DB에서 기존 유저를 삭제합니다 */ 156 | return user 157 | }, 158 | createPost: (parent, args, context) => { 159 | /* 새 게시물을 생성해서 DB에 삽입합니다 */ 160 | return post 161 | }, 162 | updatePost: (parent, args, context) => { 163 | /* args.id를 통해 DB에서 기존 게시물을 불러와 값을 수정 한 뒤 DB에 삽입합니다 */ 164 | return post 165 | }, 166 | deletePost: (parent, args, context) => { 167 | /* args.id를 통해 DB에서 기존 게시물을 삭제합니다 */ 168 | return post 169 | }, 170 | } 171 | ``` 172 | > 본 코드는 리졸버 구조 이해를 위해 작성되었습니다. 실제로는 동작하지 않은 Pseudo 코드입니다. 173 | 174 | ### 요청 175 | 클라이언트가 다음과 같은 문법으로 서버에 요청을 날리게 되면, 176 | 177 | ```graphql 178 | query { 179 | user(id: "ea9f5eac-1449-5f03-a1c9-6521622de815") { 180 | id 181 | username 182 | } 183 | } 184 | ``` 185 | 186 | 이 요청에 응답하기 위해 서버에서 187 | 188 | ![](./images/diagram-1.png) 189 | 190 | 1. `Query.user` 리졸버 함수 실행 191 | 2. 데이터베이스에서 User를 가져와서 해당 값을 리졸버 함수 내에서 `return` 192 | 3. `return` 값을 `User.id`, `User.username` 리졸버에 `parent` argument로 전달 193 | 1. `User.id` 리졸버 함수는 `parent` argument를 사용해, id 값을 `return` 194 | 2. `User.username` 리졸버 함수는 `parent` argument를 사용해, username 값을 `return` 195 | 196 | 순으로 리졸버 함수가 실행됩니다. 그 후 결과를 종합해 다음과 같이 JSON으로 응답하여 줍니다. 197 | ```json 198 | { 199 | "user": { 200 | "id": "ea9f5eac-1449-5f03-a1c9-6521622de815", 201 | "username": "tonyfromundefined" 202 | } 203 | } 204 | ``` 205 | 206 | 이러한 개발 방식을 GraphQL에서 *Schema-First (SDL-First)* 개발 방식이라고 합니다. 이러한 방식은 처음 GraphQL 구현체가 등장했을 때 많이 사용되었습니다. 207 | 208 | 하지만, *Schema-First* 개발 방식에는 다음과 같은 한계점이 존재합니다. 209 | 210 | 1. 스키마 정의와 리졸버 간의 불일치 문제 211 | 2. GraphQL 스키마 분리 문제 212 | 3. 스키마 정의의 중복 (코드 재사용 문제) 213 | 4. IDE 지원 부족으로 인한 낮은 개발 경험 214 | 5. Schema 작성 문제 215 | 216 | 따라서, 이러한 한계점을 효과적으로 해결하기 위해 *Code-First* 개발 방식이 등장하게 되었습니다. 217 | 218 | > 참고: [The Problems of "Schema-First" GraphQL Server Development](https://www.prisma.io/blog/the-problems-of-schema-first-graphql-development-x1mn4cb0tyl3) 219 | 220 | 221 | ## (3) Nexus로 시작하는 *Code-First* GraphQL 개발 222 | GraphQL에 대해 이해하셨나요? 앞서 말씀드린 *Code-First* 개발 방법에 대한 구현체로 Nexus라는 오픈소스 라이브러리를 Prisma에서 내놓았습니다. 223 | 224 | 이 챕터에서는 225 | 226 | - Node.js 227 | - TypeScript 228 | - Webpack 229 | - Nexus 230 | 231 | 를 기반으로 하는 실제 GraphQL 프로젝트를 살펴보겠습니다. 해당 프로젝트는 본 레포 안의 `/starters/server` 폴더 안에서 찾을 수 있습니다. 232 | 233 | ### 폴더 및 파일 구조 살펴보기 234 | #### `/` 235 | - `.env.example` 236 | - 환경변수 설정 파일입니다. 237 | - `.env.example`를 참고하여 폴더 내에 `.env.development`와 `.env.production`을 만들어줍니다. 238 | - `package.json`, `yarn.lock` 239 | - 현재 프로젝트가 의존하고 있는 라이브러리와 버전 정보를 담고 있습니다 240 | - `yarn` 명령어를 통해 라이브러리를 모두 설치할 수 있습니다. 241 | - `tsconfig.json` 242 | - TypeScript 관련 설정 파일입니다. 243 | - `tslint.json` 244 | - TSLint 관련 설정 파일입니다. 245 | - `webpack.config.dev.js` 246 | - 개발 모드의 Webpack 빌드 설정입니다. 247 | - `yarn dev` 명령에서 해당 설정으로 Webpack이 작동합니다. 248 | - `webpack.config.prod.js` 249 | - Production 모드의 Webpack 빌드 설정입니다. 250 | - `yarn build` 명령에서 해당 설정으로 Webpack이 작동합니다. 251 | - `serverless.yml` 252 | - 배포를 위한 Serverless Framework 설정입니다. 253 | 254 | #### `/src/` 255 | - `app.ts` 256 | - Express.js 프레임워크를 통해 API가 구현되는 엔트리 파일입니다. 만들어진 API 서버를 export 합니다. 257 | - `server.ts` 258 | - `app.ts`에서 API 서버를 가져와 3000 포트로 Listen합니다. 259 | - 개발 서버를 띄우는 데 사용합니다. 260 | - `serverless.ts` 261 | - `app.ts`에서 API 서버를 가져와 `aws-serverless-express`를 사용하여, Lambda 요청에 응답하는 함수를 초기화합니다. 262 | - API Gateway와 Lambda 기반의 서버리스 환경에 배포 할 때 사용합니다. 263 | 264 | #### `/src/resolvers/` 265 | - `index.ts` 266 | - 엔트리 파일입니다. `Query.ts`와 `Mutation.ts`가 내보낸 항목을 다시 내보내는 역할을 합니다. 267 | - `Query.ts` 268 | - 기본 Query 타입을 선언합니다. 269 | - `Mutation.ts` 270 | - 기본 Mutation 타입을 선언합니다. 271 | 272 | #### `/src/generated` 273 | - `schema.graphql` 274 | - Nexus가 자동 생성한 Schema 입니다. 275 | - `nexus.ts` 276 | - Nexus가 자동 생성한 TypeScript Typing 입니다. 277 | 278 | 279 | ### 시작하기 280 | - `/starters/server/` 폴더로 이동합니다. 281 | 282 | ```bash 283 | $ cd ./starters/server 284 | ``` 285 | 286 | - 프로젝트에 필요한 라이브러리를 설치합니다. 287 | 288 | ```bash 289 | # 기존에 yarn이 설치되어 있지 않다면, 290 | $ npm i -g yarn 291 | 292 | # 라이브러리 설치하기 293 | $ yarn 294 | ``` 295 | 296 | > 해당 작업이 완료되면 `node_modules` 폴더가 생성되고 해당 폴더 아래에 필요한 라이브러리들이 위치하게 됩니다. 297 | 298 | - 프로젝트 폴더에 아래 두 파일을 생성합니다. 299 | - `.env.development` 300 | 301 | ``` 302 | STAGE="development" 303 | IS_PLAYGROUND_ENABLED="1" 304 | IS_TRACING_ENABLED="1" 305 | 306 | PRISMA_ENDPOINT="{endpoint}/{service}/{stage}" 307 | PRISMA_MANAGEMENT_API_SECRET="{managementApiSecret}" 308 | ``` 309 | 310 | - `.env.production` 311 | 312 | ``` 313 | STAGE="production" 314 | IS_PLAYGROUND_ENABLED="1" 315 | IS_TRACING_ENABLED="1" 316 | 317 | PRISMA_ENDPOINT="{endpoint}/{service}/{stage}" 318 | PRISMA_MANAGEMENT_API_SECRET="{managementApiSecret}" 319 | ``` 320 | 321 | > 두 파일을 통해 각 스테이지에서 환경 변수를 설정할 수 있습니다. 322 | 323 | - 개발 서버 시작하기 324 | 325 | ```bash 326 | $ yarn dev 327 | ``` 328 | > 개발 서버를 시작 한 뒤에는 `http://localhost:3000`로 접근할 수 있습니다. 329 | 330 | - Nexus 기반으로 작성된 `Query`와 `Mutation` 살펴보기 331 | 332 | #### `/src/resolvers/Query.ts` 333 | ```typescript 334 | import { queryType } from 'nexus' 335 | 336 | export const Query = queryType({ 337 | definition(t) { 338 | t.string('stage', { 339 | resolve: (_parent, _args, _context) => { 340 | return process.env.STAGE as string 341 | }, 342 | }) 343 | }, 344 | }) 345 | ``` 346 | 347 | #### `/src/resolvers/Mutation.ts` 348 | ```typescript 349 | import { mutationType } from 'nexus' 350 | 351 | export const Mutation = mutationType({ 352 | definition(t) { 353 | t.string('ping', { 354 | resolve: (_parent, _args, _context) => { 355 | return 'pong' 356 | }, 357 | }) 358 | }, 359 | }) 360 | ``` 361 | 362 | 다음과 같이 Nexus를 통해서 코드를 작성하면, Nexus가 해당 코드를 이용해 `/src/generated/schema.graphql`을 자동으로 생성해줍니다. 따라서, *Schema-First*에서 존재했던 문제점인 **스키마 정의와 리졸버 간의 불일치 문제**와 **Schema 작성 문제**를 해결할 수 있습니다. 363 | 364 | 추가적으로 Nexus가 `/src/generated/nexus.ts`에 TypeScript 타이핑을 자동으로 생성해주기 때문에, GraphQL 타입 환경을 TypeScript 환경과 결합하여 초월적인 개발 편의성을 만끽할 수 있습니다. (**IDE 지원 부족으로 인한 낮은 개발 경험** 문제 해결) 365 | 366 | #### `/src/generated/schema.graphql` 367 | ```graphql 368 | type Query { 369 | stage: String! 370 | } 371 | 372 | type Mutation { 373 | ping: String! 374 | } 375 | ``` 376 | 377 | 378 | ## (4) GraphQL Playground 379 | API를 작성했다면, 해당 API가 정상적으로 동작하는지 테스트해보아야겠죠? 개발 서버를 띄워놓은 상태에서 `http://localhost:3000/graphql`로 접속하면, GraphQL 문서화 도구인, *GraphQL Playground*를 볼 수 있습니다. 380 | 381 | ![](./images/screenshot-1.png) 382 | 383 | *GraphQL Playground*를 통해, 384 | - GraphQL API를 검색하고 (DOCS 메뉴) 385 | - 구현된 API를 테스트할 수 있습니다. (좌: Query 작성 및 수행 / 우: 응답 JSON) 386 | 387 | 자, 그럼 *GraphQL Playground*를 통해 아래 쿼리가 정상적으로 동작하는지 테스트해봅시다. 388 | 389 | ```graphql 390 | query { 391 | stage 392 | } 393 | ``` 394 | 395 | ```graphql 396 | mutation { 397 | ping 398 | } 399 | ``` 400 | 401 | 402 | ## (5) `Task` 타입과 쿼리, 뮤테이션 만들기 403 | 자 이제 우리만의 타입을 하나 만들어봅시다. `/src/resolvers` 폴더 내에 `task` 폴더를 새로 생성해줍니다. 그리고 그 아래 404 | 405 | - `/src/resolvers/task/index.ts` (`Task` 타입 정의 및 Query, Mutation을 받아서 export) 406 | - `/src/resolvers/task/Query.ts` (`Query` 타입을 확장) 407 | - `/src/resolvers/task/Mutation.ts` (`Mutation` 타입을 확장) 408 | 409 | 파일을 생성해줍니다. 410 | 411 | 각 파일을 작성해볼까요? 412 | 413 | #### `/src/resolvers/task/index.ts` 414 | ```typescript 415 | import { objectType } from 'nexus' 416 | 417 | interface ITask { 418 | id: string 419 | content: string 420 | isDone: boolean 421 | } 422 | 423 | // 가상의 Database 424 | export const TASKS: ITask[] = [] 425 | 426 | export const Task = objectType({ 427 | name: 'Task', 428 | definition(t) { 429 | t.id('id', { 430 | description: 'Task 생성 시 자동 생성되는 Unique ID', 431 | }) 432 | t.string('content', { 433 | description: 'Task 내용', 434 | }) 435 | t.boolean('isDone', { 436 | description: 'Task 완료 여부', 437 | }) 438 | }, 439 | }) 440 | 441 | export * from './Query' 442 | export * from './Mutation' 443 | ``` 444 | 445 | #### `/src/resolvers/task/Query.ts` 446 | ```typescript 447 | import { extendType, idArg } from 'nexus' 448 | import { TASKS } from './' 449 | 450 | export const TaskQueries = extendType({ 451 | type: 'Query', 452 | definition(t) { 453 | t.field('task', { 454 | type: 'Task', 455 | args: { 456 | id: idArg({ 457 | required: true, 458 | }), 459 | }, 460 | resolve: (_parent, args) => { 461 | const task = TASKS.find((task) => task.id === args.id) 462 | 463 | if (task) { 464 | return task 465 | 466 | } else { 467 | throw new Error(`${args.id}를 가진 Task를 찾을 수 없습니다`) 468 | } 469 | }, 470 | }) 471 | 472 | t.list.field('tasks', { 473 | type: 'Task', 474 | resolve: () => { 475 | return TASKS 476 | }, 477 | }) 478 | }, 479 | }) 480 | ``` 481 | 482 | #### `/src/resolvers/task/Mutation.ts` 483 | ```typescript 484 | import { booleanArg, extendType, idArg, stringArg } from 'nexus' 485 | import short from 'short-uuid' 486 | import { TASKS } from './' 487 | 488 | export const TaskMutations = extendType({ 489 | type: 'Mutation', 490 | definition(t) { 491 | t.field('createTask', { 492 | type: 'Task', 493 | args: { 494 | content: stringArg({ 495 | required: true, 496 | }), 497 | }, 498 | resolve: (_parent, args) => { 499 | const task = { 500 | id: short.generate(), 501 | content: args.content, 502 | isDone: false, 503 | } 504 | 505 | TASKS.push(task) 506 | 507 | return task 508 | }, 509 | }) 510 | 511 | t.field('updateTask', { 512 | type: 'Task', 513 | args: { 514 | id: idArg({ 515 | required: true, 516 | }), 517 | content: stringArg(), 518 | isDone: booleanArg(), 519 | }, 520 | resolve: async (_parent, args) => { 521 | const taskIndex = TASKS.findIndex((task) => task.id === args.id) 522 | 523 | if (taskIndex > -1) { 524 | if (args.content) { 525 | TASKS[taskIndex].content = args.content 526 | } 527 | if (args.isDone) { 528 | TASKS[taskIndex].isDone = args.isDone 529 | } 530 | 531 | return TASKS[taskIndex] 532 | 533 | } else { 534 | throw new Error(`${args.id}라는 ID를 가진 Task를 찾을 수 없습니다`) 535 | } 536 | }, 537 | }) 538 | 539 | t.field('deleteTask', { 540 | type: 'Task', 541 | args: { 542 | id: idArg({ 543 | required: true, 544 | }), 545 | }, 546 | resolve: async (_parent, args) => { 547 | const taskIndex = TASKS.findIndex((task) => task.id === args.id) 548 | 549 | if (taskIndex > -1) { 550 | const task = TASKS[taskIndex] 551 | TASKS.splice(taskIndex, 1) 552 | 553 | return task 554 | 555 | } else { 556 | throw new Error(`${args.id}라는 ID를 가진 Task를 찾을 수 없습니다`) 557 | } 558 | }, 559 | }) 560 | }, 561 | }) 562 | ``` 563 | 564 | 기존 schema 엔트리 파일을 수정해, Task 엔트리 파일을 내보냅니다. 565 | 566 | #### `/src/resolvers/index.ts` 567 | ```typescript 568 | export * from './Query' 569 | export * from './Mutation' 570 | 571 | export * from './task' 572 | ``` 573 | 574 | 개발 서버를 띄워놓은 상태라면, Nexus가 자동으로 Schema를 생성합니다.😎 575 | ```graphql 576 | ### This file was autogenerated by GraphQL Nexus 577 | ### Do not make changes to this file directly 578 | 579 | 580 | type Mutation { 581 | createTask(content: String!): Task! 582 | deleteTask(id: ID!): Task! 583 | ping: String! 584 | updateTask(content: String, id: ID!, isDone: Boolean): Task! 585 | } 586 | 587 | type Query { 588 | stage: String! 589 | task(id: ID): Task! 590 | tasks: [Task!]! 591 | } 592 | 593 | type Task { 594 | """Task 내용""" 595 | content: String! 596 | 597 | """Task 생성 시 자동 생성되는 Unique ID""" 598 | id: ID! 599 | 600 | """Task 완료 여부""" 601 | isDone: Boolean! 602 | } 603 | ``` 604 | 605 | 만들어진 API를 GraphQL Playground로 테스트 해 볼까요? 606 | ```graphql 607 | mutation { 608 | createTask(content: "Hello, World") { 609 | id 610 | } 611 | } 612 | ``` 613 | 614 | ```graphql 615 | query { 616 | tasks { 617 | id 618 | content 619 | } 620 | } 621 | ``` 622 | 623 | 다음과 같이 Nexus를 활용하여, 빠르고 안정적으로 GraphQL 개발을 시작할 수 있습니다. 624 | 625 | 또한, 기존 *Schema-First* 방식과 다르게 Nexus의 *Code-First* 방식은 TypeScript 기반으로 코드를 자유롭게 분할할 수 있기 때문에, 앞서 제기된 **GraphQL 스키마 분리 문제** 및 **스키마 정의의 중복 (코드 재사용 문제)** 를 해결할 수 있습니다. 626 | 627 | 자, 그럼 이제 우리가 만든 API 서버를 Lambda에 배포해볼까요? 628 | 629 | 630 | ## 학습 목표 확인하기 631 | - [x] GraphQL에 대해 이해한다 632 | - [x] Node.js 환경에서 GraphQL 프로젝트를 시작한다 633 | - [x] *Schema-First* vs. *Code-First* 개념에 대해 이해한다 634 | - [x] Nexus 문법을 익힌다 635 | 636 | 637 | ## 다음으로 이동 638 | 1. **GraphQL 살펴보기** ✔ 639 | 1. GraphQL이란? 640 | 2. GraphQL Type 시스템과 `Query`, `Mutation` Type 641 | 3. Nexus로 시작하는 *Code-First* GraphQL 개발 642 | 4. GraphQL Playground 643 | 5. `Task` 타입과 쿼리, 뮤테이션 만들기 644 | 2. **👉 [Serverless로 GraphQL API 배포하기](/documents/2-serverless/README.md)** 645 | 1. IAM 사용자 생성하기 646 | 2. Serverless Framework을 사용해 Node.js 프로젝트 배포하기 647 | 3. AWS에 Prisma 배포하기 (CloudFormation) 648 | 4. Prisma 사용하기 649 | 1. Prisma란? 650 | 2. Prisma 시작하기 651 | 3. Prisma Client 사용해보기 652 | 4. `nexus-prisma`를 사용해, Prisma 연결하기 653 | 5. React.js에서 GraphQL API 사용하기 654 | 6. 삭제하기 655 | 1. API 배포 삭제하기 656 | 2. CloudFormation Stack 삭제하기 657 | 3. IAM 사용자 삭제하기 658 | 659 | --- 660 | 661 | ### References 662 | - [GraphQL 영문 문서](https://graphql.org/) 663 | - [GraphQL 한국어 문서](https://graphql-kr.github.io/learn/schema/#) 664 | -------------------------------------------------------------------------------- /documents/1-graphql/images/diagram-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/1-graphql/images/diagram-1.png -------------------------------------------------------------------------------- /documents/1-graphql/images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/1-graphql/images/screenshot-1.png -------------------------------------------------------------------------------- /documents/2-serverless/README.md: -------------------------------------------------------------------------------- 1 | # 2. Serverless로 GraphQL API 배포하기 2 | 3 | ### 본 챕터의 학습 목표는 아래와 같습니다. 👏 4 | - [ ] IAM에 대해 이해한다 5 | - [ ] API Gateway와 Lambda를 통한 서버리스 API 배포에 대해 이해한다 6 | - [ ] `aws-serverless-express`를 사용해, Node.js 앱을 Lambda에 배포한다 7 | 8 | 9 | ## (1) IAM 사용자 생성하기 10 | AWS 계정 내 자원들을 내 컴퓨터에서 사용하기 위해서는 권한 관련 설정을 컴퓨터에 주입하여야 합니다. 권한들을 담는 그릇에는 사용자와 역할 두 가지가 있는데, 그 중에 우리는 새 사용자를 생성하여 해당 사용자를 Serverless Framework을 통해 컴퓨터에 주입해주도록 합니다. 11 | 12 | - [AWS Console](https://console.aws.amazon.com)에 로그인 후 `Find Services`에서 `IAM`을 검색, 클릭합니다 13 | ![](./images/screenshot-1.png) 14 | 15 | - IAM 콘솔에 들어왔다면, `Users` 메뉴로 이동, `Add user` 버튼을 클릭합니다 16 | ![](./images/screenshot-2.png) 17 | 18 | - `User name*`에 `SERVERLESS_WORKSHOP`을 적어준 뒤 `Programmatic access`에 체크해줍니다. 19 | - `Next: Permissions` 버튼을 클릭합니다. 20 | ![](./images/screenshot-3.png) 21 | 22 | - `Next: Tags` 버튼을 클릭합니다. 23 | ![](./images/screenshot-4.png) 24 | 25 | - `Next: Review` 버튼을 클릭합니다. 26 | ![](./images/screenshot-5.png) 27 | 28 | - `Create user` 버튼을 클릭합니다. 29 | ![](./images/screenshot-6.png) 30 | 31 | - `Access key ID`와 `Secret access key`를 **안전한** 로컬 메모장에 복사해 붙여넣어 둡니다. (이 창을 떠나면 다시 확인할 수 없습니다) 32 | > 해커들은 비트코인 채굴을 위해 Access Key ID와 Secret access key를 24시간 호시탐탐 노리고 있습니다. 코드 내 삽입하여 GitHub 등 코드 저장소에 업로드 할 경우, 수천~수만달러 과금이 될 수 있습니다. 33 | - `Close` 버튼을 클릭합니다. 34 | ![](./images/screenshot-7.png) 35 | 36 | - 사용자 목록 창으로 돌아오면, 바로 생성한 `SERVERLESS_WORKSHOP`을 클릭합니다. 37 | ![](./images/screenshot-8.png) 38 | 39 | - `Add inline policy`를 클릭합니다. 40 | ![](./images/screenshot-9.png) 41 | 42 | - `JSON` 탭으로 이동합니다 43 | ![](./images/screenshot-10.png) 44 | 45 | - `JSON` 탭으로 이동 한 뒤, 아래 내용을 콘솔 내 에디터에 붙여 넣습니다. 46 | 47 | ```json 48 | { 49 | "Version": "2012-10-17", 50 | "Statement": [ 51 | { 52 | "Sid": "VisualEditor0", 53 | "Effect": "Allow", 54 | "Action": [ 55 | "iam:*", 56 | "apigateway:*", 57 | "s3:*", 58 | "logs:*", 59 | "lambda:*", 60 | "cloudformation:*" 61 | ], 62 | "Resource": "*" 63 | } 64 | ] 65 | } 66 | ``` 67 | 68 | - `Review policy`를 클릭합니다 69 | ![](./images/screenshot-11.png) 70 | 71 | - `Name*` 항목에 `SERVERLESS_WORKSHOP_POLICY`라고 적어줍니다. 72 | - `Create policy` 버튼을 클릭합니다. 73 | ![](./images/screenshot-12.png) 74 | 75 | 76 | ## (2) 내 컴퓨터에 IAM 사용자 등록하기 77 | - `server` 폴더에서 아래 명령어를 CLI에 입력합니다 78 | 79 | ```bash 80 | $ npx sls config credentials --provider aws --key (내 Access Key ID) --secret (내 Secret Access Key) --profile SERVERLESS_WORKSHOP -o 81 | ``` 82 | ![](./images/screenshot-16.png) 83 | 84 | > 주입한 설정 내역은 `~/.aws/credentials`에 저장됩니다. 85 | 86 | 87 | ## (3) Serverless Framework을 사용해 Node.js 프로젝트 배포하기 88 | Serverless Framework은 IaC(Insfrastructure as Code)의 일종으로, 코드를 통해 원하는 Serverless 환경을 깔끔하게 구성할 수 있습니다. Serverless Framework은 `serverless.yml` 설정 파일을 기반으로 구동됩니다. 89 | 90 | ### 아키텍쳐 91 | 92 | 우리는 Node.js로 구성된 API를 아래의 아키텍쳐로 배포 할 것입니다. 93 | 94 | ![](./images/diagram-1.png) 95 | 96 | 해당 아키텍쳐는 이미 `/starters/server/serverless.yml`에 구성되어 있습니다. 함께 살펴볼까요? 97 | 98 | #### serverless.yml 99 | ```yaml 100 | service: serverless-graphql-workshop 101 | 102 | provider: 103 | name: aws 104 | runtime: nodejs8.10 105 | stage: ${opt:stage, 'dev'} 106 | region: ap-northeast-1 107 | profile: SERVERLESS_WORKSHOP 108 | 109 | package: 110 | individually: true 111 | excludeDevDependencies: false 112 | 113 | functions: 114 | main: 115 | name: ${self:service}-${self:provider.stage} 116 | handler: dist/serverless.handler 117 | memorySize: 1024 118 | timeout: 10 119 | environment: 120 | NODE_ENV: production 121 | package: 122 | include: 123 | - dist/serverless.js 124 | exclude: 125 | - '**' 126 | events: 127 | - http: 128 | path: / 129 | method: any 130 | - http: 131 | path: /{proxy+} 132 | method: any 133 | 134 | plugins: 135 | - serverless-apigw-binary 136 | - serverless-dotenv-plugin 137 | 138 | custom: 139 | apigwBinary: 140 | types: 141 | - '*/*' 142 | ``` 143 | 144 | `functions` 속성에서 해당 함수의 위치와 Route를 설정해주면 Serverless Framework이 자동으로 API Gateway와 Lambda를 설정합니다. 145 | 146 | 또한, `serverless-dotenv-plugin`을 통해, 현재 프로젝트 폴더의 `.env.development`, `.env.production`에 기입된 환경 변수들을 Lambda 내 환경에 추가해줍니다. 147 | 148 | 한번 배포해볼까요? 149 | 150 | ### 배포 151 | 아래의 스크립트를 `/starters/server` 내에서 실행합니다. 152 | 153 | ```bash 154 | # TypeScript 프로젝트를 JavaScript로 빌드 155 | $ yarn build 156 | 157 | # 배포 158 | $ yarn deploy:dev 159 | ``` 160 | 161 | 잠시 기다리면, 배포가 완료된 모습을 확인할 수 있습니다. 162 | 163 | 164 | ## 학습 목표 확인하기 165 | - [x] IAM에 대해 이해한다 166 | - [x] API Gateway와 Lambda를 통한 서버리스 API 배포에 대해 이해한다 167 | - [x] `aws-serverless-express`를 사용해, Node.js 앱을 Lambda에 배포한다 168 | 169 | 170 | ## 다음으로 이동 171 | 1. **GraphQL 살펴보기** ✔ 172 | 1. GraphQL이란? 173 | 2. GraphQL Type 시스템과 `Query`, `Mutation` Type 174 | 3. Nexus로 시작하는 *Code-First* GraphQL 개발 175 | 4. GraphQL Playground 176 | 5. `Task` 타입과 쿼리, 뮤테이션 만들기 177 | 2. **Serverless로 GraphQL API 배포하기** ✔ 178 | 1. IAM 사용자 생성하기 179 | 2. Serverless Framework을 사용해 Node.js 프로젝트 배포하기 180 | 3. **👉 [AWS에 Prisma 배포하기 (CloudFormation)](/documents/3-prisma-on-aws/README.md)** 181 | 4. Prisma 사용하기 182 | 1. Prisma란? 183 | 2. Prisma 시작하기 184 | 3. Prisma Client 사용해보기 185 | 4. `nexus-prisma`를 사용해, Prisma 연결하기 186 | 5. React.js에서 GraphQL API 사용하기 187 | 6. 삭제하기 188 | 1. API 배포 삭제하기 189 | 2. CloudFormation Stack 삭제하기 190 | 3. IAM 사용자 삭제하기 191 | 192 | -------------------------------------------------------------------------------- /documents/2-serverless/images/diagram-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/diagram-1.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-1.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-10.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-11.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-12.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-13.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-14.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-15.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-16.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-2.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-3.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-4.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-5.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-6.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-7.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-8.png -------------------------------------------------------------------------------- /documents/2-serverless/images/screenshot-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/2-serverless/images/screenshot-9.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/README.md: -------------------------------------------------------------------------------- 1 | # 3. AWS에 Prisma 배포하기 (CloudFormation) 2 | 3 | ### 본 챕터의 학습 목표는 아래와 같습니다. 👏 4 | - [ ] Prisma를 AWS 상에 띄운다 5 | 6 | 7 | ## Prisma Server를 띄우는 데 필요한 AWS 자원들 8 | ![](./images/diagram-1.png) 9 | Prisma를 AWS 위에서 사용하기 위해서는 10 | 11 | - VPC 등 네트워크 관련 설정 12 | - Load Balancer (ELB) 13 | - Prisma Server (Fargate) 14 | - RDBMS (RDS) 15 | 16 | 를 AWS 환경에 설정해야 합니다. 만약 이 모든 작업을 모두 자동으로 해주는 도구가 있다면 어떨까요? 17 | 18 | 19 | ## CloudFormation 템플릿으로 Prisma를 AWS에 배포하기 20 | CloudFormation 역시 Serverless Framework과 같은 IaC의 일종입니다. CloudFormation은 AWS에서 제공하는 기본 IaC로, AWS 내 모든 자원들을 한번에 배포하거나 수정, 삭제할 수 있습니다. 21 | 22 | 자, 그럼 CloudFormation을 통해 쉽게 Prisma를 AWS에 배포해볼까요? 23 | 24 | - 둘 중에 원하는 템플릿을 다운로드 받습니다. (오른쪽 클릭 후 `다른 이름으로 링크 저장...`) 25 | - MySQL 템플릿 [다운로드](https://raw.githubusercontent.com/tonyfromundefined/serverless-graphql-workshop/master/templates/prisma.mysql.yml) 26 | - Aurora Serverless 템플릿 [다운로드](https://raw.githubusercontent.com/tonyfromundefined/serverless-graphql-workshop/master/templates/prisma.aurora.serverless.yml) 27 | 28 | > 다음 템플릿에 포함된 Fargate 서비스는 과금됩니다. 💰 [요금표](https://aws.amazon.com/ko/fargate/pricing/) 29 | 30 | > [Aurora](https://aws.amazon.com/ko/rds/aurora/)는 AWS에서 만든 MySQL 호환 RDBMS입니다. [Aurora Serverless](https://aws.amazon.com/ko/rds/aurora/serverless/)를 사용하게 되면 인스턴스 관리가 필요없는 DB를 사용할 수 있습니다. (Aurora Serverless는 프리티어가 제공되지 않으므로 과금됩니다 💰 [요금표](https://aws.amazon.com/ko/rds/aurora/serverless/)) 31 | 32 | - [AWS Console](https://console.aws.amazon.com)에 로그인 후 `Find Services`에서 `CloudFormation`을 검색, 클릭합니다. 33 | ![](./images/screenshot-1.png) 34 | 35 | - `Create stack`을 클릭합니다. 36 | ![](./images/screenshot-2.png) 37 | 38 | - `Upload a template file`을 선택 한 뒤, `Choose file`을 클릭해 다운로드 한 CloudFormation 템플릿 파일을 선택합니다. 39 | - `Next`를 클릭합니다. 40 | ![](./images/screenshot-3.png) 41 | 42 | - `Stack name`에 `Prisma`를 적어줍니다. 43 | ![](./images/screenshot-4.png) 44 | 45 | - `DatabaseName`에 `prisma`를 적어줍니다. 46 | - `DatabasePassword`에 원하는 무작위 비밀번호를 적어줍니다. 47 | ![](./images/screenshot-5.png) 48 | 49 | - `PrismaManagementApiSecret`에 원하는 비밀번호를 적은 뒤에 **안전한** 메모장에 옮겨 적어 놓습니다. 50 | - `Next`를 클릭합니다. 51 | ![](./images/screenshot-6.png) 52 | 53 | - 아래로 스크롤을 내려, 한번 더 `Next`를 클릭합니다. 54 | ![](./images/screenshot-7.png) 55 | ![](./images/screenshot-8.png) 56 | 57 | - 작성한 내용을 검토합니다. 58 | ![](./images/screenshot-9.png) 59 | 60 | - 아래로 스크롤을 내려, `I acknowledge that AWS CloudFormation might create IAM resouces`에 체크 한 뒤, `Create stack`을 클릭합니다. 61 | ![](./images/screenshot-10.png) 62 | 63 | - 스택 생성을 진행합니다. 64 | ![](./images/screenshot-11.png) 65 | 66 | - 스택 생성이 완료되면, `Outputs` 탭에서 Prisma Endpoint를 확인할 수 있습니다. 67 | ![](./images/screenshot-12.png) 68 | ![](./images/screenshot-13.png) 69 | 70 | - 웹브라우저에서 생성된 Prisma Endpoint으로 접속해보면, 빈 GraphQL Playground를 확인할 수 있습니다. 71 | ![](./images/screenshot-14.png) 72 | 73 | > ECS Service Linked Role 관련 오류가 발생한 경우 74 | > 75 | > ```bash 76 | > $ aws iam create-service-linked-role --aws-service-name ecs.amazonaws.com 77 | > ``` 78 | > 79 | > 를 터미널에 입력합니다 80 | 81 | 82 | ## 학습 목표 확인하기 83 | - [x] Prisma를 AWS 상에 띄운다 84 | 85 | 86 | ## 다음으로 이동 87 | 1. **GraphQL 살펴보기** ✔ 88 | 1. GraphQL이란? 89 | 2. GraphQL Type 시스템과 `Query`, `Mutation` Type 90 | 3. Nexus로 시작하는 *Code-First* GraphQL 개발 91 | 4. GraphQL Playground 92 | 5. `Task` 타입과 쿼리, 뮤테이션 만들기 93 | 2. **Serverless로 GraphQL API 배포하기** ✔ 94 | 1. IAM 사용자 생성하기 95 | 2. Serverless Framework을 사용해 Node.js 프로젝트 배포하기 96 | 3. **AWS에 Prisma 배포하기 (CloudFormation)** ✔ 97 | 4. **👉 [Prisma 사용하기](/documents/4-prisma/README.md)** 98 | 1. Prisma란? 99 | 2. Prisma 시작하기 100 | 3. Prisma Client 사용해보기 101 | 4. `nexus-prisma`를 사용해, Prisma 연결하기 102 | 5. React.js에서 GraphQL API 사용하기 103 | 6. 삭제하기 104 | 1. API 배포 삭제하기 105 | 2. CloudFormation Stack 삭제하기 106 | 3. IAM 사용자 삭제하기 107 | -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/diagram-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/diagram-1.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-1.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-10.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-11.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-12.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-13.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-14.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-2.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-3.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-4.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-5.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-6.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-7.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-8.png -------------------------------------------------------------------------------- /documents/3-prisma-on-aws/images/screenshot-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/3-prisma-on-aws/images/screenshot-9.png -------------------------------------------------------------------------------- /documents/4-prisma/README.md: -------------------------------------------------------------------------------- 1 | # 4. Prisma 사용하기 2 | 3 | ### 본 챕터의 학습 목표는 아래와 같습니다. 👏 4 | - [ ] Prisma에 대해 이해한다 5 | - [ ] Prisma CLI를 설치하고, Migration(`prisma deploy`)를 수행해본다 6 | - [ ] Prisma Client를 사용해본다 7 | - [ ] Nexus Prisma를 사용해본다 8 | 9 | 10 | ## (1) Prisma란? 11 | 12 | ### 출발 13 | Prisma는 *Graphcool*이라는 서비스로부터 시작되었습니다. 14 | 15 | #### Graphcool 16 | ![](./images/diagram-1.png) 17 | 18 | *Graphcool*은 GraphQL 기반 오픈소스 BaaS(Backend as a service)로, 프론트엔드 개발자들이 더 쉽게 백엔드를 개발할 수 있도록 도와주는 서비스입니다. GraphQL SDL(Schema Definition Language) 기반으로 데이터 모델을 구축하면 *Graphcool*이 이를 인식, 오브젝트 타입부터 CRUD(Create, Read, Update, Delete) 쿼리/뮤테이션을 생성해주는 서비스입니다. Prisma는 해당 서비스를 2년간 운영하면서 **몇가지 문제점들**을 만났습니다. 19 | 20 | - *Graphcool*은 사용하기 쉽기 때문에 큰 사랑을 받았습니다. 개발자는 빠른 프로토타이핑을 위해 이 도구를 적극적으로 사용했지만, 실제 프로덕션 환경에서 자체 GraphQL 서버를 구축 할 때는 이 도구를 사용하기 힘들었습니다. 21 | - 백엔드 개발자는 보다 많은 제어와 유연성을 원했습니다. 예를 들면 다음과 같습니다. 22 | - API 계층에서 데이터베이스 분리 23 | - 자신만의 도메인 중심 GraphQL 스키마 정의 (일반적인 CRUD 대신) 24 | - 프로그래밍 언어, 프레임 워크, 테스트 및 CI/CD 도구 선택 시 유연성 제공 25 | 26 | #### Prisma 1.0 + Prisma Bindings 27 | ![](./images/diagram-2.png) 28 | 이를 해결하기 위해, *Graphcool*의 *Query Engine* 컴포넌트를 가지고 *Prisma bindings*라는 새로운 서비스를 출시하게 되었습니다. 이를 통해, Prisma가 SDL(Schema Definition Language)을 기반으로 자동으로 생성해준 GraphQL CRUD 쿼리/뮤테이션을 GraphQL API에 포함하여 빠르게 백엔드를 개발할 수 있습니다. 29 | 30 | 또한 회사 브랜드 역시 *Graphcool*에서 *Prisma*로 완전히 전환하고, 앞으로 Prisma에 더 집중하게 됩니다. 31 | 32 | #### Prisma Client 33 | ![](./images/diagram-3.png) 34 | 이에 더 나아가서, Prisma는 Prisma Client라는 신규 라이브러리를 통해 API 환경이 GraphQL이 아니더라도 ORM과 같이 Prisma를 사용할 수 있게 하였습니다. GraphQL의 타입 시스템을 기반으로 만들어진 Type-Safe ORM은 백엔드 개발에 날개를 달아줍니다. 35 | 36 | 일반적인 ORM과 비슷하게, 37 | 38 | - 다음과 같이 SDL을 통해 타입을 정의하고 39 | 40 | #### `datamodel.prisma` 41 | ```graphql 42 | type Task { 43 | id: ID! 44 | content: String! 45 | isDone: Boolean! 46 | } 47 | ``` 48 | 49 | - Migration 명령어를 입력하면 50 | 51 | #### CLI 52 | ```bash 53 | $ prisma deploy 54 | ``` 55 | 56 | Prisma는 해당 데이터 모델을 기반으로 RDBMS에 테이블을 생성하고, Prisma Server 내부에, 해당 데이터 모델에 대한 CRUD 쿼리/뮤테이션을 생성합니다. 57 | 58 | 59 | ## (2) Prisma 시작하기 60 | 자, 그럼 한번 Prisma를 사용해볼까요? 61 | 62 | - Prisma CLI를 설치합니다. 63 | 64 | ```bash 65 | $ npm i -g prisma 66 | ``` 67 | 68 | - 프로젝트 폴더로 내에 `.env.development` 파일과 `.env.production` 파일을 수정해줍니다. 69 | - 전에 배포한 Prisma의 엔드포인트를 넣어주세요. 70 | - 엔드포인트 뒤에 서비스 이름은 알맞게 지어주시고, stage는 각각 `dev`와 `prod`를 적어주세요. (`http://엔드포인트/서비스/스테이지`) 71 | - 예시: `http://prisma.ap-northeast-1.elb.amazonaws.com/serverless/dev` 72 | - 마찬가지로 Prisma의 `managementApiSecret`을 넣어줍니다. 73 | 74 | #### `.env.development` 75 | ``` 76 | STAGE="development" 77 | IS_PLAYGROUND_ENABLED="1" 78 | IS_TRACING_ENABLED="1" 79 | 80 | PRISMA_ENDPOINT="http://prisma.ap-northeast-1.elb.amazonaws.com/serverless/dev" 81 | PRISMA_MANAGEMENT_API_SECRET="serverless" 82 | ``` 83 | 84 | #### `.env.production` 85 | ``` 86 | STAGE="production" 87 | IS_PLAYGROUND_ENABLED="0" 88 | IS_TRACING_ENABLED="0" 89 | 90 | PRISMA_ENDPOINT="http://prisma.ap-northeast-1.elb.amazonaws.com/serverless/prod" 91 | PRISMA_MANAGEMENT_API_SECRET="serverless" 92 | ``` 93 | 94 | - 다음과 같이 CLI에 입력하면, `datamodel.prisma` 파일을 통해 RDBMS에 테이블을 생성하고, Prisma에 오브젝트 타입과 CRUD 쿼리/뮤테이션을 생성합니다. 95 | 96 | ```bash 97 | yarn prisma:deploy:dev 98 | ``` 99 | ![](./images/screenshot-1.png) 100 | 101 | - Prisma가 RDBMS에 해당 데이터 모델과 일치하는 테이블을 생성했습니다. 102 | ![](./images/screenshot-2.png) 103 | 104 | - Prisma에 직접 접근하기 위해서는 토큰 발급이 필요합니다. CLI에서 다음과 같이 입력하면, 새 토큰을 발급 받을 수 있습니다. 105 | 106 | ```bash 107 | yarn prisma:token:dev 108 | ``` 109 | ![](./images/screenshot-3.png) 110 | 111 | 112 | - CLI 결과로 출력된 엔드포인트를 웹브라우저에 입력하면, Prisma 내부의 GraphQL Playground를 확인할 수 있습니다. GraphQL Playground 내 하단의 `HTTP HEADERS`내에 발급한 토큰을 삽입해주면, Prisma가 생성한 CRUD 쿼리/뮤테이션을 확인할 수 있습니다. 113 | 114 | ```json 115 | { 116 | "Authorization": "Bearer {생성된 토큰}" 117 | } 118 | ``` 119 | ![](./images/screenshot-4.png) 120 | 121 | - 또, CLI 결과로 출력된 `Prisma Admin` 링크로 들어가면, 데이터를 생성, 수정, 삭제할 수 있는 어드민 페이지를 확인할 수 있습니다. 우측 상단 설정 버튼을 눌러 토큰을 입력해줍니다. 122 | ![](./images/screenshot-5.png) 123 | ![](./images/screenshot-6.png) 124 | 125 | - 올바른 토큰임이 확인되면, 데이터를 생성, 수정, 삭제할 수 있습니다. 126 | ![](./images/screenshot-7.png) 127 | 128 | > CLI에 다음 명령어를 입력하면 토큰 입력 과정 없이 Prisma Admin을 바로 사용할 수 있습니다. 129 | > 130 | > ```bash 131 | > $ yarn prisma:admin:dev 132 | > ``` 133 | 134 | 135 | ## (3) Prisma Client 사용해보기 136 | 아까 만든 `task` 관련 Resolver를 Prisma Client를 사용해 구현해볼까요? 137 | 138 | - 리졸버 내 기존 목업 DB를 삭제하고 Prisma Client를 이용해 데이터를 가져와서 `return` 합니다. 139 | 140 | #### `/src/resolvers/task/Query.ts` 141 | ```typescript 142 | import { extendType, idArg } from 'nexus' 143 | import { prisma } from '../../generated/prisma' 144 | 145 | export const TaskQueries = extendType({ 146 | type: 'Query', 147 | definition(t) { 148 | t.field('task', { 149 | type: 'Task', 150 | args: { 151 | id: idArg({ 152 | required: true, 153 | }), 154 | }, 155 | resolve: async (_parent, args) => { 156 | const task = await prisma.task({ 157 | id: args.id, 158 | }) 159 | 160 | if (task) { 161 | return task 162 | 163 | } else { 164 | throw new Error(`${args.id}를 가진 Task를 찾을 수 없습니다`) 165 | } 166 | }, 167 | }) 168 | 169 | t.list.field('tasks', { 170 | type: 'Task', 171 | resolve: () => { 172 | return prisma.tasks() 173 | }, 174 | }) 175 | }, 176 | }) 177 | ``` 178 | 179 | - 마찬가지로 `createTask`, `updateTask`, `deleteTask` 내에서도 기존 목업 DB 조작 로직을 삭제하고 Prisma Client를 이용해 데이터를 수정해 `return` 합니다. 180 | 181 | #### `/src/resolvers/task/Mutation.ts` 182 | ```typescript 183 | import { booleanArg, extendType, idArg, stringArg } from 'nexus' 184 | import { prisma } from '../../generated/prisma' 185 | 186 | export const TaskMutations = extendType({ 187 | type: 'Mutation', 188 | definition(t) { 189 | t.field('createTask', { 190 | type: 'Task', 191 | args: { 192 | content: stringArg({ 193 | required: true, 194 | }), 195 | }, 196 | resolve: (_parent, args) => { 197 | return prisma.createTask({ 198 | content: args.content, 199 | isDone: false, 200 | }) 201 | }, 202 | }) 203 | 204 | t.field('updateTask', { 205 | type: 'Task', 206 | args: { 207 | id: idArg({ 208 | required: true, 209 | }), 210 | content: stringArg(), 211 | isDone: booleanArg(), 212 | }, 213 | resolve: (_parent, args) => { 214 | return prisma.updateTask({ 215 | data: { 216 | content: args.content, 217 | isDone: args.isDone, 218 | }, 219 | where: { 220 | id: args.id, 221 | }, 222 | }) 223 | }, 224 | }) 225 | 226 | t.field('deleteTask', { 227 | type: 'Task', 228 | args: { 229 | id: idArg({ 230 | required: true, 231 | }), 232 | }, 233 | resolve: (_parent, args) => { 234 | return prisma.deleteTask({ 235 | id: args.id, 236 | }) 237 | }, 238 | }) 239 | }, 240 | }) 241 | ``` 242 | 243 | - 기존에 생성해줬던 목업 DB를 삭제합니다. 244 | 245 | #### `/src/resolvers/task/index.ts` 246 | ```typescript 247 | import { objectType } from 'nexus' 248 | 249 | export const Task = objectType({ 250 | name: 'Task', 251 | definition(t) { 252 | t.id('id', { 253 | description: 'Task 생성 시 자동 생성되는 Unique ID', 254 | }) 255 | t.string('content', { 256 | description: 'Task 내용', 257 | }) 258 | t.boolean('isDone', { 259 | description: 'Task 완료 여부', 260 | }) 261 | }, 262 | }) 263 | 264 | export * from './Query' 265 | export * from './Mutation' 266 | ``` 267 | 268 | 269 | ## (4) Nexus Prisma 사용해, Prisma를 API에 연결하기 270 | 생각해보니, `Task` 타입과 CRUD GraphQL 쿼리/뮤테이션들을 Prisma가 이미 만들었었죠! Nexus Prisma를 사용해 생성된 타입과 쿼리/뮤테이션을 그대로 이용해볼까요? 271 | 272 | - 기존에 설정된 Nexus의 `makeSchema`를 Nexus Prisma의 `makePrismaSchema`로 변경해줍니다. 273 | - `makePrismaSchema`에 Prisma Client와 `datamodelInfo`를 주입해줍니다. 274 | 275 | #### `/src/app.ts` 276 | ```typescript 277 | import { ApolloServer } from 'apollo-server-express' 278 | import cors from 'cors' 279 | import express from 'express' 280 | import { makePrismaSchema } from 'nexus-prisma' 281 | import path from 'path' 282 | import datamodelInfo from './generated/nexus-prisma' 283 | import { prisma } from './generated/prisma' 284 | 285 | import * as types from './resolvers' 286 | 287 | const playground = !!Number(process.env.IS_PLAYGROUND_ENABLED || '0') 288 | const tracing = !!Number(process.env.IS_TRACING_ENABLED || '0') 289 | 290 | const app = express() 291 | 292 | app.use(cors()) 293 | 294 | app.get('/', (_req, res) => { 295 | return res.json('ok') 296 | }) 297 | 298 | const server = new ApolloServer({ 299 | schema: makePrismaSchema({ 300 | types, 301 | prisma: { 302 | client: prisma, 303 | datamodelInfo, 304 | }, 305 | outputs: { 306 | schema: path.resolve('./src/generated', 'schema.graphql'), 307 | typegen: path.resolve('./src/generated', 'nexus.ts'), 308 | }, 309 | }), 310 | introspection: playground, 311 | playground, 312 | tracing, 313 | }) 314 | 315 | server.applyMiddleware({ 316 | app, 317 | }) 318 | 319 | export default app 320 | ``` 321 | 322 | - Nexus의 `objectType` 함수를 Nexus Prisma의 `prismaObjectType`으로 변경해줍니다. 323 | - `prismaObjectType`의 definition 내에 `t.prismaFields`를 사용할 수 있게되며, Prisma가 생성한 타입 내 속성들을 노출할 수 있습니다. 324 | 325 | #### `/src/resolvers/task/index.ts` 326 | ```typescript 327 | import { prismaObjectType } from 'nexus-prisma' 328 | 329 | export const Task = prismaObjectType({ 330 | name: 'Task', 331 | definition(t) { 332 | t.prismaFields(['*']) 333 | 334 | // 또는 다음과 같이 원하는 필드만 노출할 수 있습니다. 335 | // t.prismaFields(['content', 'isDone']) 336 | }, 337 | }) 338 | 339 | export * from './Query' 340 | export * from './Mutation' 341 | ``` 342 | 343 | - `task`와 `tasks` 쿼리를 연결해줍니다. 344 | #### `/src/resolvers/task/Query.ts` 345 | ```typescript 346 | import { prismaExtendType } from 'nexus-prisma' 347 | 348 | export const TaskQueries = prismaExtendType({ 349 | type: 'Query', 350 | definition(t) { 351 | t.prismaFields(['task', 'tasks']) 352 | }, 353 | }) 354 | ``` 355 | 356 | - `createTask`, `updateTask`, `deleteTask` 뮤테이션을 연결해줍니다. 357 | 358 | #### `/src/resolvers/task/Mutation.ts` 359 | ```typescript 360 | import { prismaExtendType } from 'nexus-prisma' 361 | 362 | export const TaskMutations = prismaExtendType({ 363 | type: 'Mutation', 364 | definition(t) { 365 | t.prismaFields([ 366 | 'createTask', 367 | 'updateTask', 368 | 'deleteTask', 369 | ]) 370 | }, 371 | }) 372 | ``` 373 | 374 | > 다음과 같이 기본적인 CRUD를 Prisma를 이용해 쉽게 만들어 붙일 수 있습니다. 또한, Nexus 내부의 TypeScript 타이핑을 통해 간혹 발생할 수 있는 실수를 줄여줍니다. 375 | 376 | - 완성된 서버를 서버리스 환경으로 다시 배포합니다. 377 | 378 | ```bash 379 | $ yarn build 380 | $ yarn deploy:dev 381 | ``` 382 | 383 | 자 이제 API를 완성하고 배포까지 끝냈으니, 웹 클라이언트에서 GraphQL을 어떻게 사용할 수 있는지 알아볼까요? 384 | 385 | ## 학습 목표 확인하기 386 | - [x] Prisma에 대해 이해한다 387 | - [x] Prisma CLI를 설치하고, Migration(`prisma deploy`)를 수행해본다 388 | - [x] Prisma Client를 사용해본다 389 | - [x] Nexus Prisma를 사용해본다 390 | 391 | 392 | ## 다음으로 이동 393 | 1. **GraphQL 살펴보기** ✔ 394 | 1. GraphQL이란? 395 | 2. GraphQL Type 시스템과 `Query`, `Mutation` Type 396 | 3. Nexus로 시작하는 *Code-First* GraphQL 개발 397 | 4. GraphQL Playground 398 | 5. `Task` 타입과 쿼리, 뮤테이션 만들기 399 | 2. **Serverless로 GraphQL API 배포하기** ✔ 400 | 1. IAM 사용자 생성하기 401 | 2. Serverless Framework을 사용해 Node.js 프로젝트 배포하기 402 | 3. **AWS에 Prisma 배포하기 (CloudFormation)** ✔ 403 | 4. **Prisma 사용하기** ✔ 404 | 1. Prisma란? 405 | 2. Prisma 시작하기 406 | 3. Prisma Client 사용해보기 407 | 4. `nexus-prisma`를 사용해, Prisma 연결하기 408 | 5. **👉 [React.js에서 GraphQL API 사용하기](/documents/5-react-graphql/README.md)** 409 | 6. 삭제하기 410 | 1. API 배포 삭제하기 411 | 2. CloudFormation Stack 삭제하기 412 | 3. IAM 사용자 삭제하기 413 | 414 | --- 415 | 416 | ### References 417 | - [How Prisma and GraphQL fit together](https://www.prisma.io/blog/prisma-and-graphql-mfl5y2r7t49c/) 418 | -------------------------------------------------------------------------------- /documents/4-prisma/images/diagram-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/diagram-1.png -------------------------------------------------------------------------------- /documents/4-prisma/images/diagram-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/diagram-2.png -------------------------------------------------------------------------------- /documents/4-prisma/images/diagram-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/diagram-3.png -------------------------------------------------------------------------------- /documents/4-prisma/images/diagram-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/diagram-4.png -------------------------------------------------------------------------------- /documents/4-prisma/images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/screenshot-1.png -------------------------------------------------------------------------------- /documents/4-prisma/images/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/screenshot-2.png -------------------------------------------------------------------------------- /documents/4-prisma/images/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/screenshot-3.png -------------------------------------------------------------------------------- /documents/4-prisma/images/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/screenshot-4.png -------------------------------------------------------------------------------- /documents/4-prisma/images/screenshot-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/screenshot-5.png -------------------------------------------------------------------------------- /documents/4-prisma/images/screenshot-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/screenshot-6.png -------------------------------------------------------------------------------- /documents/4-prisma/images/screenshot-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/4-prisma/images/screenshot-7.png -------------------------------------------------------------------------------- /documents/5-react-graphql/README.md: -------------------------------------------------------------------------------- 1 | # 5. React.js에서 GraphQL API 사용하기 2 | 3 | ### 본 챕터의 학습 목표는 아래와 같습니다. 👏 4 | - [ ] GraphQL Code Generator로 React 코드를 생성하여 사용해본다. 5 | - [ ] End-to-end Typing 개념에 대해 이해한다. 6 | 7 | 8 | ## React 클라이언트 프로젝트 시작하기 9 | 이제 만든 API를 웹 클라이언트에서 사용해볼까요? 클라이언트 프로젝트로 이동해보겠습니다. 해당 프로젝트는 본 레포 안의 `/starters/client` 폴더 안에서 찾을 수 있습니다. 10 | 11 | ### 폴더 및 파일 구조 살펴보기 12 | > 본 워크숍은 GraphQL 워크숍으로 클라이언트 기술에 대해 깊게 살펴보지 않을 것입니다. 따라서 클라이언트 프로젝트 구조는 따로 설명하지 않습니다. 13 | 14 | ### 시작하기 15 | - `/starters/client/` 폴더로 이동합니다. 16 | 17 | ```bash 18 | $ cd ./starters/client 19 | ``` 20 | 21 | - 프로젝트에 필요한 라이브러리를 설치합니다. 22 | 23 | ```bash 24 | # 라이브러리 설치하기 25 | $ yarn 26 | ``` 27 | 28 | > 해당 작업이 완료되면 `node_modules` 폴더가 생성되고 해당 폴더 아래에 필요한 라이브러리들이 위치하게 됩니다. 29 | 30 | - 프로젝트 폴더에 아래 두 파일을 생성합니다. 31 | - `NEXT_APP_GRAPHQL_ENDPOINT`에 우리가 배포한 GraphQL API 엔드포인트를 넣어줍니다. 32 | 33 | #### `.env.development` 34 | 35 | ``` 36 | NEXT_APP_STAGE = "development" 37 | NEXT_APP_GRAPHQL_ENDPOINT = "https://fyeitajxaa.execute-api.ap-northeast-1.amazonaws.com/dev/graphql" 38 | NEXT_APP_VERSION = "0.0.1" 39 | ``` 40 | 41 | #### `.env.production` 42 | 43 | ``` 44 | NEXT_APP_STAGE = "production" 45 | NEXT_APP_GRAPHQL_ENDPOINT = "https://fyeitajxaa.execute-api.ap-northeast-1.amazonaws.com/dev/graphql" 46 | NEXT_APP_VERSION = "0.0.1" 47 | ``` 48 | 49 | > 두 파일을 통해 각 스테이지에서 환경 변수를 설정할 수 있습니다. 50 | 51 | - 개발 서버 시작하기 52 | 53 | ```bash 54 | $ yarn dev 55 | ``` 56 | > 개발 서버를 시작 한 뒤에는 브라우저에서 `http://localhost:3000`로 접근할 수 있습니다. 57 | 58 | 59 | ## GraphQL API 사용하기 60 | 자 그럼 우리가 만들어서 AWS에 올려놓은 GraphQL API를 사용해볼까요? 61 | 62 | ### 사용 할 쿼리 작성하기 63 | - `/src/queries` 폴더를 만듭니다. 64 | - 해당 폴더 내에 다음 네 개의 파일을 만들어줍니다. 65 | 66 | #### `/src/queries/getTasks.graphql` 67 | ```graphql 68 | query getTasks { 69 | tasks { 70 | id 71 | content 72 | isDone 73 | } 74 | } 75 | ``` 76 | 77 | #### `/src/queries/createTask.graphql` 78 | ```graphql 79 | mutation createTask($data: TaskCreateInput!) { 80 | createTask(data: $data) { 81 | id 82 | } 83 | } 84 | ``` 85 | 86 | #### `/src/queries/updateTask.graphql` 87 | ```graphql 88 | mutation updateTask($data: TaskUpdateInput!, $where: TaskWhereUniqueInput!) { 89 | updateTask(data: $data, where: $where) { 90 | id 91 | } 92 | } 93 | ``` 94 | 95 | #### `/src/queries/deleteTask.graphql` 96 | ```graphql 97 | mutation deleteTask($where: TaskWhereUniqueInput!) { 98 | deleteTask(where: $where) { 99 | id 100 | } 101 | } 102 | ``` 103 | 104 | - React 프로젝트 내에서 사용할 코드 조각들을 GraphQL Code Generator를 통해 생성해줍니다. 105 | 106 | ```bash 107 | $ yarn generate 108 | ``` 109 | 110 | ### GraphQL Code Generator가 생성한 코드 조각 사용하기 111 | - `/src/pages/index.tsx`를 다음과 같이 수정해줍니다. 112 | 113 | ```tsx 114 | import { useState } from 'react' 115 | import { 116 | IGetTasksQuery, 117 | useCreateTaskMutation, 118 | useDeleteTaskMutation, 119 | useGetTasksQuery, 120 | useUpdateTaskMutation, 121 | } from '~/generated/graphql' 122 | 123 | export default function PageIndex() { 124 | const [content, setContent] = useState('') 125 | 126 | const { error, loading, data, refetch } = useGetTasksQuery() 127 | const createTask = useCreateTaskMutation() 128 | 129 | const onInputChange = (event: React.ChangeEvent) => { 130 | setContent(event.target.value) 131 | } 132 | 133 | const onAddButtonClick = async () => { 134 | await createTask({ 135 | variables: { 136 | data: { 137 | content, 138 | isDone: false, 139 | }, 140 | }, 141 | }) 142 | await refetch() 143 | } 144 | 145 | return ( 146 |
147 |

📝 할 일 목록

148 |
149 | 150 | 151 |
152 | 158 |
159 | AWSKRUG 슬랙 가입하기 😎: https://slack.awskr.org 160 |
161 |
162 | ) 163 | } 164 | 165 | interface ITasksProps { 166 | error: any 167 | loading: boolean 168 | data?: IGetTasksQuery 169 | refetch: () => any 170 | } 171 | function Tasks(props: ITasksProps) { 172 | const updateTask = useUpdateTaskMutation() 173 | const deleteTask = useDeleteTaskMutation() 174 | 175 | if (props.error) { 176 | return ( 177 |
error
178 | ) 179 | } 180 | 181 | if (props.loading || !props.data || !props.data.tasks) { 182 | return ( 183 |
loading...
184 | ) 185 | } 186 | 187 | return ( 188 |
    189 | {props.data.tasks.map((task) => { 190 | const onCompleteButtonClick = async () => { 191 | await updateTask({ 192 | variables: { 193 | data: { 194 | isDone: true, 195 | }, 196 | where: { 197 | id: task.id, 198 | }, 199 | }, 200 | }) 201 | await props.refetch() 202 | } 203 | 204 | const onDeleteButtonClick = async () => { 205 | await deleteTask({ 206 | variables: { 207 | where: { 208 | id: task.id, 209 | }, 210 | }, 211 | }) 212 | await props.refetch() 213 | } 214 | 215 | return ( 216 |
  • 217 | {task.id} 218 | {'\u00A0'}|{'\u00A0'} 219 | {task.content} 220 | {task.isDone && {'\u00A0'}✔} 221 | {'\u00A0'} 222 | 223 | 224 |
  • 225 | ) 226 | })} 227 |
228 | ) 229 | } 230 | ``` 231 | 232 | 코드를 살펴보시면, TypeScript로 안전하게 타입이 지켜지는 모습을 확인할 수 있습니다. 이렇게 GraphQL과 TypeScript를 사용하면, RDBMS 스키마부터 클라이언트 쿼리까지 모든 부분에서 엄격하게 타입 체킹을 확인할 수 있고 이를 *End-to-end Typing*이라고 표현합니다. 233 | 234 | 이런식으로 *Nexus*와 *GraphQL Code Generator*를 사용하면, Prisma와 GraphQL, TypeScript의 타입 시스템을 자동으로 통합할 수 있고, 이를 통해 우리는 제품 전 영역에 걸쳐 안전하게 타입을 지킬 수 있게 됩니다. 이는 제품을 더 안전하게 만들고 이에 더불어 IDE(통합 개발 환경)에서 지원하는 각종 편의 기능을 통해 더 빠르게 제품을 개발할 수 있습니다. 235 | 236 | ## 완료 🥳 237 | 축하합니다!🎉 서버리스 GraphQL 핸즈온을 훌륭하게 마치셨습니다! Prisma를 올리는데 사용한 AWS Fargate는 과금됩니다. 미리 준비된 [삭제 가이드](/documents/6-delete/README.md)를 참고하여 집에 돌아가시기 전에 반드시 삭제해주세요. 238 | ![](./images/screenshot-1.png) 239 | 240 | 241 | ## 학습 목표 확인하기 242 | - [x] GraphQL Code Generator로 React 코드를 생성하여 사용해본다. 243 | - [x] End-to-end Typing 개념에 대해 이해한다. 244 | 245 | 246 | ## 다음으로 이동 247 | 1. **GraphQL 살펴보기** ✔ 248 | 1. GraphQL이란? 249 | 2. GraphQL Type 시스템과 `Query`, `Mutation` Type 250 | 3. Nexus로 시작하는 *Code-First* GraphQL 개발 251 | 4. GraphQL Playground 252 | 5. `Task` 타입과 쿼리, 뮤테이션 만들기 253 | 2. **Serverless로 GraphQL API 배포하기** ✔ 254 | 1. IAM 사용자 생성하기 255 | 2. Serverless Framework을 사용해 Node.js 프로젝트 배포하기 256 | 3. **AWS에 Prisma 배포하기 (CloudFormation)** ✔ 257 | 4. **Prisma 사용하기** ✔ 258 | 1. Prisma란? 259 | 2. Prisma 시작하기 260 | 3. Prisma Client 사용해보기 261 | 4. `nexus-prisma`를 사용해, Prisma 연결하기 262 | 5. **React.js에서 GraphQL API 사용하기** ✔ 263 | 6. **[삭제하기](/documents/6-delete/README.md)** 264 | 1. API 배포 삭제하기 265 | 2. CloudFormation Stack 삭제하기 266 | 3. IAM 사용자 삭제하기 267 | -------------------------------------------------------------------------------- /documents/5-react-graphql/images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/5-react-graphql/images/screenshot-1.png -------------------------------------------------------------------------------- /documents/6-delete/README.md: -------------------------------------------------------------------------------- 1 | # 6. 삭제하기 2 | 3 | ## (1) API 배포 삭제하기 4 | - `/starters/server/` 폴더로 이동합니다. 5 | 6 | ```bash 7 | $ cd ./starters/server 8 | ``` 9 | 10 | - 다음 스크립트를 CLI에 입력합니다. 11 | 12 | ```bash 13 | $ yarn undeploy:dev 14 | ``` 15 | 16 | ## (2) CloudFormation Stack 삭제하기 17 | - [AWS Console](https://console.aws.amazon.com)에 로그인 후 `Find Services`에서 `CloudFormation`을 검색, 클릭합니다. 18 | ![](./images/screenshot-1.png) 19 | 20 | - 챕터 3에서 생성한 CloudFormation 스택을 선택 한 뒤 `DELETE` 버튼을 클릭합니다. 21 | ![](./images/screenshot-2.png) 22 | 23 | - `Delete stack`을 클릭합니다. 24 | ![](./images/screenshot-3.png) 25 | 26 | ## (3) IAM 사용자 삭제하기 27 | - [AWS Console](https://console.aws.amazon.com)에 로그인 후 `Find Services`에서 `IAM`을 검색, 클릭합니다. 28 | ![](./images/screenshot-4.png) 29 | 30 | - `Users` 메뉴로 이동합니다 31 | ![](./images/screenshot-5.png) 32 | 33 | - 전에 생성한 `SERVERLESS_WORKSHOP`을 선택 한 뒤, `Delete user`를 클릭합니다. 34 | ![](./images/screenshot-6.png) 35 | 36 | - `One or more...`를 선택 한 뒤, `Yes, delete`를 클릭합니다. 37 | ![](./images/screenshot-7.png) 38 | 39 | 40 | ## 다음으로 이동 41 | 1. **GraphQL 살펴보기** ✔ 42 | 1. GraphQL이란? 43 | 2. GraphQL Type 시스템과 `Query`, `Mutation` Type 44 | 3. Nexus로 시작하는 *Code-First* GraphQL 개발 45 | 4. GraphQL Playground 46 | 5. `Task` 타입과 쿼리, 뮤테이션 만들기 47 | 2. **Serverless로 GraphQL API 배포하기** ✔ 48 | 1. IAM 사용자 생성하기 49 | 2. Serverless Framework을 사용해 Node.js 프로젝트 배포하기 50 | 3. **AWS에 Prisma 배포하기 (CloudFormation)** ✔ 51 | 4. **Prisma 사용하기** ✔ 52 | 1. Prisma란? 53 | 2. Prisma 시작하기 54 | 3. Prisma Client 사용해보기 55 | 4. `nexus-prisma`를 사용해, Prisma 연결하기 56 | 5. **React.js에서 GraphQL API 사용하기** ✔ 57 | 6. **삭제하기** ✔ 58 | 1. API 배포 삭제하기 59 | 2. CloudFormation Stack 삭제하기 60 | 3. IAM 사용자 삭제하기 61 | 0. **[처음으로 돌아가기](/README.md)** 62 | -------------------------------------------------------------------------------- /documents/6-delete/images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/6-delete/images/screenshot-1.png -------------------------------------------------------------------------------- /documents/6-delete/images/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/6-delete/images/screenshot-2.png -------------------------------------------------------------------------------- /documents/6-delete/images/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/6-delete/images/screenshot-3.png -------------------------------------------------------------------------------- /documents/6-delete/images/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/6-delete/images/screenshot-4.png -------------------------------------------------------------------------------- /documents/6-delete/images/screenshot-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/6-delete/images/screenshot-5.png -------------------------------------------------------------------------------- /documents/6-delete/images/screenshot-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/6-delete/images/screenshot-6.png -------------------------------------------------------------------------------- /documents/6-delete/images/screenshot-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/documents/6-delete/images/screenshot-7.png -------------------------------------------------------------------------------- /full-architecture-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/full-architecture-3.png -------------------------------------------------------------------------------- /starters/client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel", 4 | "@zeit/next-typescript/babel" 5 | ], 6 | "plugins": [ 7 | ["@babel/plugin-proposal-decorators", { 8 | "legacy": true 9 | }], 10 | ["@babel/plugin-proposal-class-properties", { 11 | "loose": true 12 | }], 13 | ["module-resolver", { 14 | "root": ["./src"], 15 | "alias": { 16 | "~": "./src", 17 | "~~": "./src/services" 18 | } 19 | }] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /starters/client/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_APP_STAGE = "example" 2 | NEXT_APP_GRAPHQL_ENDPOINT = "https://fyeitajxaa.execute-api.ap-northeast-1.amazonaws.com/dev/graphql" 3 | NEXT_APP_VERSION = "0.0.1" 4 | -------------------------------------------------------------------------------- /starters/client/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | app: 3 | schemaPath: ./src/generated/schema.graphql 4 | includes: ["**/*.graphql"] 5 | extensions: 6 | endpoints: 7 | default: ${env:NEXT_APP_GRAPHQL_ENDPOINT} 8 | -------------------------------------------------------------------------------- /starters/client/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ./src/generated/schema.graphql 3 | documents: '**/*.graphql' 4 | generates: 5 | src/generated/graphql.tsx: 6 | plugins: 7 | - typescript 8 | - typescript-operations 9 | - typescript-react-apollo 10 | config: 11 | typesPrefix: I 12 | withHOC: true 13 | withHooks: true 14 | withComponent: true 15 | hooksImportFrom: react-apollo-hooks 16 | -------------------------------------------------------------------------------- /starters/client/index.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser') 2 | const cookieParser = require('cookie-parser') 3 | const express = require('express') 4 | const next = require('next') 5 | const path = require('path') 6 | 7 | const config = require('./next.config') 8 | 9 | const IS_PROD = process.env.NODE_ENV === 'production' 10 | const PORT = IS_PROD ? 80 : 3000 11 | 12 | main() 13 | 14 | async function main() { 15 | const server = await createServer() 16 | 17 | server.listen(PORT) 18 | } 19 | 20 | async function createServer() { 21 | const app = next({ 22 | config, 23 | dev: !IS_PROD, 24 | dir: path.resolve(__dirname, './src'), 25 | }) 26 | 27 | await app.prepare() 28 | 29 | const server = express() 30 | 31 | server.use(bodyParser.json()) 32 | server.use(bodyParser.urlencoded({ extended: true })) 33 | server.use(cookieParser()) 34 | 35 | server.use((req, res) => app.render(req, res, req._parsedUrl.pathname, req.query)) 36 | 37 | return server 38 | } 39 | -------------------------------------------------------------------------------- /starters/client/next.config.js: -------------------------------------------------------------------------------- 1 | const withOptimizedImages = require('next-optimized-images') 2 | const withTypescript = require('@zeit/next-typescript') 3 | 4 | module.exports = ( 5 | withOptimizedImages( 6 | withTypescript({ 7 | target: 'serverless', 8 | distDir: '../dist', 9 | imagesName: '[hash].[ext]', 10 | }) 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /starters/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/tonyfromundefined/next-starter", 6 | "author": "Tony Won ", 7 | "license": "MIT", 8 | "private": false, 9 | "scripts": { 10 | "dev": "dotenv -e .env.development -- node ./index.js", 11 | "prebuild": "rimraf ./dist", 12 | "build": "next build ./src", 13 | "postbuild": "parallel-webpack", 14 | "deploy:dev": "sls deploy --stage dev", 15 | "deploy:stage": "NODE_ENV=production sls deploy --stage stage", 16 | "deploy:prod": "NODE_ENV=production sls deploy --stage prod", 17 | "lint": "tslint --project '.'", 18 | "pregenerate": "graphql --dotenv .env.development get-schema", 19 | "generate": "graphql-codegen --config codegen.yml" 20 | }, 21 | "dependencies": { 22 | "apollo-cache-inmemory": "^1.6.2", 23 | "apollo-client": "^2.6.2", 24 | "apollo-link-http": "^1.5.14", 25 | "aws-serverless-express": "^3.3.6", 26 | "body-parser": "^1.19.0", 27 | "cookie-parser": "^1.4.4", 28 | "encoding": "^0.1.12", 29 | "express": "^4.17.1", 30 | "graphql": "^14.3.1", 31 | "isomorphic-unfetch": "^3.0.0", 32 | "lodash": "^4.17.11", 33 | "mobx": "^5.10.1", 34 | "mobx-react-lite": "^1.4.0", 35 | "next": "^8.1.0", 36 | "nocache": "^2.1.0", 37 | "react": "^16.8.6", 38 | "react-apollo": "^2.5.6", 39 | "react-apollo-hooks": "^0.4.5", 40 | "react-dom": "^16.8.6" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.4.5", 44 | "@babel/plugin-proposal-class-properties": "^7.4.4", 45 | "@babel/plugin-proposal-decorators": "^7.4.4", 46 | "@graphql-codegen/cli": "1.1.3", 47 | "@graphql-codegen/typescript": "1.1.3", 48 | "@graphql-codegen/typescript-operations": "1.1.3", 49 | "@graphql-codegen/typescript-react-apollo": "1.1.3", 50 | "@types/express-serve-static-core": "^4.16.7", 51 | "@types/lodash": "^4.14.134", 52 | "@types/next": "^8.0.5", 53 | "@types/react": "^16.8.19", 54 | "@types/react-dom": "^16.8.4", 55 | "@zeit/next-typescript": "^1.1.1", 56 | "babel-plugin-module-resolver": "^3.2.0", 57 | "dotenv-cli": "^2.0.0", 58 | "graphql-cli": "^3.0.11", 59 | "imagemin-mozjpeg": "^8.0.0", 60 | "imagemin-optipng": "^7.0.0", 61 | "imagemin-svgo": "^7.0.0", 62 | "next-optimized-images": "^2.5.1", 63 | "parallel-webpack": "^2.4.0", 64 | "rimraf": "^2.6.3", 65 | "serverless": "^1.44.1", 66 | "serverless-apigw-binary": "^0.4.4", 67 | "serverless-dotenv-plugin": "^2.1.1", 68 | "serverless-s3-sync": "^1.8.0", 69 | "tslint": "^5.17.0", 70 | "tslint-react": "^4.0.0", 71 | "typescript": "^3.5.1", 72 | "webpack": "^4.33.0", 73 | "webpack-cli": "^3.3.3" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /starters/client/serverless.yml: -------------------------------------------------------------------------------- 1 | service: next-starter # 1. Edit service name 2 | 3 | plugins: 4 | - serverless-s3-sync 5 | - serverless-apigw-binary 6 | - serverless-dotenv-plugin 7 | 8 | package: 9 | individually: true 10 | excludeDevDependencies: false 11 | 12 | provider: 13 | name: aws 14 | runtime: nodejs10.x 15 | stage: ${opt:stage, 'dev'} 16 | region: us-east-1 # 2. Edit AWS region name 17 | 18 | custom: 19 | ####################################### 20 | # Unique ID included in resource names. 21 | # Replace it with a random value for every first distribution. 22 | # https://www.random.org/strings/?num=1&len=6&digits=on&loweralpha=on&unique=on&format=html&rnd=new 23 | stackId: '0uelbz' # 3. Update Random Stack ID 24 | ####################################### 25 | 26 | buckets: 27 | ASSETS_BUCKET_NAME: ${self:service}-${self:custom.stackId}-${self:provider.stage}-assets 28 | s3Sync: 29 | - bucketName: ${self:custom.buckets.ASSETS_BUCKET_NAME} 30 | localDir: dist 31 | apigwBinary: 32 | types: 33 | - '*/*' 34 | 35 | functions: 36 | main: 37 | name: ${self:service}-${self:custom.stackId}-${self:provider.stage}-main 38 | handler: dist/serverless/bundles/main.handler 39 | memorySize: 2048 40 | timeout: 10 41 | environment: 42 | NODE_ENV: production 43 | package: 44 | include: 45 | - dist/serverless/bundles/main.js 46 | exclude: 47 | - '**' 48 | events: 49 | - http: 50 | path: / 51 | method: any 52 | - http: 53 | path: /{proxy+} 54 | method: any 55 | - http: 56 | path: /_next/{proxy+} 57 | method: any 58 | integration: http-proxy 59 | request: 60 | uri: https://${self:custom.buckets.ASSETS_BUCKET_NAME}.s3.${self:provider.region}.amazonaws.com/{proxy} 61 | parameters: 62 | paths: 63 | proxy: true 64 | 65 | # 4. If you implement more than 1 entry, add entries. 66 | # hello: 67 | # name: ${self:service}-${self:custom.stackId}-${self:provider.stage}-hello 68 | # handler: dist/serverless/bundles/hello.handler 69 | # memorySize: 2048 70 | # timeout: 10 71 | # environment: 72 | # NODE_ENV: production 73 | # package: 74 | # include: 75 | # - dist/serverless/bundles/hello.js 76 | # exclude: 77 | # - '**' 78 | # events: 79 | # - http: 80 | # path: / 81 | # method: any 82 | # - http: 83 | # path: /{proxy+} 84 | # method: any 85 | 86 | 87 | resources: 88 | Resources: 89 | ClientAssetsBucket: 90 | Type: AWS::S3::Bucket 91 | Properties: 92 | BucketName: ${self:custom.buckets.ASSETS_BUCKET_NAME} 93 | CorsConfiguration: 94 | CorsRules: 95 | - 96 | AllowedOrigins: 97 | - '*' 98 | AllowedHeaders: 99 | - '*' 100 | AllowedMethods: 101 | - GET 102 | - HEAD 103 | - PUT 104 | - POST 105 | - DELETE 106 | MaxAge: 3000 107 | ExposedHeaders: 108 | - x-amz-server-side-encryption 109 | - x-amz-request-id 110 | - x-amz-id-2 111 | 112 | ClientAssetsBucketPolicy: 113 | Type: AWS::S3::BucketPolicy 114 | Properties: 115 | Bucket: 116 | Ref: ClientAssetsBucket 117 | PolicyDocument: 118 | Version: '2012-10-17' 119 | Statement: [ 120 | { 121 | Action: ['s3:GetObject'], 122 | Effect: 'Allow', 123 | Resource: { 124 | Fn::Join: ['', ['arn:aws:s3:::', { Ref: 'ClientAssetsBucket' }, '/*']], 125 | }, 126 | Principal: '*' 127 | }, 128 | ] 129 | -------------------------------------------------------------------------------- /starters/client/src/apollo/index.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryCache, NormalizedCacheObject } from 'apollo-cache-inmemory' 2 | import { ApolloClient } from 'apollo-client' 3 | import { createHttpLink } from 'apollo-link-http' 4 | import fetch from 'isomorphic-unfetch' 5 | import { Store } from '../store' 6 | 7 | let apolloClient: ApolloClient 8 | 9 | const isServer = typeof window === 'undefined' 10 | 11 | export function createApolloClient(store: Store, state?: any) { 12 | if (apolloClient) { 13 | return apolloClient 14 | 15 | } else { 16 | const link = createHttpLink({ 17 | fetch, 18 | uri: store.environments.NEXT_APP_GRAPHQL_ENDPOINT, 19 | }) 20 | 21 | const cache = new InMemoryCache() 22 | 23 | cache.restore(state || {}) 24 | 25 | if (isServer) { 26 | return new ApolloClient({ 27 | cache, 28 | link, 29 | ssrMode: true, 30 | }) 31 | 32 | } else { 33 | return apolloClient = new ApolloClient({ 34 | cache, 35 | connectToDevTools: true, 36 | link, 37 | }) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /starters/client/src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyfromundefined/workshop-serverless-graphql/7dd126cd38d112c1da1ba702495bf11a8c9035d5/starters/client/src/assets/favicon.png -------------------------------------------------------------------------------- /starters/client/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import ApolloClient from 'apollo-client' 2 | import pickBy from 'lodash/pickBy' 3 | import { Container, default as NextApp } from 'next/app' 4 | import Head from 'next/head' 5 | import React, { Fragment } from 'react' 6 | import { ApolloProvider as ApolloHookProvider, getMarkupFromTree } from 'react-apollo-hooks' 7 | import { renderToString as renderFunction } from 'react-dom/server' 8 | import { createApolloClient } from '~/apollo' 9 | import FaviconImage from '~/assets/favicon.png?url' 10 | import { createStore, IEnvironments, Store, StoreProvider } from '~/store' 11 | 12 | const isServer = typeof window === 'undefined' 13 | 14 | const environments = isServer ? extractNextEnvironments(process.env) : undefined 15 | 16 | export default class extends React.Component { 17 | static async getInitialProps(appContext: any) { 18 | const appProps = await App.getInitialProps(appContext) 19 | 20 | const { Component, router } = appContext 21 | 22 | const store = createStore({ 23 | environments, 24 | }) 25 | 26 | const apollo = createApolloClient(store) 27 | 28 | appContext.ctx.store = store 29 | 30 | if (isServer) { 31 | try { 32 | await Promise.all([ 33 | store.nextServerInit(appContext.ctx.req, appContext.ctx.res), 34 | getMarkupFromTree({ 35 | tree: ( 36 | 43 | ), 44 | renderFunction, 45 | }), 46 | ]) 47 | 48 | } catch (error) { 49 | // tslint:disable-next-line:no-console 50 | console.error('[Error 29948] Pre-operation required for SSR failed') 51 | } 52 | 53 | Head.rewind() 54 | } 55 | 56 | return { 57 | apolloState: apollo.cache.extract(), 58 | store, 59 | ...appProps, 60 | } 61 | } 62 | 63 | apolloClient: ApolloClient 64 | store: Store 65 | 66 | constructor(props: any) { 67 | super(props) 68 | this.store = createStore(props.store) 69 | this.apolloClient = createApolloClient(this.store, props.apolloState) 70 | } 71 | 72 | render() { 73 | return ( 74 | 79 | ) 80 | } 81 | } 82 | 83 | class App extends NextApp { 84 | render() { 85 | const { Component, pageProps } = this.props 86 | 87 | return ( 88 | 89 | 90 | Hello, AWSKRUG 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ) 102 | } 103 | } 104 | 105 | function extractNextEnvironments(environments: IEnvironments): IEnvironments { 106 | return pickBy(environments, (_value, key) => key.indexOf('NEXT_APP') !== -1) 107 | } 108 | -------------------------------------------------------------------------------- /starters/client/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Head, Main, NextScript } from 'next/document' 2 | 3 | export default class extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /starters/client/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export default function PageIndex() { 4 | const [content, setContent] = useState('') 5 | 6 | const onInputChange = (event: React.ChangeEvent) => { 7 | setContent(event.target.value) 8 | } 9 | 10 | return ( 11 |
12 |

📝 할 일 목록

13 |
14 | 15 | 16 |
17 | 18 |
19 | AWSKRUG 슬랙 가입하기 😎: https://slack.awskr.org 20 |
21 |
22 | ) 23 | } 24 | 25 | function Tasks() { 26 | return ( 27 |
    28 |
  • 29 | id 30 | {'\u00A0'}|{'\u00A0'} 31 | content 32 | {'\u00A0'} 33 | 34 | {'\u00A0'} 35 | 36 | 37 |
  • 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /starters/client/src/serverless/main.js: -------------------------------------------------------------------------------- 1 | const awsServerlessExpress = require('aws-serverless-express') 2 | const bodyParser = require('body-parser') 3 | const cookieParser = require('cookie-parser') 4 | const express = require('express') 5 | const nocache = require('nocache') 6 | 7 | const index = require('../../dist/serverless/pages/index') 8 | const error = require('../../dist/serverless/pages/_error') 9 | 10 | const BINARY_MIME_TYPES = [ 11 | 'application/javascript', 12 | 'application/json', 13 | 'application/octet-stream', 14 | 'application/xml', 15 | 'text/css', 16 | 'text/html', 17 | 'text/javascript', 18 | 'text/plain', 19 | 'text/text', 20 | 'text/xml' 21 | ] 22 | 23 | const app = express() 24 | 25 | app.use(nocache()) 26 | app.use(bodyParser.json()) 27 | app.use(bodyParser.urlencoded({ extended: true })) 28 | app.use(cookieParser()) 29 | 30 | app.get('/', index.render) 31 | app.get('/_error', error.render) 32 | 33 | const server = awsServerlessExpress.createServer(app, null, BINARY_MIME_TYPES) 34 | 35 | exports.handler = (event, context) => { 36 | return awsServerlessExpress.proxy(server, event, context) 37 | } 38 | -------------------------------------------------------------------------------- /starters/client/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express-serve-static-core' 2 | import { action, observable } from 'mobx' 3 | import { useStaticRendering } from 'mobx-react-lite' 4 | import { createContext, useContext } from 'react' 5 | 6 | const isServer = typeof window === 'undefined' 7 | 8 | let store: Store 9 | 10 | useStaticRendering(isServer) 11 | 12 | export interface IEnvironments { 13 | [key: string]: string | undefined 14 | } 15 | 16 | export function createStore(storeState?: Partial) { 17 | switch (true) { 18 | case isServer: 19 | return new Store(storeState) 20 | 21 | case typeof store !== 'undefined': 22 | return store 23 | 24 | default: 25 | return store = new Store(storeState) 26 | } 27 | } 28 | 29 | export class Store { 30 | @observable 31 | environments: IEnvironments 32 | 33 | constructor(storeState: Partial = {}) { 34 | this.environments = storeState.environments || {} 35 | } 36 | 37 | /** 38 | * Store Hydration 39 | * @param req Request 40 | * @param res Response 41 | */ 42 | @action 43 | async nextServerInit(req: Request, res: Response) { 44 | if (!req || !res) { 45 | return 46 | } 47 | } 48 | } 49 | 50 | export const StoreContext = createContext({} as Store) 51 | export const StoreProvider = StoreContext.Provider 52 | export const useStore = () => useContext(StoreContext) 53 | -------------------------------------------------------------------------------- /starters/client/src/types/graphql.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.graphql' { 2 | import { DocumentNode } from 'graphql' 3 | const content: DocumentNode 4 | export default content 5 | } 6 | -------------------------------------------------------------------------------- /starters/client/src/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg' { 2 | const url: string 3 | export default url 4 | } 5 | 6 | declare module '*.svg' { 7 | const url: string 8 | export default url 9 | } 10 | 11 | declare module '*.png' { 12 | const url: string 13 | export default url 14 | } 15 | 16 | declare module '*.jpg?include' { 17 | const url: string 18 | export default url 19 | } 20 | 21 | declare module '*.svg?include' { 22 | const url: string 23 | export default url 24 | } 25 | 26 | declare module '*.png?include' { 27 | const url: string 28 | export default url 29 | } 30 | 31 | declare module '*.jpg?webp' { 32 | const url: string 33 | export default url 34 | } 35 | 36 | declare module '*.svg?webp' { 37 | const url: string 38 | export default url 39 | } 40 | 41 | declare module '*.png?webp' { 42 | const url: string 43 | export default url 44 | } 45 | 46 | declare module '*.jpg?inline' { 47 | const url: string 48 | export default url 49 | } 50 | 51 | declare module '*.svg?inline' { 52 | const url: string 53 | export default url 54 | } 55 | 56 | declare module '*.png?inline' { 57 | const url: string 58 | export default url 59 | } 60 | 61 | declare module '*.jpg?url' { 62 | const url: string 63 | export default url 64 | } 65 | 66 | declare module '*.svg?url' { 67 | const url: string 68 | export default url 69 | } 70 | 71 | declare module '*.png?url' { 72 | const url: string 73 | export default url 74 | } 75 | -------------------------------------------------------------------------------- /starters/client/src/types/vendors.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'open-color' { 2 | interface IColors { 3 | 0: string 4 | 1: string 5 | 2: string 6 | 3: string 7 | 4: string 8 | 5: string 9 | 6: string 10 | 7: string 11 | 8: string 12 | 9: string 13 | } 14 | export const black: string 15 | export const blue: IColors 16 | export const cyan: IColors 17 | export const grape: IColors 18 | export const gray: IColors 19 | export const green: IColors 20 | export const indigo: IColors 21 | export const lime: IColors 22 | export const orange: IColors 23 | export const pink: IColors 24 | export const red: IColors 25 | export const teal: IColors 26 | export const violet: IColors 27 | export const white: string 28 | export const yellow: IColors 29 | } 30 | -------------------------------------------------------------------------------- /starters/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es6", 5 | "lib": [ 6 | "esnext", 7 | "dom", 8 | "dom.iterable", 9 | "scripthost", 10 | "esnext.asynciterable" 11 | ], 12 | "module": "es6", 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "jsx": "preserve", 16 | "allowSyntheticDefaultImports": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "removeComments": false, 20 | "preserveConstEnums": true, 21 | "sourceMap": true, 22 | "skipLibCheck": true, 23 | "esModuleInterop": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "baseUrl": ".", 27 | "paths": { 28 | "~~/*": ["./src/services/*"], 29 | "~/*": ["./src/*"] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /starters/client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-react" 6 | ], 7 | "linterOptions": { 8 | "exclude": [ 9 | "node_modules/**/*.ts", 10 | "dist/**/*.js", 11 | "src/generated/graphql.tsx" 12 | ] 13 | }, 14 | "rules": { 15 | "semicolon": [true, "never"], 16 | "quotemark": [true, "single"], 17 | "indent": [true, "spaces", 2], 18 | "object-literal-sort-keys": false, 19 | "no-shadowed-variable": false, 20 | "max-line-length": false, 21 | "prefer-for-of": false, 22 | "variable-name": false, 23 | "no-empty": false, 24 | "jsx-boolean-value": false, 25 | "no-console": [true, "log"], 26 | "member-access": false, 27 | "max-classes-per-file": false, 28 | "jsx-no-multiline-js": false, 29 | "member-ordering": false, 30 | "no-var-requires": false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /starters/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const isProd = process.env.NODE_ENV === 'production' 5 | 6 | const filenames = fs.readdirSync(path.resolve(__dirname, './src/serverless')) 7 | 8 | const entries = [] 9 | 10 | for (const filename of filenames) { 11 | entries.push({ 12 | [filename.split('.')[0]]: path.resolve(__dirname, './src/serverless', filename), 13 | }) 14 | } 15 | 16 | module.exports = entries.map((entry) => ({ 17 | mode: isProd ? 'production' : 'development', 18 | entry, 19 | output: { 20 | path: path.resolve(__dirname, './dist/serverless/bundles'), 21 | filename: `[name].js`, 22 | libraryTarget: 'commonjs', 23 | }, 24 | target: 'node', 25 | optimization: { 26 | minimize: false, 27 | }, 28 | stats: 'errors-only', 29 | })) 30 | -------------------------------------------------------------------------------- /starters/server/.env.example: -------------------------------------------------------------------------------- 1 | STAGE="example" 2 | IS_PLAYGROUND_ENABLED="1" 3 | IS_TRACING_ENABLED="1" 4 | 5 | PRISMA_ENDPOINT="{endpoint}/{service}/{stage}" 6 | PRISMA_MANAGEMENT_API_SECRET="{managementApiSecret}" 7 | -------------------------------------------------------------------------------- /starters/server/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /starters/server/datamodel.prisma: -------------------------------------------------------------------------------- 1 | type Task { 2 | id: ID! @id 3 | content: String! 4 | isDone: Boolean! 5 | } 6 | -------------------------------------------------------------------------------- /starters/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-graphql-workshop-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/serverless-graphql-workshop", 6 | "author": "tonyfromundefined", 7 | "license": "MIT", 8 | "private": false, 9 | "scripts": { 10 | "predev": "rimraf ./dist", 11 | "dev": "dotenv -e ./.env.development -- webpack --config webpack.config.dev.js", 12 | "prebuild": "rimraf ./dist", 13 | "build": "webpack --config webpack.config.prod.js", 14 | "start": "dotenv -e ./.env.production -- node ./dist/server.js", 15 | "deploy:dev": "sls deploy --stage dev", 16 | "deploy:prod": "NODE_ENV=production sls deploy --stage prod", 17 | "undeploy:dev": "sls remove --stage dev", 18 | "undeploy:prod": "sls remove --stage prod", 19 | "prisma:deploy:dev": "prisma deploy -e .env.development", 20 | "prisma:deploy:prod": "prisma deploy -e .env.production", 21 | "prisma:token:dev": "prisma token -e .env.development", 22 | "prisma:token:prod": "prisma token -e .env.production", 23 | "prisma:admin:dev": "prisma admin -e .env.development", 24 | "prisma:admin:prod": "prisma admin -e .env.production" 25 | }, 26 | "dependencies": { 27 | "apollo-server-express": "^2.6.3", 28 | "aws-serverless-express": "^3.3.6", 29 | "cors": "^2.8.5", 30 | "express": "^4.17.1", 31 | "nexus": "^0.12.0-beta.6", 32 | "nexus-prisma": "^0.3.7", 33 | "prisma-client-lib": "^1.34.0", 34 | "short-uuid": "^3.1.1" 35 | }, 36 | "devDependencies": { 37 | "@types/aws-serverless-express": "^3.3.1", 38 | "@types/dotenv": "^6.1.1", 39 | "@types/graphql": "^14.2.1", 40 | "dotenv-cli": "^2.0.0", 41 | "fork-ts-checker-webpack-plugin": "^1.3.7", 42 | "nexus-prisma-generate": "^0.3.7", 43 | "nodemon-webpack-plugin": "^4.0.8", 44 | "rimraf": "^2.6.3", 45 | "serverless": "^1.45.1", 46 | "serverless-apigw-binary": "^0.4.4", 47 | "serverless-dotenv-plugin": "^2.1.1", 48 | "ts-loader": "^6.0.2", 49 | "tslint": "^5.17.0", 50 | "typescript": "3.4", 51 | "webpack": "^4.34.0", 52 | "webpack-cli": "^3.3.4", 53 | "webpackbar": "^3.2.0" 54 | }, 55 | "resolutions": { 56 | "nexus-prisma-generate/**/graphql": "0.13.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /starters/server/prisma.yml: -------------------------------------------------------------------------------- 1 | endpoint: ${env:PRISMA_ENDPOINT} 2 | datamodel: datamodel.prisma 3 | secret: 03ef7cac 4 | 5 | generate: 6 | - generator: typescript-client 7 | output: ./src/generated/prisma/ 8 | 9 | hooks: 10 | post-deploy: 11 | - nexus-prisma-generate --output ./src/generated/nexus-prisma 12 | -------------------------------------------------------------------------------- /starters/server/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-graphql-workshop 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs8.10 6 | stage: ${opt:stage, 'dev'} 7 | region: ap-northeast-1 8 | profile: SERVERLESS_WORKSHOP 9 | 10 | package: 11 | individually: true 12 | excludeDevDependencies: false 13 | 14 | functions: 15 | main: 16 | name: ${self:service}-${self:provider.stage} 17 | handler: dist/serverless.handler 18 | memorySize: 1024 19 | timeout: 10 20 | environment: 21 | NODE_ENV: production 22 | package: 23 | include: 24 | - dist/serverless.js 25 | exclude: 26 | - '**' 27 | events: 28 | - http: 29 | path: / 30 | method: any 31 | - http: 32 | path: /{proxy+} 33 | method: any 34 | 35 | plugins: 36 | - serverless-apigw-binary 37 | - serverless-dotenv-plugin 38 | 39 | custom: 40 | apigwBinary: 41 | types: 42 | - '*/*' 43 | -------------------------------------------------------------------------------- /starters/server/src/app.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server-express' 2 | import cors from 'cors' 3 | import express from 'express' 4 | import { makeSchema } from 'nexus' 5 | import path from 'path' 6 | 7 | import * as types from './resolvers' 8 | 9 | const playground = !!Number(process.env.IS_PLAYGROUND_ENABLED || '0') 10 | const tracing = !!Number(process.env.IS_TRACING_ENABLED || '0') 11 | 12 | const app = express() 13 | 14 | app.use(cors()) 15 | 16 | app.get('/', (_req, res) => { 17 | return res.json('ok') 18 | }) 19 | 20 | const server = new ApolloServer({ 21 | schema: makeSchema({ 22 | types, 23 | outputs: { 24 | schema: path.resolve('./src/generated', 'schema.graphql'), 25 | typegen: path.resolve('./src/generated', 'nexus.ts'), 26 | }, 27 | }), 28 | introspection: playground, 29 | playground, 30 | tracing, 31 | }) 32 | 33 | server.applyMiddleware({ 34 | app, 35 | }) 36 | 37 | export default app 38 | -------------------------------------------------------------------------------- /starters/server/src/resolvers/Mutation.ts: -------------------------------------------------------------------------------- 1 | import { mutationType } from 'nexus' 2 | 3 | export const Mutation = mutationType({ 4 | definition(t) { 5 | t.string('ping', { 6 | resolve: (_parent, _args, _context) => { 7 | return 'pong' 8 | }, 9 | }) 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /starters/server/src/resolvers/Query.ts: -------------------------------------------------------------------------------- 1 | import { queryType } from 'nexus' 2 | 3 | export const Query = queryType({ 4 | definition(t) { 5 | t.string('stage', { 6 | resolve: (_parent, _args, _context) => { 7 | return process.env.STAGE as string 8 | }, 9 | }) 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /starters/server/src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Query' 2 | export * from './Mutation' 3 | -------------------------------------------------------------------------------- /starters/server/src/server.ts: -------------------------------------------------------------------------------- 1 | import app from './app' 2 | 3 | const isProd = process.env.NODE_ENV === 'production' 4 | const port = isProd ? 80 : 3000 5 | 6 | app.listen(port) 7 | -------------------------------------------------------------------------------- /starters/server/src/serverless.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, Context } from 'aws-lambda' 2 | import awsServerlessExpress from 'aws-serverless-express' 3 | import app from './app' 4 | 5 | const BINARY_MIME_TYPES = [ 6 | 'application/javascript', 7 | 'application/json', 8 | 'application/octet-stream', 9 | 'application/xml', 10 | 'text/css', 11 | 'text/html', 12 | 'text/javascript', 13 | 'text/plain', 14 | 'text/text', 15 | 'text/xml', 16 | ] 17 | 18 | const server = awsServerlessExpress.createServer(app, undefined, BINARY_MIME_TYPES) 19 | 20 | export function handler(event: APIGatewayProxyEvent, context: Context) { 21 | return awsServerlessExpress.proxy(server, event, context) 22 | } 23 | -------------------------------------------------------------------------------- /starters/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "esnext", 7 | ], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "removeComments": false, 14 | "preserveConstEnums": true, 15 | "sourceMap": true, 16 | "skipLibCheck": true, 17 | "esModuleInterop": true, 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "baseUrl": ".", 21 | "paths": {} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /starters/server/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**/*.ts", 9 | "dist/**/*.js", 10 | "src/generated/**/*.ts" 11 | ] 12 | }, 13 | "rules": { 14 | "semicolon": [true, "never"], 15 | "quotemark": [true, "single"], 16 | "indent": [true, "spaces", 2], 17 | "object-literal-sort-keys": false, 18 | "no-shadowed-variable": false, 19 | "max-line-length": false, 20 | "prefer-for-of": false, 21 | "variable-name": false, 22 | "no-empty": false, 23 | "jsx-boolean-value": false, 24 | "no-console": [true, "log"], 25 | "member-access": false, 26 | "max-classes-per-file": false, 27 | "jsx-no-multiline-js": false, 28 | "member-ordering": false, 29 | "no-var-requires": false, 30 | "interface-name": false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /starters/server/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') 3 | const NodemonPlugin = require('nodemon-webpack-plugin') 4 | const WebpackbarPlugin = require('webpackbar') 5 | 6 | module.exports = { 7 | context: __dirname, 8 | mode: 'development', 9 | target: 'node', 10 | entry: path.resolve(__dirname, './src/server.ts'), 11 | output: { 12 | path: path.resolve(__dirname, './dist'), 13 | filename: 'index.js', 14 | libraryTarget: 'commonjs', 15 | }, 16 | resolve: { 17 | extensions: ['.ts', '.tsx', '.js'], 18 | alias: { 19 | '~': path.resolve(__dirname, './src'), 20 | }, 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.mjs$/, 26 | include: /node_modules/, 27 | type: 'javascript/auto', 28 | }, 29 | { 30 | test: /\.tsx?$/, 31 | use: [ 32 | { 33 | loader: 'ts-loader', 34 | options: { 35 | transpileOnly: true, 36 | experimentalWatchApi: true, 37 | }, 38 | } 39 | ], 40 | }, 41 | ], 42 | }, 43 | stats: 'errors-only', 44 | watch: true, 45 | plugins: [ 46 | new WebpackbarPlugin({ 47 | name: 'Server', 48 | color: '#228be6', 49 | }), 50 | new ForkTsCheckerWebpackPlugin(), 51 | new NodemonPlugin(), 52 | ], 53 | } 54 | -------------------------------------------------------------------------------- /starters/server/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') 3 | const WebpackbarPlugin = require('webpackbar') 4 | 5 | module.exports = { 6 | mode: 'production', 7 | target: 'node', 8 | entry: { 9 | server: path.resolve(__dirname, './src/server.ts'), 10 | serverless: path.resolve(__dirname, './src/serverless.ts'), 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, './dist'), 14 | filename: '[name].js', 15 | libraryTarget: 'commonjs', 16 | }, 17 | resolve: { 18 | extensions: ['.ts', '.tsx', '.js'], 19 | alias: { 20 | '~': path.resolve(__dirname, './src'), 21 | }, 22 | }, 23 | module: { 24 | rules: [ 25 | { test: /\.mjs$/, include: /node_modules/, type: 'javascript/auto' }, 26 | { test: /\.tsx?$/, loader: 'ts-loader' }, 27 | ], 28 | }, 29 | stats: 'errors-only', 30 | optimization: { 31 | minimize: false, 32 | }, 33 | plugins: [ 34 | new WebpackbarPlugin({ 35 | name: 'Server (Production)', 36 | color: '#fa5252', 37 | }), 38 | new ForkTsCheckerWebpackPlugin(), 39 | ], 40 | } 41 | -------------------------------------------------------------------------------- /templates/prisma.aurora.serverless.yml: -------------------------------------------------------------------------------- 1 | # References 2 | # https://www.prisma.io/tutorials/deploy-prisma-to-aws-fargate-ct14 3 | # https://github.com/prisma/database-templates/blob/master/aws/mysql.yml 4 | # https://github.com/prisma/prisma-templates/blob/master/aws/fargate.yml 5 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html 6 | 7 | AWSTemplateFormatVersion: '2010-09-09' 8 | Description: Prisma deployment on AWS Fargate, RDS (Aurora Serverless) 9 | 10 | Parameters: 11 | DatabaseName: 12 | Default: "prisma" 13 | Description: The database name (Default "prisma", change if you are creating more than one database) 14 | Type: String 15 | 16 | DatabaseUsername: 17 | Default: "prisma" 18 | AllowedPattern: "[a-zA-Z0-9]+" 19 | ConstraintDescription: must contain only alphanumeric characters. Must have length 1-16 20 | Description: The database admin account user name. (Default "prisma") 21 | MaxLength: '16' 22 | MinLength: '1' 23 | Type: String 24 | 25 | DatabasePassword: 26 | AllowedPattern: "[a-zA-Z0-9]+" 27 | ConstraintDescription: must contain only alphanumeric characters. Must have length 8-41. 28 | Description: The database admin account password. (Choose a secure password) 29 | MaxLength: '41' 30 | MinLength: '8' 31 | NoEcho: 'true' 32 | Type: String 33 | 34 | PrismaCpu: 35 | Type: String 36 | Description: The CPU units for the container. Must adhere to https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html. 37 | Default: 256 38 | 39 | PrismaMemory: 40 | Description: The memory reservation for the container. Must adhere to https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html. 41 | Type: Number 42 | Default: 512 43 | 44 | PrismaVersion: 45 | Type: String 46 | Default: 1.34.0 47 | AllowedValues: 48 | - 1.34.0 49 | - 1.33.0 50 | - 1.32.0 51 | - 1.31.0 52 | - 1.30.0 53 | - 1.29.0 54 | - 1.28.0 55 | - 1.27.0 56 | - 1.26.0 57 | - 1.25.0 58 | - 1.24.0 59 | - 1.23.0 60 | - 1.22.0 61 | - 1.21.0 62 | - 1.20.0 63 | - 1.19.0 64 | - 1.18.0 65 | - 1.17.0 66 | - 1.16.0 67 | - 1.15.0 68 | - 1.14.0 69 | - 1.13.0 70 | - 1.12.0 71 | - 1.11.0 72 | - 1.10.2 73 | - 1.9.0 74 | - 1.8.4 75 | - 1.8.3 76 | - 1.7.4 77 | 78 | PrismaJvmOpts: 79 | Description: The JVM options passed to prisma. For example, change this value when changing the memory parameter. Max heap memory (Xmx) should be roughly two thirds of the total memory. 80 | Type: String 81 | Default: '-Xmx1350m' 82 | 83 | PrismaManagementApiSecret: 84 | Description: The secret for your Prisma server. 85 | Type: String 86 | NoEcho: 'true' 87 | 88 | 89 | Mappings: 90 | SubnetConfig: 91 | VPC: 92 | CIDR: '10.0.0.0/16' 93 | PublicOne: 94 | CIDR: '10.0.0.0/24' 95 | PublicTwo: 96 | CIDR: '10.0.1.0/24' 97 | 98 | 99 | Resources: 100 | VPC: 101 | Type: AWS::EC2::VPC 102 | Properties: 103 | EnableDnsSupport: true 104 | EnableDnsHostnames: true 105 | CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] 106 | 107 | PublicSubnetOne: 108 | Type: AWS::EC2::Subnet 109 | Properties: 110 | AvailabilityZone: ap-northeast-1a 111 | VpcId: !Ref 'VPC' 112 | CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR'] 113 | MapPublicIpOnLaunch: true 114 | 115 | PublicSubnetTwo: 116 | Type: AWS::EC2::Subnet 117 | Properties: 118 | AvailabilityZone: ap-northeast-1c 119 | VpcId: !Ref 'VPC' 120 | CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR'] 121 | MapPublicIpOnLaunch: true 122 | 123 | InternetGateway: 124 | Type: AWS::EC2::InternetGateway 125 | 126 | GatewayAttachement: 127 | Type: AWS::EC2::VPCGatewayAttachment 128 | Properties: 129 | VpcId: !Ref 'VPC' 130 | InternetGatewayId: !Ref 'InternetGateway' 131 | 132 | PublicRouteTable: 133 | Type: AWS::EC2::RouteTable 134 | Properties: 135 | VpcId: !Ref 'VPC' 136 | 137 | PublicRoute: 138 | Type: AWS::EC2::Route 139 | DependsOn: GatewayAttachement 140 | Properties: 141 | RouteTableId: !Ref 'PublicRouteTable' 142 | DestinationCidrBlock: '0.0.0.0/0' 143 | GatewayId: !Ref 'InternetGateway' 144 | 145 | PublicSubnetOneRouteTableAssociation: 146 | Type: AWS::EC2::SubnetRouteTableAssociation 147 | Properties: 148 | SubnetId: !Ref PublicSubnetOne 149 | RouteTableId: !Ref PublicRouteTable 150 | 151 | PublicSubnetTwoRouteTableAssociation: 152 | Type: AWS::EC2::SubnetRouteTableAssociation 153 | Properties: 154 | SubnetId: !Ref PublicSubnetTwo 155 | RouteTableId: !Ref PublicRouteTable 156 | 157 | 158 | DatabaseSubnetGroup: 159 | Type: AWS::RDS::DBSubnetGroup 160 | Properties: 161 | DBSubnetGroupDescription: CloudFormation managed DB subnet group. 162 | SubnetIds: 163 | - !Ref PublicSubnetOne 164 | - !Ref PublicSubnetTwo 165 | 166 | DatabaseSecurityGroup: 167 | Type: AWS::EC2::SecurityGroup 168 | Properties: 169 | VpcId: !Ref VPC 170 | GroupDescription: Access to database 171 | 172 | DatabaseSecurityGroupIngressFromPrisma: 173 | Type: AWS::EC2::SecurityGroupIngress 174 | Properties: 175 | Description: Ingress from prisma service 176 | GroupId: !Ref 'DatabaseSecurityGroup' 177 | IpProtocol: -1 178 | SourceSecurityGroupId: !Ref 'PrismaServiceSecurityGroup' 179 | 180 | DatabaseCluster: 181 | Type: AWS::RDS::DBCluster 182 | Properties: 183 | MasterUsername: 184 | Ref: DatabaseUsername 185 | MasterUserPassword: 186 | Ref: DatabasePassword 187 | Engine: aurora 188 | EngineMode: serverless 189 | ScalingConfiguration: 190 | AutoPause: true 191 | MaxCapacity: 4 192 | MinCapacity: 1 193 | SecondsUntilAutoPause: 300 194 | DBClusterIdentifier: !Ref DatabaseName 195 | DBSubnetGroupName: 196 | Ref: DatabaseSubnetGroup 197 | VpcSecurityGroupIds: 198 | - Ref: DatabaseSecurityGroup 199 | 200 | 201 | LoadBalancerSecurityGroup: 202 | Type: AWS::EC2::SecurityGroup 203 | Properties: 204 | GroupDescription: Access to the public facing load balancer 205 | VpcId: !Ref 'VPC' 206 | SecurityGroupIngress: 207 | - CidrIp: 0.0.0.0/0 208 | IpProtocol: -1 209 | 210 | LoadBalancer: 211 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 212 | Properties: 213 | Scheme: internet-facing 214 | LoadBalancerAttributes: 215 | - Key: idle_timeout.timeout_seconds 216 | Value: '30' 217 | Subnets: 218 | - !Ref PublicSubnetOne 219 | - !Ref PublicSubnetTwo 220 | SecurityGroups: [!Ref 'LoadBalancerSecurityGroup'] 221 | 222 | LoadBalancerTargetGroup: 223 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 224 | Properties: 225 | HealthCheckIntervalSeconds: 6 226 | HealthCheckPath: /status 227 | HealthCheckProtocol: HTTP 228 | HealthCheckTimeoutSeconds: 5 229 | HealthyThresholdCount: 2 230 | Name: !Join ['-', [!Ref 'AWS::StackName', 'prisma']] 231 | Port: 80 232 | Protocol: HTTP 233 | UnhealthyThresholdCount: 2 234 | VpcId: !Ref 'VPC' 235 | TargetType: 'ip' 236 | 237 | LoadBalancerListener: 238 | Type: AWS::ElasticLoadBalancingV2::Listener 239 | DependsOn: 240 | - LoadBalancer 241 | Properties: 242 | DefaultActions: 243 | - TargetGroupArn: !Ref 'LoadBalancerTargetGroup' 244 | Type: 'forward' 245 | LoadBalancerArn: !Ref 'LoadBalancer' 246 | Port: 80 247 | Protocol: HTTP 248 | 249 | 250 | PrismaCluster: 251 | Type: AWS::ECS::Cluster 252 | 253 | PrismaLogs: 254 | Type: "AWS::Logs::LogGroup" 255 | Properties: 256 | LogGroupName: !Ref 'AWS::StackName' 257 | RetentionInDays: 7 258 | 259 | PrismaTaskDefinition: 260 | Type: AWS::ECS::TaskDefinition 261 | DependsOn: DatabaseCluster 262 | Properties: 263 | Cpu: !Ref PrismaCpu 264 | Memory: !Ref PrismaMemory 265 | RequiresCompatibilities: 266 | - FARGATE 267 | Family: prisma 268 | NetworkMode: awsvpc 269 | ExecutionRoleArn: !Ref PrismaTaskExecutionRole 270 | TaskRoleArn: !Ref PrismaTaskExecutionRole 271 | ContainerDefinitions: 272 | - Name: PrismaContainer 273 | Essential: true 274 | Image: !Join [':', [ 'prismagraphql/prisma', !Ref PrismaVersion ]] 275 | PortMappings: 276 | - ContainerPort: 60000 277 | Environment: 278 | - Name: PRISMA_CONFIG 279 | Value: !Sub 280 | - | 281 | port: 60000 282 | managementApiSecret: ${PrismaManagementApiSecret} 283 | databases: 284 | default: 285 | connector: 'mysql' 286 | host: ${DatabaseCluster.Endpoint.Address} 287 | port: ${DatabaseCluster.Endpoint.Port} 288 | user: ${DatabaseUsername} 289 | password: ${DatabasePassword} 290 | migrations: true 291 | - {} 292 | - Name: JAVA_OPTS 293 | Value: !Ref PrismaJvmOpts 294 | Ulimits: 295 | - Name: nofile 296 | HardLimit: 1000000 297 | SoftLimit: 1000000 298 | LogConfiguration: 299 | LogDriver: awslogs 300 | Options: 301 | awslogs-group: !Ref 'AWS::StackName' 302 | awslogs-region: !Ref AWS::Region 303 | awslogs-stream-prefix: prisma 304 | 305 | PrismaServiceSecurityGroup: 306 | Type: AWS::EC2::SecurityGroup 307 | Properties: 308 | GroupDescription: Access to the Fargate containers 309 | VpcId: !Ref 'VPC' 310 | 311 | PrismaServiceSecurityGroupIngressFromLoadBalancer: 312 | Type: AWS::EC2::SecurityGroupIngress 313 | Properties: 314 | Description: Ingress from the load balancer 315 | GroupId: !Ref 'PrismaServiceSecurityGroup' 316 | IpProtocol: -1 317 | SourceSecurityGroupId: !Ref 'LoadBalancerSecurityGroup' 318 | 319 | PrismaServiceSecurityGroupIngressFromSelf: 320 | Type: AWS::EC2::SecurityGroupIngress 321 | Properties: 322 | Description: Ingress from other containers in the same security group 323 | GroupId: !Ref 'PrismaServiceSecurityGroup' 324 | IpProtocol: -1 325 | SourceSecurityGroupId: !Ref 'PrismaServiceSecurityGroup' 326 | 327 | PrismaService: 328 | Type: AWS::ECS::Service 329 | DependsOn: LoadBalancerListener 330 | Properties: 331 | Cluster: !Ref PrismaCluster 332 | ServiceName: Prisma 333 | LaunchType: FARGATE 334 | DesiredCount: 1 335 | DeploymentConfiguration: 336 | MaximumPercent: 200 337 | MinimumHealthyPercent: 100 338 | HealthCheckGracePeriodSeconds: 30 339 | TaskDefinition: !Ref PrismaTaskDefinition 340 | LoadBalancers: 341 | - ContainerName: PrismaContainer 342 | ContainerPort: 60000 343 | TargetGroupArn: !Ref LoadBalancerTargetGroup 344 | NetworkConfiguration: 345 | AwsvpcConfiguration: 346 | AssignPublicIp: ENABLED 347 | SecurityGroups: 348 | - !Ref PrismaServiceSecurityGroup 349 | Subnets: 350 | - !Ref PublicSubnetOne 351 | - !Ref PublicSubnetTwo 352 | 353 | PrismaRole: 354 | Type: AWS::IAM::Role 355 | Properties: 356 | AssumeRolePolicyDocument: 357 | Statement: 358 | - Effect: Allow 359 | Principal: 360 | Service: [ecs.amazonaws.com] 361 | Action: ['sts:AssumeRole'] 362 | Path: / 363 | Policies: 364 | - PolicyName: ecs-service 365 | PolicyDocument: 366 | Statement: 367 | - Effect: Allow 368 | Action: 369 | - 'ec2:AttachNetworkInterface' 370 | - 'ec2:CreateNetworkInterface' 371 | - 'ec2:CreateNetworkInterfacePermission' 372 | - 'ec2:DeleteNetworkInterface' 373 | - 'ec2:DeleteNetworkInterfacePermission' 374 | - 'ec2:Describe*' 375 | - 'ec2:DetachNetworkInterface' 376 | - 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer' 377 | - 'elasticloadbalancing:DeregisterTargets' 378 | - 'elasticloadbalancing:Describe*' 379 | - 'elasticloadbalancing:RegisterInstancesWithLoadBalancer' 380 | - 'elasticloadbalancing:RegisterTargets' 381 | Resource: '*' 382 | 383 | PrismaTaskExecutionRole: 384 | Type: AWS::IAM::Role 385 | Properties: 386 | AssumeRolePolicyDocument: 387 | Statement: 388 | - Effect: Allow 389 | Principal: 390 | Service: [ecs-tasks.amazonaws.com] 391 | Action: ['sts:AssumeRole'] 392 | Path: / 393 | Policies: 394 | - PolicyName: AmazonECSTaskExecutionRolePolicy 395 | PolicyDocument: 396 | Statement: 397 | - Effect: Allow 398 | Action: 399 | - 'ecr:GetAuthorizationToken' 400 | - 'ecr:BatchCheckLayerAvailability' 401 | - 'ecr:GetDownloadUrlForLayer' 402 | - 'ecr:BatchGetImage' 403 | - 'logs:CreateLogStream' 404 | - 'logs:PutLogEvents' 405 | Resource: '*' 406 | 407 | 408 | Outputs: 409 | PrismaEndpoint: 410 | Description: The endpoint of the external load balancer 411 | Value: !Join ['', ['http://', !GetAtt 'LoadBalancer.DNSName']] 412 | Export: 413 | Name: !Join [':', [ !Ref 'AWS::StackName', 'PrismaEndpoint' ]] 414 | -------------------------------------------------------------------------------- /templates/prisma.mysql.yml: -------------------------------------------------------------------------------- 1 | # References 2 | # https://www.prisma.io/tutorials/deploy-prisma-to-aws-fargate-ct14 3 | # https://github.com/prisma/database-templates/blob/master/aws/mysql.yml 4 | # https://github.com/prisma/prisma-templates/blob/master/aws/fargate.yml 5 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html 6 | 7 | AWSTemplateFormatVersion: '2010-09-09' 8 | Description: Prisma deployment on AWS Fargate, RDS (MySQL) 9 | 10 | Parameters: 11 | DatabaseName: 12 | Default: "prisma" 13 | Description: The database name (Default "prisma", change if you are creating more than one database) 14 | Type: String 15 | 16 | DatabaseInstanceType: 17 | Default: db.t2.micro 18 | AllowedValues: 19 | - db.t2.micro 20 | - db.t2.small 21 | - db.t2.medium 22 | - db.t2.large 23 | - db.t2.xlarge 24 | - db.t2.2xlarge 25 | - db.r4.large 26 | - db.r4.xlarge 27 | - db.r4.2xlarge 28 | - db.r4.4xlarge 29 | - db.r4.8xlarge 30 | - db.r4.16xlarge 31 | - db.m4.large 32 | - db.m4.2xlarge 33 | - db.m4.4xlarge 34 | - db.m4.10xlarge 35 | - db.m4.16xlarge 36 | Description: "The instance type to use for the database. Pricing: https://aws.amazon.com/rds/mysql/pricing/" 37 | Type: String 38 | 39 | DatabaseUsername: 40 | Default: "prisma" 41 | AllowedPattern: "[a-zA-Z0-9]+" 42 | ConstraintDescription: must contain only alphanumeric characters. Must have length 1-16 43 | Description: The database admin account user name. (Default "prisma") 44 | MaxLength: '16' 45 | MinLength: '1' 46 | Type: String 47 | 48 | DatabasePassword: 49 | AllowedPattern: "[a-zA-Z0-9]+" 50 | ConstraintDescription: must contain only alphanumeric characters. Must have length 8-41. 51 | Description: The database admin account password. (Choose a secure password) 52 | MaxLength: '41' 53 | MinLength: '8' 54 | NoEcho: 'true' 55 | Type: String 56 | 57 | DatabaseAllocatedStorage: 58 | Default: 20 59 | Description: Storage to allocate in GB (Default "20") 60 | Type: Number 61 | MinValue: 20 62 | MaxValue: 16384 63 | ConstraintDescription: Allocated storage size must be in range 20-16384 GB 64 | 65 | PrismaCpu: 66 | Type: String 67 | Description: The CPU units for the container. Must adhere to https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html. 68 | Default: 256 69 | 70 | PrismaMemory: 71 | Description: The memory reservation for the container. Must adhere to https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html. 72 | Type: Number 73 | Default: 512 74 | 75 | PrismaVersion: 76 | Type: String 77 | Default: 1.34.0 78 | AllowedValues: 79 | - 1.34.0 80 | - 1.33.0 81 | - 1.32.0 82 | - 1.31.0 83 | - 1.30.0 84 | - 1.29.0 85 | - 1.28.0 86 | - 1.27.0 87 | - 1.26.0 88 | - 1.25.0 89 | - 1.24.0 90 | - 1.23.0 91 | - 1.22.0 92 | - 1.21.0 93 | - 1.20.0 94 | - 1.19.0 95 | - 1.18.0 96 | - 1.17.0 97 | - 1.16.0 98 | - 1.15.0 99 | - 1.14.0 100 | - 1.13.0 101 | - 1.12.0 102 | - 1.11.0 103 | - 1.10.2 104 | - 1.9.0 105 | - 1.8.4 106 | - 1.8.3 107 | - 1.7.4 108 | 109 | PrismaJvmOpts: 110 | Description: The JVM options passed to prisma. For example, change this value when changing the memory parameter. Max heap memory (Xmx) should be roughly two thirds of the total memory. 111 | Type: String 112 | Default: '-Xmx1350m' 113 | 114 | PrismaManagementApiSecret: 115 | Description: The secret for your Prisma server. 116 | Type: String 117 | NoEcho: 'true' 118 | 119 | 120 | Mappings: 121 | SubnetConfig: 122 | VPC: 123 | CIDR: '10.0.0.0/16' 124 | PublicOne: 125 | CIDR: '10.0.0.0/24' 126 | PublicTwo: 127 | CIDR: '10.0.1.0/24' 128 | 129 | 130 | Resources: 131 | VPC: 132 | Type: AWS::EC2::VPC 133 | Properties: 134 | EnableDnsSupport: true 135 | EnableDnsHostnames: true 136 | CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] 137 | 138 | PublicSubnetOne: 139 | Type: AWS::EC2::Subnet 140 | Properties: 141 | AvailabilityZone: ap-northeast-1a 142 | VpcId: !Ref 'VPC' 143 | CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR'] 144 | MapPublicIpOnLaunch: true 145 | 146 | PublicSubnetTwo: 147 | Type: AWS::EC2::Subnet 148 | Properties: 149 | AvailabilityZone: ap-northeast-1c 150 | VpcId: !Ref 'VPC' 151 | CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR'] 152 | MapPublicIpOnLaunch: true 153 | 154 | InternetGateway: 155 | Type: AWS::EC2::InternetGateway 156 | 157 | GatewayAttachement: 158 | Type: AWS::EC2::VPCGatewayAttachment 159 | Properties: 160 | VpcId: !Ref 'VPC' 161 | InternetGatewayId: !Ref 'InternetGateway' 162 | 163 | PublicRouteTable: 164 | Type: AWS::EC2::RouteTable 165 | Properties: 166 | VpcId: !Ref 'VPC' 167 | 168 | PublicRoute: 169 | Type: AWS::EC2::Route 170 | DependsOn: GatewayAttachement 171 | Properties: 172 | RouteTableId: !Ref 'PublicRouteTable' 173 | DestinationCidrBlock: '0.0.0.0/0' 174 | GatewayId: !Ref 'InternetGateway' 175 | 176 | PublicSubnetOneRouteTableAssociation: 177 | Type: AWS::EC2::SubnetRouteTableAssociation 178 | Properties: 179 | SubnetId: !Ref PublicSubnetOne 180 | RouteTableId: !Ref PublicRouteTable 181 | 182 | PublicSubnetTwoRouteTableAssociation: 183 | Type: AWS::EC2::SubnetRouteTableAssociation 184 | Properties: 185 | SubnetId: !Ref PublicSubnetTwo 186 | RouteTableId: !Ref PublicRouteTable 187 | 188 | 189 | DatabaseSubnetGroup: 190 | Type: AWS::RDS::DBSubnetGroup 191 | Properties: 192 | DBSubnetGroupDescription: CloudFormation managed DB subnet group. 193 | SubnetIds: 194 | - !Ref PublicSubnetOne 195 | - !Ref PublicSubnetTwo 196 | 197 | DatabaseParameterGroup: 198 | Type: "AWS::RDS::DBParameterGroup" 199 | Properties: 200 | Description: Prisma DB parameter group 201 | Family: MySQL5.7 202 | Parameters: 203 | max_connections: 300 204 | 205 | DatabaseSecurityGroup: 206 | Type: AWS::EC2::SecurityGroup 207 | Properties: 208 | VpcId: !Ref VPC 209 | GroupDescription: Access to database 210 | 211 | DatabaseSecurityGroupIngressFromPrisma: 212 | Type: AWS::EC2::SecurityGroupIngress 213 | Properties: 214 | Description: Ingress from prisma service 215 | GroupId: !Ref 'DatabaseSecurityGroup' 216 | IpProtocol: -1 217 | SourceSecurityGroupId: !Ref 'PrismaServiceSecurityGroup' 218 | 219 | DatabaseInstance: 220 | Type: AWS::RDS::DBInstance 221 | Properties: 222 | Engine: mysql 223 | EngineVersion: 5.7.19 224 | DBInstanceClass: 225 | Ref: DatabaseInstanceType 226 | DBSubnetGroupName: 227 | Ref: DatabaseSubnetGroup 228 | DBParameterGroupName: !Ref DatabaseParameterGroup 229 | PubliclyAccessible: "true" 230 | StorageType: "gp2" 231 | AllocatedStorage: !Ref DatabaseAllocatedStorage 232 | BackupRetentionPeriod: 35 233 | DBInstanceIdentifier: !Ref DatabaseName 234 | MasterUsername: 235 | Ref: DatabaseUsername 236 | MasterUserPassword: 237 | Ref: DatabasePassword 238 | PreferredBackupWindow: 02:00-03:00 239 | PreferredMaintenanceWindow: mon:03:00-mon:04:00 240 | DBSubnetGroupName: 241 | Ref: DatabaseSubnetGroup 242 | VPCSecurityGroups: 243 | - Ref: DatabaseSecurityGroup 244 | 245 | 246 | LoadBalancerSecurityGroup: 247 | Type: AWS::EC2::SecurityGroup 248 | Properties: 249 | GroupDescription: Access to the public facing load balancer 250 | VpcId: !Ref 'VPC' 251 | SecurityGroupIngress: 252 | - CidrIp: 0.0.0.0/0 253 | IpProtocol: -1 254 | 255 | LoadBalancer: 256 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 257 | Properties: 258 | Scheme: internet-facing 259 | LoadBalancerAttributes: 260 | - Key: idle_timeout.timeout_seconds 261 | Value: '30' 262 | Subnets: 263 | - !Ref PublicSubnetOne 264 | - !Ref PublicSubnetTwo 265 | SecurityGroups: [!Ref 'LoadBalancerSecurityGroup'] 266 | 267 | LoadBalancerTargetGroup: 268 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 269 | Properties: 270 | HealthCheckIntervalSeconds: 6 271 | HealthCheckPath: /status 272 | HealthCheckProtocol: HTTP 273 | HealthCheckTimeoutSeconds: 5 274 | HealthyThresholdCount: 2 275 | Name: !Join ['-', [!Ref 'AWS::StackName', 'prisma']] 276 | Port: 80 277 | Protocol: HTTP 278 | UnhealthyThresholdCount: 2 279 | VpcId: !Ref 'VPC' 280 | TargetType: 'ip' 281 | 282 | LoadBalancerListener: 283 | Type: AWS::ElasticLoadBalancingV2::Listener 284 | DependsOn: 285 | - LoadBalancer 286 | Properties: 287 | DefaultActions: 288 | - TargetGroupArn: !Ref 'LoadBalancerTargetGroup' 289 | Type: 'forward' 290 | LoadBalancerArn: !Ref 'LoadBalancer' 291 | Port: 80 292 | Protocol: HTTP 293 | 294 | 295 | PrismaCluster: 296 | Type: AWS::ECS::Cluster 297 | 298 | PrismaLogs: 299 | Type: "AWS::Logs::LogGroup" 300 | Properties: 301 | LogGroupName: !Ref 'AWS::StackName' 302 | RetentionInDays: 7 303 | 304 | PrismaTaskDefinition: 305 | Type: AWS::ECS::TaskDefinition 306 | DependsOn: DatabaseInstance 307 | Properties: 308 | Cpu: !Ref PrismaCpu 309 | Memory: !Ref PrismaMemory 310 | RequiresCompatibilities: 311 | - FARGATE 312 | Family: prisma 313 | NetworkMode: awsvpc 314 | ExecutionRoleArn: !Ref PrismaTaskExecutionRole 315 | TaskRoleArn: !Ref PrismaTaskExecutionRole 316 | ContainerDefinitions: 317 | - Name: PrismaContainer 318 | Essential: true 319 | Image: !Join [':', [ 'prismagraphql/prisma', !Ref PrismaVersion ]] 320 | PortMappings: 321 | - ContainerPort: 60000 322 | Environment: 323 | - Name: PRISMA_CONFIG 324 | Value: !Sub 325 | - | 326 | port: 60000 327 | managementApiSecret: ${PrismaManagementApiSecret} 328 | databases: 329 | default: 330 | connector: 'mysql' 331 | host: ${DatabaseInstance.Endpoint.Address} 332 | port: ${DatabaseInstance.Endpoint.Port} 333 | user: ${DatabaseUsername} 334 | password: ${DatabasePassword} 335 | migrations: true 336 | - {} 337 | - Name: JAVA_OPTS 338 | Value: !Ref PrismaJvmOpts 339 | Ulimits: 340 | - Name: nofile 341 | HardLimit: 1000000 342 | SoftLimit: 1000000 343 | LogConfiguration: 344 | LogDriver: awslogs 345 | Options: 346 | awslogs-group: !Ref 'AWS::StackName' 347 | awslogs-region: !Ref AWS::Region 348 | awslogs-stream-prefix: prisma 349 | 350 | PrismaServiceSecurityGroup: 351 | Type: AWS::EC2::SecurityGroup 352 | Properties: 353 | GroupDescription: Access to the Fargate containers 354 | VpcId: !Ref 'VPC' 355 | 356 | PrismaServiceSecurityGroupIngressFromLoadBalancer: 357 | Type: AWS::EC2::SecurityGroupIngress 358 | Properties: 359 | Description: Ingress from the load balancer 360 | GroupId: !Ref 'PrismaServiceSecurityGroup' 361 | IpProtocol: -1 362 | SourceSecurityGroupId: !Ref 'LoadBalancerSecurityGroup' 363 | 364 | PrismaServiceSecurityGroupIngressFromSelf: 365 | Type: AWS::EC2::SecurityGroupIngress 366 | Properties: 367 | Description: Ingress from other containers in the same security group 368 | GroupId: !Ref 'PrismaServiceSecurityGroup' 369 | IpProtocol: -1 370 | SourceSecurityGroupId: !Ref 'PrismaServiceSecurityGroup' 371 | 372 | PrismaService: 373 | Type: AWS::ECS::Service 374 | DependsOn: LoadBalancerListener 375 | Properties: 376 | Cluster: !Ref PrismaCluster 377 | ServiceName: Prisma 378 | LaunchType: FARGATE 379 | DesiredCount: 1 380 | DeploymentConfiguration: 381 | MaximumPercent: 200 382 | MinimumHealthyPercent: 100 383 | HealthCheckGracePeriodSeconds: 30 384 | TaskDefinition: !Ref PrismaTaskDefinition 385 | LoadBalancers: 386 | - ContainerName: PrismaContainer 387 | ContainerPort: 60000 388 | TargetGroupArn: !Ref LoadBalancerTargetGroup 389 | NetworkConfiguration: 390 | AwsvpcConfiguration: 391 | AssignPublicIp: ENABLED 392 | SecurityGroups: 393 | - !Ref PrismaServiceSecurityGroup 394 | Subnets: 395 | - !Ref PublicSubnetOne 396 | - !Ref PublicSubnetTwo 397 | 398 | PrismaRole: 399 | Type: AWS::IAM::Role 400 | Properties: 401 | AssumeRolePolicyDocument: 402 | Statement: 403 | - Effect: Allow 404 | Principal: 405 | Service: [ecs.amazonaws.com] 406 | Action: ['sts:AssumeRole'] 407 | Path: / 408 | Policies: 409 | - PolicyName: ecs-service 410 | PolicyDocument: 411 | Statement: 412 | - Effect: Allow 413 | Action: 414 | - 'ec2:AttachNetworkInterface' 415 | - 'ec2:CreateNetworkInterface' 416 | - 'ec2:CreateNetworkInterfacePermission' 417 | - 'ec2:DeleteNetworkInterface' 418 | - 'ec2:DeleteNetworkInterfacePermission' 419 | - 'ec2:Describe*' 420 | - 'ec2:DetachNetworkInterface' 421 | - 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer' 422 | - 'elasticloadbalancing:DeregisterTargets' 423 | - 'elasticloadbalancing:Describe*' 424 | - 'elasticloadbalancing:RegisterInstancesWithLoadBalancer' 425 | - 'elasticloadbalancing:RegisterTargets' 426 | Resource: '*' 427 | 428 | PrismaTaskExecutionRole: 429 | Type: AWS::IAM::Role 430 | Properties: 431 | AssumeRolePolicyDocument: 432 | Statement: 433 | - Effect: Allow 434 | Principal: 435 | Service: [ecs-tasks.amazonaws.com] 436 | Action: ['sts:AssumeRole'] 437 | Path: / 438 | Policies: 439 | - PolicyName: AmazonECSTaskExecutionRolePolicy 440 | PolicyDocument: 441 | Statement: 442 | - Effect: Allow 443 | Action: 444 | - 'ecr:GetAuthorizationToken' 445 | - 'ecr:BatchCheckLayerAvailability' 446 | - 'ecr:GetDownloadUrlForLayer' 447 | - 'ecr:BatchGetImage' 448 | - 'logs:CreateLogStream' 449 | - 'logs:PutLogEvents' 450 | Resource: '*' 451 | 452 | 453 | Outputs: 454 | PrismaEndpoint: 455 | Description: The endpoint of the external load balancer 456 | Value: !Join ['', ['http://', !GetAtt 'LoadBalancer.DNSName']] 457 | Export: 458 | Name: !Join [':', [ !Ref 'AWS::StackName', 'PrismaEndpoint' ]] 459 | --------------------------------------------------------------------------------