├── .gitignore ├── .npm-scripts ├── prod.sh ├── seed.sh └── watch.sh ├── Dockerfile ├── README.md ├── database └── sql │ └── database.sql ├── docker-compose.yml ├── nodemon.json ├── package.json ├── scripts └── seed │ ├── db │ ├── connect.ts │ └── index.ts │ └── todo-list │ ├── data.json │ └── index.ts ├── src ├── app.ts ├── db │ ├── connect.ts │ └── index.ts ├── graphql │ ├── resolvers │ │ ├── index.ts │ │ ├── mutations │ │ │ ├── createTodoItem.ts │ │ │ ├── deleteTodoItem.ts │ │ │ └── updateTodoItem.ts │ │ ├── queries │ │ │ └── todoListItems.ts │ │ └── types │ │ │ └── date.ts │ └── types │ │ ├── scalar-type.graphql │ │ ├── schema.graphql │ │ └── todo-list.graphql ├── models │ ├── index.ts │ └── todo-list.ts ├── server.ts └── types │ ├── db.ts │ ├── index.ts │ ├── query-config.ts │ ├── response.ts │ └── status.ts ├── test └── README.md ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.lock 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | .env.test 65 | 66 | # parcel-bundler cache (https://parceljs.org/) 67 | .cache 68 | 69 | # next.js build output 70 | .next 71 | 72 | # nuxt.js build output 73 | .nuxt 74 | 75 | # vuepress build output 76 | .vuepress/dist 77 | 78 | # Serverless directories 79 | .serverless/ 80 | 81 | # FuseBox cache 82 | .fusebox/ 83 | 84 | # DynamoDB Local files 85 | .dynamodb/ 86 | 87 | # Build 88 | dist/ -------------------------------------------------------------------------------- /.npm-scripts/prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf ./dist 4 | 5 | echo "Building ts..." 6 | npm run build 7 | 8 | npm run serve -------------------------------------------------------------------------------- /.npm-scripts/seed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Filling data to `todo_list` table" 4 | node ./scripts/seed/todo-list/index.ts 5 | -------------------------------------------------------------------------------- /.npm-scripts/watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nodemon src/server.js -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15.3-alpine as builder 2 | WORKDIR /todo_api 3 | COPY . ./ 4 | RUN yarn install 5 | RUN yarn build 6 | 7 | FROM node:10.15.3-alpine 8 | WORKDIR /todo_api 9 | COPY --from=builder /todo_api ./ 10 | RUN yarn install --production=true 11 | EXPOSE 8080 12 | ENTRYPOINT ["yarn", "serve"] 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Graphql Todo API 2 | 3 | - [Getting started](#getting-started) 4 | - [.env file](#env-file) 5 | - [Database](#database) 6 | - [Seeding data](#seeding-data) 7 | - [Development](#development) 8 | - [Production](#production) 9 | - [How to write GraphQL](#how-to-write-graphql) 10 | - [1. Define the schema & type](#1-define-the-schema--type) 11 | - [2. Register \*.graphql in schema.graphql](#2-register-graphql-in-schemagraphql) 12 | - [3. Define models for data](#3-define-models-for-data) 13 | - [4. Implement the resolvers](#4-implement-the-resolvers) 14 | - [Playground](#playground) 15 | - [Usage](#usage) 16 | - [References](#references) 17 | 18 | ## Getting started 19 | 20 | - Make sure you have [Docker](https://www.docker.com/) installed on your machine. 21 | - Make sure you have [NodeJS](https://nodejs.org/en/) installed on your machine. 22 | 23 | Then run 24 | 25 | **npm** 26 | 27 | ```bash 28 | npm i 29 | ``` 30 | 31 | **yarn** 32 | 33 | ```bash 34 | yarn install 35 | ``` 36 | 37 | ### .env file 38 | 39 | **.env file** 40 | 41 | 1. Create the `.env` file 42 | 2. Copy and parse the database connection information below: 43 | 44 | ```bash 45 | POSTGRES_USER=docker 46 | POSTGRES_PASSWORD=docker 47 | POSTGRES_HOST=localhost 48 | POSTGRES_DB=todo 49 | POSTGRES_PORT=54320 50 | ``` 51 | 52 | ### Database 53 | 54 | To create database, run: 55 | 56 | ```bash 57 | docker-compose up -d 58 | ``` 59 | 60 | ### Seeding data 61 | 62 | **dump data** 63 | 64 | To initialize the dump data for a database, run: 65 | 66 | ```bash 67 | npm run seed 68 | ``` 69 | 70 | ## Development 71 | 72 | To run on development environment 73 | 74 | ```bash 75 | npm run dev 76 | ``` 77 | 78 | ## Production 79 | 80 | To run on production environment 81 | 82 | ```bash 83 | npm start 84 | ``` 85 | 86 | ## How to write GraphQL 87 | 88 | ### 1. Define the schema & type 89 | 90 | For more information: [https://graphql.org/learn/schema/](https://graphql.org/learn/schema/) 91 | 92 | **graphql/types/todo-list.graphql** 93 | 94 | ```bash 95 | type ResponseTodoList { 96 | status: String! 97 | message: String! 98 | data: [TodoListItem] 99 | } 100 | 101 | type TodoListItem { 102 | id: ID! 103 | content: String! 104 | } 105 | 106 | input InputTodoListItem { 107 | content: String! 108 | } 109 | 110 | type Query { 111 | todoListItems(keyword: String): ResponseTodoList! 112 | } 113 | 114 | type Mutation { 115 | createTodoItem(item: InputTodoListItem): ResponseTodoList! 116 | } 117 | ``` 118 | 119 | ### 2. Register \*.graphql in schema.graphql 120 | 121 | **graphql/types/schema.graphql** 122 | 123 | ```bash 124 | # import Query.*, Mutation.* from "todo-list.graphql" 125 | ``` 126 | 127 | ### 3. Define models for data 128 | 129 | The model actually the type of data mapped to fields of table in database. 130 | 131 | **models/todo-list.ts** 132 | 133 | ```ts 134 | export interface TodoListItem { 135 | id: number; 136 | content: string; 137 | created_at: Date; 138 | updated_at: Date; 139 | } 140 | 141 | export interface InputTodoListItem { 142 | content: string; 143 | } 144 | ``` 145 | 146 | ### 4. Implement the resolvers 147 | 148 | **graphql/resolvers/queries/todoListItems.ts** 149 | 150 | ```ts 151 | import { DB } from '../../../types'; 152 | 153 | export async function todoListItems(db: DB, args: any) { 154 | const res = await db.query('SELECT * FROM todo_list'); 155 | ... 156 | } 157 | ``` 158 | 159 | **graphql/resolvers/mutations/createTodoItem.ts** 160 | 161 | ```ts 162 | import { DB } from '../../../types'; 163 | 164 | export async function createTodoItem(db: DB, args: any) { 165 | const query = 'INSERT INTO todo_list(content) VALUES($1) RETURNING *'; 166 | const values = ['Call Your Dad']; 167 | const res = await db.query(query, values); 168 | ... 169 | } 170 | ``` 171 | 172 | ## Playground 173 | 174 | This sandbox can only run in development mode by command `yarn dev` or `npm run dev`. After starting the development environment, open: 175 | 176 | - [http://localhost:4000/graphql](http://localhost:4000/graphql) 177 | 178 | **query - without param** 179 | 180 | ```bash 181 | query{ 182 | todoListItems{ 183 | status 184 | data{ 185 | content 186 | } 187 | } 188 | } 189 | ``` 190 | 191 | **query - with param** 192 | 193 | ```bash 194 | query{ 195 | todoListItems(keyword: "Call your Mom"){ 196 | status 197 | data{ 198 | content 199 | } 200 | } 201 | } 202 | ``` 203 | 204 | **mutation - createTodoItem** 205 | 206 | ```bash 207 | mutation{ 208 | createTodoItem(item:{ 209 | content: "Just relax, dude!" 210 | }){ 211 | status 212 | data{ 213 | content 214 | } 215 | } 216 | } 217 | ``` 218 | 219 | # Usage 220 | 221 | With `express-graphql`, you can just send an HTTP **POST** request to the endpoint you mounted your GraphQL server on, passing the GraphQL query as the query field in a JSON payload. 222 | 223 | **POST cURL** 224 | 225 | ```bash 226 | curl -X POST \ 227 | http://localhost:4000/graphql \ 228 | -H 'Content-Type: application/json' \ 229 | -H 'Postman-Token: c011dc94-6f67-483a-84cb-2bd4ed442a95' \ 230 | -H 'cache-control: no-cache' \ 231 | -d '{ 232 | "query": "{ todoListItems{ data { content } } }" 233 | }' 234 | ``` 235 | 236 | **GET cURL** 237 | 238 | ```bash 239 | curl -X GET \ 240 | 'http://localhost:4000/graphql?query=query+todoListItems%28$keyword:String%29{todoListItems%28keyword:$keyword%29{status}}&variables={%22keyword%22:%22Call%20your%20Mom%22}' \ 241 | -H 'Postman-Token: f92396a4-4f51-47f0-ac20-3c900289358f' \ 242 | -H 'cache-control: no-cache' 243 | ``` 244 | 245 | **POST fetch** 246 | 247 | ```js 248 | const keyword = 'Call your Mom'; 249 | const query = `{ todoListItems(keyword: "${keyword}"){ data { content } } }`; 250 | 251 | fetch('/graphql', { 252 | method: 'POST', 253 | headers: { 254 | 'Content-Type': 'application/json', 255 | Accept: 'application/json', 256 | }, 257 | body: JSON.stringify({ query }), 258 | }) 259 | .then(res => res.json()) 260 | .then(data => console.log('data returned:', data)); 261 | ``` 262 | 263 | **GET fetch** 264 | 265 | ```js 266 | const query = ` 267 | todoListItems($keyword:String){ 268 | todoListItems(keyword:$keyword){ 269 | status 270 | data{ 271 | content 272 | } 273 | } 274 | } 275 | `; 276 | 277 | const variables = `{"keyword":"Call your Mom"}`; 278 | 279 | fetch(`/graphql?query=query+${query}&variables=${variables}`) 280 | .then(res => res.json()) 281 | .then(data => console.log('data returned:', data)); 282 | ``` 283 | 284 | _For more information check:_ 285 | 286 | - [https://graphql.org/graphql-js/graphql-clients/](https://graphql.org/graphql-js/graphql-clients/) 287 | - [https://blog.apollographql.com/4-simple-ways-to-call-a-graphql-api-a6807bcdb355](https://blog.apollographql.com/4-simple-ways-to-call-a-graphql-api-a6807bcdb355) 288 | 289 | # References 290 | 291 | - [node-postgres](https://node-postgres.com/) 292 | - [graphql](https://graphql.org/) 293 | - [Apollo server](https://www.apollographql.com/docs/apollo-server/) 294 | - [Docker](https://www.docker.com/) 295 | - [Docker compose](https://docs.docker.com/compose/) 296 | - [https://graphql.org/graphql-js/graphql-clients/](https://graphql.org/graphql-js/graphql-clients/) 297 | - [https://blog.apollographql.com/4-simple-ways-to-call-a-graphql-api-a6807bcdb355](https://blog.apollographql.com/4-simple-ways-to-call-a-graphql-api-a6807bcdb355) 298 | -------------------------------------------------------------------------------- /database/sql/database.sql: -------------------------------------------------------------------------------- 1 | create table todo_list 2 | ( 3 | id serial primary key not null, 4 | content text not null, 5 | created_at timestamptz not null DEFAULT NOW(), 6 | updated_at timestamptz not null DEFAULT NOW() 7 | ); 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | image: "postgres:11" 5 | container_name: "todo" 6 | env_file: 7 | - ./.env 8 | ports: 9 | - "54320:5432" 10 | volumes: 11 | - ./database/sql/database.sql:/docker-entrypoint-initdb.d/database.sql 12 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node ./src/server.ts" 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-api", 3 | "version": "0.1.0", 4 | "description": "The API back end for Todo List", 5 | "author": "Dzung Nguyen (dzungnguyen179@gmail.com)", 6 | "contributors": [ 7 | "Dzung Nguyen (dzungnguyen179@gmail.com)" 8 | ], 9 | "scripts": { 10 | "start": "./.npm-scripts/prod.sh", 11 | "dev": "./.npm-scripts/watch.sh", 12 | "serve": "node dist/server.js", 13 | "build": "tsc", 14 | "seed": "./.npm-scripts/seed.sh" 15 | }, 16 | "keywords": [ 17 | "typescript", 18 | "graphql", 19 | "boilerplate", 20 | "postgres", 21 | "apollo", 22 | "apollo-server" 23 | ], 24 | "dependencies": { 25 | "apollo-server-express": "^2.5.0", 26 | "body-parser": "^1.18.3", 27 | "compression": "^1.7.4", 28 | "cookie-parser": "^1.4.4", 29 | "dotenv": "^7.0.0", 30 | "errorhandler": "^1.5.0", 31 | "express": "^4.16.4", 32 | "graphql": "^14.3.1", 33 | "graphql-import": "^0.7.1", 34 | "graphql-tools": "^4.0.4", 35 | "graphql-type-json": "^0.3.0", 36 | "jsonwebtoken": "^8.5.1", 37 | "pg": "^7.9.0", 38 | "typescript": "^3.4.2", 39 | "winston": "^3.2.1" 40 | }, 41 | "devDependencies": { 42 | "@types/body-parser": "^1.17.0", 43 | "@types/compression": "^0.0.36", 44 | "@types/cookie-parser": "^1.4.1", 45 | "@types/errorhandler": "^0.0.32", 46 | "@types/express": "^4.16.1", 47 | "@types/express-graphql": "^0.8.0", 48 | "@types/graphql": "^14.2.0", 49 | "@types/jsonwebtoken": "^8.3.2", 50 | "@types/pg": "^7.4.14", 51 | "@types/winston": "^2.4.4", 52 | "nodemon": "1.17.5", 53 | "rimraf": "^2.6.3", 54 | "ts-node": "^8.0.3", 55 | "tslint": "^5.15.0" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "https://github.com/davidnguyen179/typescript-graphql-postgres-boilerplate" 60 | }, 61 | "license": "MIT", 62 | "bugs": { 63 | "url": "https://github.com/davidnguyen179/typescript-graphql-postgres-boilerplate/issues" 64 | }, 65 | "homepage": "https://github.com/davidnguyen179/typescript-graphql-postgres-boilerplate/blob/master/README.md" 66 | } 67 | -------------------------------------------------------------------------------- /scripts/seed/db/connect.ts: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | require('dotenv').config(); 3 | 4 | function connect() { 5 | return new Pool({ 6 | user: process.env.POSTGRES_USER, 7 | host: process.env.POSTGRES_HOST, 8 | database: process.env.POSTGRES_DB, 9 | password: process.env.POSTGRES_PASSWORD, 10 | port: process.env.POSTGRES_PORT, 11 | }); 12 | } 13 | 14 | module.exports = { 15 | connect, 16 | }; 17 | -------------------------------------------------------------------------------- /scripts/seed/db/index.ts: -------------------------------------------------------------------------------- 1 | const { connect } = require('../../../scripts/seed/db/connect.ts'); 2 | const pool = connect(); 3 | 4 | const db = { 5 | query: function(text, params, callback) { 6 | return pool.query(text, params, callback); 7 | }, 8 | }; 9 | 10 | module.exports = { 11 | db, 12 | }; 13 | -------------------------------------------------------------------------------- /scripts/seed/todo-list/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "content": "Call your Mom" 4 | }, 5 | { 6 | "content": "Do some meditation" 7 | }, 8 | { 9 | "content": "Playing game" 10 | }, 11 | { 12 | "content": "Coding through the night" 13 | }, 14 | { 15 | "content": "Let's dance til the morning" 16 | }, 17 | { 18 | "content": "Let's help other people" 19 | }, 20 | { 21 | "content": "Go to office" 22 | }, 23 | { 24 | "content": "Let's do something crazy" 25 | } 26 | ] -------------------------------------------------------------------------------- /scripts/seed/todo-list/index.ts: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var { db } = require('../../../scripts/seed/db/index.ts'); 3 | const data = require('../../../scripts/seed/todo-list/data.json'); 4 | 5 | const todoListItems = []; 6 | 7 | const query = 'INSERT INTO todo_list(content) VALUES($1) RETURNING *'; 8 | 9 | data.map(function(item) { 10 | const value = [item.content]; 11 | 12 | db.query(query, value, function(err, result) { 13 | if (err) { 14 | console.log(err.stack); 15 | } else { 16 | console.log(result.rows[0]); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import compression from 'compression'; 3 | import bodyParser from 'body-parser'; 4 | import cookieParser from 'cookie-parser'; 5 | 6 | import { ApolloServer, gql } from 'apollo-server-express'; 7 | import { importSchema } from 'graphql-import'; 8 | 9 | import { getResolver } from './graphql/resolvers'; 10 | import { db } from './db'; 11 | 12 | // A map of functions which return data for the schema. 13 | const resolvers = getResolver(); 14 | const type = importSchema('./src/graphql/types/schema.graphql'); 15 | 16 | // The GraphQL schema 17 | const typeDefs = gql` 18 | ${type} 19 | `; 20 | 21 | const apollo = new ApolloServer({ 22 | typeDefs, 23 | resolvers, 24 | rootValue: db, 25 | tracing: true, 26 | }); 27 | 28 | export const app = express(); 29 | apollo.applyMiddleware({ app }); 30 | 31 | // set 32 | app.set('port', process.env.PORT || 4000); 33 | app.set('env', process.env.NODE_ENV || 'development'); 34 | 35 | // get 36 | app.get('/healthz', function(req, res) { 37 | res.send('OK'); 38 | }); 39 | 40 | // use 41 | app.use(compression()); 42 | app.use(bodyParser.json()); 43 | app.use(bodyParser.urlencoded({ extended: true })); 44 | app.use(cookieParser()); 45 | -------------------------------------------------------------------------------- /src/db/connect.ts: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | require('dotenv').config(); 3 | 4 | export function connect() { 5 | return new Pool({ 6 | user: process.env.POSTGRES_USER, 7 | host: process.env.POSTGRES_HOST, 8 | database: process.env.POSTGRES_DB, 9 | password: process.env.POSTGRES_PASSWORD, 10 | port: process.env.POSTGRES_PORT, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { connect } from './connect'; 2 | import { DB } from '../types/db'; 3 | const pool = connect(); 4 | 5 | export const db: DB = { 6 | query: (text: string, params: any, callback: void) => { 7 | return pool.query(text, params, callback) 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/graphql/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'fs'; 2 | import path from 'path'; 3 | 4 | type Resolver = { 5 | [key: string]: any; 6 | }; 7 | 8 | export function getResolver() { 9 | let resolver: Resolver = {}; 10 | 11 | // Register Query to resolvers 12 | const queries = readdirSync('./src/graphql/resolvers/queries'); 13 | queries.forEach(name => { 14 | const fileName = `${path.resolve(__dirname, 'queries')}/${name}`; 15 | const module = require(fileName); 16 | resolver.Query = { 17 | ...resolver.Query, 18 | ...module, 19 | }; 20 | }); 21 | 22 | // Register Mutation to resolvers 23 | const mutations = readdirSync('./src/graphql/resolvers/mutations'); 24 | mutations.forEach(name => { 25 | const fileName = `${path.resolve(__dirname, 'mutations')}/${name}`; 26 | const module = require(fileName); 27 | resolver.Mutation = { 28 | ...resolver.Mutation, 29 | ...module, 30 | }; 31 | }); 32 | 33 | // Register scalar types 34 | const scalarTypes = readdirSync('./src/graphql/resolvers/types'); 35 | scalarTypes.forEach(name => { 36 | const fileName = `${path.resolve(__dirname, 'types')}/${name}`; 37 | const module = require(fileName); 38 | const key = Object.keys(module.default)[0]; 39 | resolver[key] = module.default[key]; 40 | }); 41 | 42 | return resolver; 43 | } 44 | -------------------------------------------------------------------------------- /src/graphql/resolvers/mutations/createTodoItem.ts: -------------------------------------------------------------------------------- 1 | import { DB, QueryConfig, APIResponse } from '../../../types'; 2 | import { TodoListItem } from '../../../models'; 3 | 4 | export async function createTodoItem(db: DB, args: any) { 5 | const { 6 | item: { content }, 7 | } = args; 8 | 9 | const query: QueryConfig = { 10 | text: `INSERT INTO todo_list(content) VALUES($1) RETURNING *`, 11 | values: [content], 12 | }; 13 | 14 | const result = await db.query(query); 15 | const response: APIResponse = { 16 | status: 'fetching', 17 | }; 18 | 19 | try { 20 | if (result.rowCount > 0) { 21 | const data = result.rows.map((item: TodoListItem) => { 22 | return { 23 | id: item.id, 24 | content: item.content, 25 | createdAt: item.created_at, 26 | updatedAt: item.updated_at, 27 | }; 28 | }); 29 | response.status = 'success'; 30 | response.data = data; 31 | } 32 | } catch (e) { 33 | response.status = 'error'; 34 | } 35 | 36 | return response; 37 | } 38 | -------------------------------------------------------------------------------- /src/graphql/resolvers/mutations/deleteTodoItem.ts: -------------------------------------------------------------------------------- 1 | import { DB, QueryConfig, APIResponse } from '../../../types'; 2 | import { TodoListItem } from '../../../models'; 3 | 4 | export async function deleteTodoItem(db: DB, args: any) { 5 | const { id } = args; 6 | 7 | const query: QueryConfig = { 8 | text: `DELETE FROM todo_list WHERE id = $1 RETURNING *`, 9 | values: [id], 10 | }; 11 | 12 | const result = await db.query(query); 13 | const response: APIResponse = { 14 | status: 'fetching', 15 | }; 16 | 17 | try { 18 | if (result.rowCount > 0) { 19 | const data = result.rows.map((item: TodoListItem) => { 20 | return { 21 | id: item.id, 22 | content: item.content, 23 | createdAt: item.created_at, 24 | updatedAt: item.updated_at, 25 | }; 26 | }); 27 | response.status = 'success'; 28 | response.data = data; 29 | } else { 30 | response.status = 'error'; 31 | response.message = 'Record is not found'; 32 | } 33 | } catch (e) { 34 | response.status = 'error'; 35 | } 36 | 37 | return response; 38 | } 39 | -------------------------------------------------------------------------------- /src/graphql/resolvers/mutations/updateTodoItem.ts: -------------------------------------------------------------------------------- 1 | import { DB, QueryConfig, APIResponse } from '../../../types'; 2 | import { TodoListItem } from '../../../models'; 3 | 4 | export async function updateTodoItem(db: DB, args: any) { 5 | const { 6 | id, 7 | item: { content }, 8 | } = args; 9 | 10 | const query: QueryConfig = { 11 | text: `UPDATE todo_list SET content = $2 WHERE id = $1 RETURNING *`, 12 | values: [id, content], 13 | }; 14 | 15 | const result = await db.query(query); 16 | const response: APIResponse = { 17 | status: 'fetching', 18 | }; 19 | 20 | try { 21 | if (result.rowCount > 0) { 22 | const data = result.rows.map((item: TodoListItem) => { 23 | return { 24 | id: item.id, 25 | content: item.content, 26 | createdAt: item.created_at, 27 | updatedAt: item.updated_at, 28 | }; 29 | }); 30 | response.status = 'success'; 31 | response.data = data; 32 | } 33 | } catch (e) { 34 | response.status = 'error'; 35 | } 36 | 37 | return response; 38 | } 39 | -------------------------------------------------------------------------------- /src/graphql/resolvers/queries/todoListItems.ts: -------------------------------------------------------------------------------- 1 | import { DB, QueryConfig, APIResponse } from '../../../types'; 2 | import { TodoListItem } from '../../../models'; 3 | 4 | export async function todoListItems(db: DB, args: any) { 5 | let query: QueryConfig = { text: 'SELECT * FROM todo_list' }; 6 | 7 | if (args && args.keyword) { 8 | query = { 9 | text: `SELECT * FROM todo_list WHERE content LIKE '%' || $1 || '%'`, 10 | values: [args.keyword], 11 | }; 12 | } 13 | 14 | const result = await db.query(query); 15 | 16 | const response: APIResponse = { 17 | status: 'fetching', 18 | }; 19 | 20 | try { 21 | if (result.rowCount > 0) { 22 | const data = result.rows.map((item: TodoListItem) => { 23 | return { 24 | id: item.id, 25 | content: item.content, 26 | createdAt: item.created_at, 27 | updatedAt: item.updated_at, 28 | }; 29 | }); 30 | response.status = 'success'; 31 | response.data = data; 32 | } 33 | } catch (e) { 34 | response.status = 'error'; 35 | } 36 | 37 | return response; 38 | } 39 | -------------------------------------------------------------------------------- /src/graphql/resolvers/types/date.ts: -------------------------------------------------------------------------------- 1 | const { GraphQLScalarType, Kind } = require('graphql'); 2 | 3 | const date = { 4 | Date: new GraphQLScalarType({ 5 | name: 'Date', 6 | description: 'Date custom scalar type', 7 | parseValue(value: any) { 8 | return new Date(value); // value from the client 9 | }, 10 | serialize(value: any) { 11 | return value.getTime(); // value sent to the client 12 | }, 13 | parseLiteral(ast: any) { 14 | if (ast.kind === Kind.INT) { 15 | return parseInt(ast.value, 10); // ast value is always in string format 16 | } 17 | return null; 18 | }, 19 | }), 20 | }; 21 | 22 | export default date; 23 | -------------------------------------------------------------------------------- /src/graphql/types/scalar-type.graphql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | -------------------------------------------------------------------------------- /src/graphql/types/schema.graphql: -------------------------------------------------------------------------------- 1 | # import * from "scalar-type.graphql" 2 | # import Query.*, Mutation.* from "todo-list.graphql" 3 | -------------------------------------------------------------------------------- /src/graphql/types/todo-list.graphql: -------------------------------------------------------------------------------- 1 | type ResponseTodoList { 2 | status: String! 3 | message: String! 4 | data: [TodoListItem] 5 | } 6 | 7 | type TodoListItem { 8 | id: ID! 9 | content: String! 10 | createdAt: Date! 11 | updatedAt: Date! 12 | } 13 | 14 | input InputTodoListItem { 15 | content: String! 16 | } 17 | 18 | type Query { 19 | todoListItems(keyword: String): ResponseTodoList! 20 | } 21 | 22 | type Mutation { 23 | createTodoItem(item: InputTodoListItem): ResponseTodoList! 24 | updateTodoItem(id: ID!, item: InputTodoListItem!): ResponseTodoList! 25 | deleteTodoItem(id: ID!): ResponseTodoList! 26 | } 27 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | import { TodoListItem, InputTodoListItem } from './todo-list'; 2 | 3 | export { TodoListItem, InputTodoListItem }; 4 | -------------------------------------------------------------------------------- /src/models/todo-list.ts: -------------------------------------------------------------------------------- 1 | export interface TodoListItem { 2 | id: number; 3 | content: string; 4 | created_at: Date; 5 | updated_at: Date; 6 | } 7 | 8 | export interface InputTodoListItem { 9 | content: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import errorHandler from 'errorhandler'; 2 | 3 | import { app } from './app'; 4 | 5 | /** 6 | * Error Handler. Provides full stack - remove for production 7 | */ 8 | app.use(errorHandler()); 9 | const port = app.get('port'); 10 | const env = app.get('env'); 11 | 12 | /** 13 | * Start Express server. 14 | */ 15 | const server = app.listen(port, () => { 16 | console.log(' App is running at http://localhost:%d in %s mode', port, env); 17 | 18 | if (env === 'development') { 19 | console.log(' Playground is running at http://localhost:%d/graphql', port); 20 | } 21 | console.log(' Press CTRL-C to stop\n'); 22 | }); 23 | 24 | export { server }; 25 | -------------------------------------------------------------------------------- /src/types/db.ts: -------------------------------------------------------------------------------- 1 | export interface DB { 2 | query: any 3 | } 4 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { DB } from './db'; 2 | import { QueryConfig } from './query-config'; 3 | import { APIResponse } from './response'; 4 | 5 | export { DB, QueryConfig, APIResponse }; 6 | -------------------------------------------------------------------------------- /src/types/query-config.ts: -------------------------------------------------------------------------------- 1 | export interface QueryConfig { 2 | text: string; 3 | values?: string[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/response.ts: -------------------------------------------------------------------------------- 1 | import { Status } from './status'; 2 | 3 | export interface APIResponse { 4 | status: Status; 5 | message?: string; 6 | data?: T; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/status.ts: -------------------------------------------------------------------------------- 1 | export type Status = 'fetching' | 'success' | 'error'; 2 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidnguyen11/typescript-graphql-postgres-boilerplate/f3f727db720056ebe5b59be01a6072e50cddef6d/test/README.md -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": ["node_modules/*", "src/types/*"] 13 | } 14 | }, 15 | "include": ["src/**/*", "scripts/migration/utils/name-to-url.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rulesDirectory": "built/local/tslint/rules", 4 | "linterOptions": { 5 | "exclude": ["tests/**/*"] 6 | }, 7 | "rules": { 8 | "no-unnecessary-type-assertion": true, 9 | 10 | "array-type": [true, "array"], 11 | "ban": [true, "setInterval", "setTimeout"], 12 | "ban-types": { 13 | "options": [ 14 | ["Object", "Avoid using the `Object` type. Did you mean `object`?"], 15 | [ 16 | "Function", 17 | "Avoid using the `Function` type. Prefer a specific function type, like `() => void`, or use `ts.AnyFunction`." 18 | ], 19 | ["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"], 20 | ["Number", "Avoid using the `Number` type. Did you mean `number`?"], 21 | ["String", "Avoid using the `String` type. Did you mean `string`?"] 22 | ] 23 | }, 24 | "boolean-trivia": true, 25 | "class-name": true, 26 | "comment-format": [true, "check-space"], 27 | "curly": [true, "ignore-same-line"], 28 | "debug-assert": true, 29 | "indent": [true, "spaces"], 30 | "interface-name": [true, "never-prefix"], 31 | "interface-over-type-literal": true, 32 | "jsdoc-format": true, 33 | "linebreak-style": [true, "CRLF"], 34 | "next-line": [true, "check-catch", "check-else"], 35 | "no-bom": true, 36 | "no-double-space": true, 37 | "no-eval": true, 38 | "no-in-operator": true, 39 | "no-increment-decrement": true, 40 | "no-inferrable-types": true, 41 | "no-internal-module": true, 42 | "no-null-keyword": true, 43 | "no-switch-case-fall-through": true, 44 | "no-trailing-whitespace": [true, "ignore-template-strings"], 45 | "no-type-assertion-whitespace": true, 46 | "no-unnecessary-qualifier": true, 47 | "no-var-keyword": true, 48 | "object-literal-shorthand": true, 49 | "object-literal-surrounding-space": true, 50 | "one-line": [true, "check-open-brace", "check-whitespace"], 51 | "prefer-const": true, 52 | "quotemark": [true, "double", "avoid-escape"], 53 | "semicolon": [true, "always", "ignore-bound-class-methods"], 54 | "space-within-parens": true, 55 | "triple-equals": true, 56 | "type-operator-spacing": true, 57 | "typedef-whitespace": [ 58 | true, 59 | { 60 | "call-signature": "nospace", 61 | "index-signature": "nospace", 62 | "parameter": "nospace", 63 | "property-declaration": "nospace", 64 | "variable-declaration": "nospace" 65 | }, 66 | { 67 | "call-signature": "onespace", 68 | "index-signature": "onespace", 69 | "parameter": "onespace", 70 | "property-declaration": "onespace", 71 | "variable-declaration": "onespace" 72 | } 73 | ], 74 | "whitespace": [ 75 | true, 76 | "check-branch", 77 | "check-decl", 78 | "check-operator", 79 | "check-module", 80 | "check-separator", 81 | "check-type" 82 | ], 83 | 84 | // Config different from tslint:latest 85 | "no-implicit-dependencies": [true, "dev"], 86 | "object-literal-key-quotes": [true, "consistent-as-needed"], 87 | "variable-name": [ 88 | true, 89 | "ban-keywords", 90 | "check-format", 91 | "allow-leading-underscore" 92 | ], 93 | 94 | // TODO 95 | "arrow-parens": false, // [true, "ban-single-arg-parens"] 96 | "arrow-return-shorthand": false, 97 | "ban-types": false, 98 | "forin": false, 99 | "member-access": false, // [true, "no-public"] 100 | "no-conditional-assignment": false, 101 | "no-console": false, 102 | "no-debugger": false, 103 | "no-empty-interface": false, 104 | "no-object-literal-type-assertion": false, 105 | "no-shadowed-variable": false, 106 | "no-submodule-imports": false, 107 | "no-var-requires": false, 108 | "ordered-imports": false, 109 | "prefer-conditional-expression": false, 110 | "radix": false, 111 | "trailing-comma": false, 112 | 113 | // These should be done automatically by a formatter. https://github.com/Microsoft/TypeScript/issues/18340 114 | "align": false, 115 | "eofline": false, 116 | "max-line-length": false, 117 | "no-consecutive-blank-lines": false, 118 | "space-before-function-paren": false, 119 | 120 | // Not doing 121 | "ban-comma-operator": false, 122 | "max-classes-per-file": false, 123 | "member-ordering": false, 124 | "no-angle-bracket-type-assertion": false, 125 | "no-bitwise": false, 126 | "no-namespace": false, 127 | "no-reference": false, 128 | "object-literal-sort-keys": false, 129 | "one-variable-per-declaration": false 130 | } 131 | } 132 | --------------------------------------------------------------------------------