├── .coveralls.yml ├── .env.example ├── .gitignore ├── .sequelizerc ├── .travis.yml ├── controllers └── index.js ├── database ├── config │ └── config.js ├── migrations │ ├── 20190806094052-create-user.js │ ├── 20190806094146-create-post.js │ └── 20190806094219-create-comment.js ├── models │ ├── comment.js │ ├── index.js │ ├── post.js │ └── user.js └── seeders │ ├── 20190806135744-User.js │ ├── 20190806135753-Post.js │ └── 20190806135804-Comment.js ├── index.js ├── package-lock.json ├── package.json ├── readme.md ├── routes └── index.js ├── server └── index.js └── tests ├── routes.test.js └── sample.test.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: ayspWv4lYCp9izyeJhJ1v0DtwJKRvmudV -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DEV_DATABASE_URL= 2 | TEST_DATABASE_URL= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env 3 | coverage -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | "config": path.resolve('./database/config', 'config.js'), 5 | "models-path": path.resolve('./database/models'), 6 | "seeders-path": path.resolve('./database/seeders'), 7 | "migrations-path": path.resolve('./database/migrations') 8 | }; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | install: npm install 5 | services: 6 | - postgresql 7 | before_script: 8 | - psql -c 'create database test_db;' -U postgres 9 | script: npm test 10 | after_success: npm run coverage -------------------------------------------------------------------------------- /controllers/index.js: -------------------------------------------------------------------------------- 1 | const models = require("../database/models"); 2 | 3 | const createPost = async (req, res) => { 4 | try { 5 | const post = await models.Post.create(req.body); 6 | return res.status(201).json({ 7 | post 8 | }); 9 | } catch (error) { 10 | return res.status(500).json({ error: error.message }); 11 | } 12 | }; 13 | 14 | const getAllPosts = async (req, res) => { 15 | try { 16 | const posts = await models.Post.findAll({ 17 | include: [ 18 | { 19 | model: models.Comment, 20 | as: "comments" 21 | }, 22 | { 23 | model: models.User, 24 | as: "author" 25 | } 26 | ] 27 | }); 28 | return res.status(200).json({ posts }); 29 | } catch (error) { 30 | return res.status(500).send(error.message); 31 | } 32 | }; 33 | 34 | const getPostById = async (req, res) => { 35 | try { 36 | const { postId } = req.params; 37 | const post = await models.Post.findOne({ 38 | where: { id: postId }, 39 | include: [ 40 | { 41 | model: models.Comment, 42 | as: "comments", 43 | include: [ 44 | { 45 | model: models.User, 46 | as: "author" 47 | } 48 | ] 49 | }, 50 | { 51 | model: models.User, 52 | as: "author" 53 | } 54 | ] 55 | }); 56 | if (post) { 57 | return res.status(200).json({ post }); 58 | } 59 | return res.status(404).send("Post with the specified ID does not exists"); 60 | } catch (error) { 61 | return res.status(500).send(error.message); 62 | } 63 | }; 64 | 65 | const updatePost = async (req, res) => { 66 | try { 67 | const { postId } = req.params; 68 | const [updated] = await models.Post.update(req.body, { 69 | where: { id: postId } 70 | }); 71 | if (updated) { 72 | const updatedPost = await models.Post.findOne({ where: { id: postId } }); 73 | return res.status(200).json({ post: updatedPost }); 74 | } 75 | throw new Error("Post not found"); 76 | } catch (error) { 77 | return res.status(500).send(error.message); 78 | } 79 | }; 80 | 81 | const deletePost = async (req, res) => { 82 | try { 83 | const { postId } = req.params; 84 | const deleted = await models.Post.destroy({ 85 | where: { id: postId } 86 | }); 87 | if (deleted) { 88 | return res.status(204).send("Post deleted"); 89 | } 90 | throw new Error("Post not found"); 91 | } catch (error) { 92 | return res.status(500).send(error.message); 93 | } 94 | }; 95 | 96 | module.exports = { 97 | createPost, 98 | getAllPosts, 99 | getPostById, 100 | updatePost, 101 | deletePost 102 | }; 103 | -------------------------------------------------------------------------------- /database/config/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | development: { 5 | url: process.env.DEV_DATABASE_URL, 6 | dialect: 'postgres', 7 | }, 8 | test: { 9 | url: process.env.TEST_DATABASE_URL, 10 | dialect: 'postgres', 11 | }, 12 | production: { 13 | url: process.env.DATABASE_URL, 14 | dialect: 'postgres', 15 | }, 16 | }; -------------------------------------------------------------------------------- /database/migrations/20190806094052-create-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Users', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | name: { 12 | type: Sequelize.STRING 13 | }, 14 | email: { 15 | type: Sequelize.STRING 16 | }, 17 | createdAt: { 18 | allowNull: false, 19 | type: Sequelize.DATE 20 | }, 21 | updatedAt: { 22 | allowNull: false, 23 | type: Sequelize.DATE 24 | } 25 | }); 26 | }, 27 | down: (queryInterface, Sequelize) => { 28 | return queryInterface.dropTable('Users'); 29 | } 30 | }; -------------------------------------------------------------------------------- /database/migrations/20190806094146-create-post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Posts', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | title: { 12 | type: Sequelize.STRING 13 | }, 14 | content: { 15 | type: Sequelize.TEXT 16 | }, 17 | userId: { 18 | type: Sequelize.INTEGER, 19 | allowNull: false, 20 | }, 21 | createdAt: { 22 | allowNull: false, 23 | type: Sequelize.DATE 24 | }, 25 | updatedAt: { 26 | allowNull: false, 27 | type: Sequelize.DATE 28 | } 29 | }); 30 | }, 31 | down: (queryInterface, Sequelize) => { 32 | return queryInterface.dropTable('Posts'); 33 | } 34 | }; -------------------------------------------------------------------------------- /database/migrations/20190806094219-create-comment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Comments', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | postId: { 12 | type: Sequelize.INTEGER, 13 | allowNull: false, 14 | }, 15 | comment: { 16 | type: Sequelize.TEXT 17 | }, 18 | userId: { 19 | type: Sequelize.INTEGER, 20 | allowNull: false, 21 | }, 22 | createdAt: { 23 | allowNull: false, 24 | type: Sequelize.DATE 25 | }, 26 | updatedAt: { 27 | allowNull: false, 28 | type: Sequelize.DATE 29 | } 30 | }); 31 | }, 32 | down: (queryInterface, Sequelize) => { 33 | return queryInterface.dropTable('Comments'); 34 | } 35 | }; -------------------------------------------------------------------------------- /database/models/comment.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (sequelize, DataTypes) => { 3 | const Comment = sequelize.define('Comment', { 4 | postId: DataTypes.INTEGER, 5 | comment: DataTypes.TEXT, 6 | userId: DataTypes.INTEGER 7 | }, {}); 8 | Comment.associate = function(models) { 9 | // associations can be defined here 10 | Comment.belongsTo(models.User, { 11 | foreignKey: 'userId', 12 | as: 'author' 13 | }); 14 | Comment.belongsTo(models.Post, { 15 | foreignKey: 'postId', 16 | as: 'post' 17 | }); 18 | }; 19 | return Comment; 20 | }; -------------------------------------------------------------------------------- /database/models/index.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const Sequelize = require('sequelize'); 5 | const envConfigs = require('../config/config'); 6 | 7 | const basename = path.basename(__filename); 8 | const env = process.env.NODE_ENV || 'development'; 9 | const config = envConfigs[env]; 10 | const db = {}; 11 | 12 | let sequelize; 13 | if (config.url) { 14 | sequelize = new Sequelize(config.url, config); 15 | } else { 16 | sequelize = new Sequelize(config.database, config.username, config.password, config); 17 | } 18 | 19 | fs 20 | .readdirSync(__dirname) 21 | .filter(file => { 22 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 23 | }) 24 | .forEach(file => { 25 | const model = sequelize['import'](path.join(__dirname, file)); 26 | db[model.name] = model; 27 | }); 28 | 29 | Object.keys(db).forEach(modelName => { 30 | if (db[modelName].associate) { 31 | db[modelName].associate(db); 32 | } 33 | }); 34 | 35 | db.sequelize = sequelize; 36 | db.Sequelize = Sequelize; 37 | 38 | module.exports = db; 39 | -------------------------------------------------------------------------------- /database/models/post.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (sequelize, DataTypes) => { 3 | const Post = sequelize.define('Post', { 4 | title: DataTypes.STRING, 5 | content: DataTypes.TEXT, 6 | userId: DataTypes.INTEGER 7 | }, {}); 8 | Post.associate = function(models) { 9 | // associations can be defined here 10 | Post.hasMany(models.Comment, { 11 | foreignKey: 'postId', 12 | as: 'comments', 13 | onDelete: 'CASCADE', 14 | }); 15 | 16 | Post.belongsTo(models.User, { 17 | foreignKey: 'userId', 18 | as: 'author', 19 | onDelete: 'CASCADE', 20 | }) 21 | }; 22 | return Post; 23 | }; -------------------------------------------------------------------------------- /database/models/user.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (sequelize, DataTypes) => { 3 | const User = sequelize.define('User', { 4 | name: DataTypes.STRING, 5 | email: DataTypes.STRING 6 | }, {}); 7 | User.associate = function(models) { 8 | // associations can be defined here 9 | User.hasMany(models.Post, { 10 | foreignKey: 'userId', 11 | as: 'posts', 12 | onDelete: 'CASCADE', 13 | }); 14 | 15 | User.hasMany(models.Comment, { 16 | foreignKey: 'userId', 17 | as: 'comments', 18 | onDelete: 'CASCADE', 19 | }); 20 | }; 21 | return User; 22 | }; -------------------------------------------------------------------------------- /database/seeders/20190806135744-User.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.bulkInsert( 3 | 'Users', 4 | [ 5 | { 6 | name: 'Jane Doe', 7 | email: 'janedoe@example.com', 8 | createdAt: new Date(), 9 | updatedAt: new Date(), 10 | }, 11 | { 12 | name: 'Jon Doe', 13 | email: 'jondoe@example.com', 14 | createdAt: new Date(), 15 | updatedAt: new Date(), 16 | }, 17 | ], 18 | {}, 19 | ), 20 | 21 | down: (queryInterface, Sequelize) => queryInterface.bulkDelete('Users', null, {}), 22 | }; 23 | -------------------------------------------------------------------------------- /database/seeders/20190806135753-Post.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => 3 | queryInterface.bulkInsert( 4 | "Posts", 5 | [ 6 | { 7 | userId: 1, 8 | title: "hispotan de nu", 9 | content: 10 | "Nulla mollis molestie lorem. Quisque ut erat. Curabitur gravida nisi at nibh.", 11 | createdAt: new Date(), 12 | updatedAt: new Date() 13 | }, 14 | { 15 | userId: 2, 16 | title: 'some dummy title', 17 | content: 18 | "Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque. Quisque porta volutpat erat.", 19 | createdAt: new Date(), 20 | updatedAt: new Date() 21 | } 22 | ], 23 | 24 | {} 25 | ), 26 | 27 | down: (queryInterface, Sequelize) => 28 | queryInterface.bulkDelete("Posts", null, {}) 29 | }; 30 | -------------------------------------------------------------------------------- /database/seeders/20190806135804-Comment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => 3 | queryInterface.bulkInsert( 4 | "Comments", 5 | [ 6 | { 7 | userId: 1, 8 | postId: 2, 9 | comment: 10 | "Nulla mollis molestie lorem. Quisque ut erat. Curabitur gravida nisi at nibh.", 11 | createdAt: new Date(), 12 | updatedAt: new Date() 13 | }, 14 | { 15 | userId: 2, 16 | postId: 1, 17 | comment: 18 | "Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque. Quisque porta volutpat erat.", 19 | createdAt: new Date(), 20 | updatedAt: new Date() 21 | } 22 | ], 23 | {} 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => 27 | queryInterface.bulkDelete("Comments", null, {}) 28 | }; 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const server = require('./server'); 4 | 5 | const PORT = process.env.PORT || 3300; 6 | 7 | server.listen(PORT, () => console.log(`Server is live at localhost:${PORT}`)); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequelize-with-postgres", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "orie chinedu", 6 | "license": "MIT", 7 | "scripts": { 8 | "start-dev": "nodemon index.js", 9 | "migrate": "npx sequelize-cli db:migrate", 10 | "test": "cross-env NODE_ENV=test jest --testTimeout=10000", 11 | "migrate:reset": "npx sequelize-cli db:migrate:undo:all && npm run migrate", 12 | "pretest": "cross-env NODE_ENV=test npm run migrate:reset", 13 | "coverage": "npm run pretest && jest --coverage && cat ./coverage/lcov.info | coveralls" 14 | }, 15 | "jest": { 16 | "testEnvironment": "node", 17 | "coveragePathIgnorePatterns": [ 18 | "/node_modules/" 19 | ], 20 | "verbose": true 21 | }, 22 | "dependencies": { 23 | "dotenv": "^8.0.0", 24 | "express": "^4.17.1", 25 | "pg": "^7.12.0", 26 | "pg-hstore": "^2.3.3", 27 | "sequelize": "^5.12.3" 28 | }, 29 | "devDependencies": { 30 | "coveralls": "^3.0.6", 31 | "cross-env": "^5.2.0", 32 | "jest": "^24.9.0", 33 | "nodemon": "^1.19.1", 34 | "supertest": "^4.0.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/nedssoft/sequelize-with-postgres-tutorial.svg?branch=master)](https://travis-ci.org/nedssoft/sequelize-with-postgres-tutorial) [![Coverage Status](https://coveralls.io/repos/github/oriechinedu/sequelize-with-postgres-tutorial/badge.svg?branch=master)](https://coveralls.io/github/oriechinedu/sequelize-with-postgres-tutorial?branch=master) 2 | 3 | # Sequelize, PostgreSQL, Node.js tutorial 4 | 5 | 6 | # Usage 7 | 8 | - `git clone git@github.com:nedssoft/sequelize-with-postgres-tutorial.git && cd sequelize-with-postgres-tutorial` 9 | - `cp .env.example .env` 10 | - Create two Postgres databases one for test and the other for development and assign the values of the connection strings to `DEV_DATABASE_URL` and `TEST_DATABASE_URL`= respectively. 11 | - `npm install` 12 | - npm run dev 13 | - Keep working 🔥 14 | 15 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 2 | const { Router } = require('express'); 3 | const controllers = require('../controllers'); 4 | 5 | const router = Router(); 6 | 7 | router.get('/', (req, res) => res.send('Welcome')) 8 | 9 | router.post('/posts', controllers.createPost); 10 | router.get('/posts', controllers.getAllPosts); 11 | router.get('/posts/:postId', controllers.getPostById); 12 | router.put('/posts/:postId', controllers.updatePost); 13 | router.delete('/posts/:postId', controllers.deletePost); 14 | 15 | module.exports = router; -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const routes = require('../routes'); 3 | 4 | const server = express(); 5 | server.use(express.json()); 6 | 7 | server.use('/api', routes); 8 | 9 | module.exports = server; -------------------------------------------------------------------------------- /tests/routes.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../server'); 3 | 4 | describe('Post Endpoints', () => { 5 | it('should create a new post', async () => { 6 | const res = await request(app) 7 | .post('/api/posts') 8 | .send({ 9 | userId: 1, 10 | title: 'test is cool', 11 | content: 'Lorem ipsum', 12 | }); 13 | expect(res.statusCode).toEqual(201); 14 | expect(res.body).toHaveProperty('post'); 15 | }); 16 | 17 | it('should fetch a single post', async () => { 18 | const postId = 1; 19 | const res = await request(app).get(`/api/posts/${postId}`); 20 | expect(res.statusCode).toEqual(200); 21 | expect(res.body).toHaveProperty('post'); 22 | }); 23 | 24 | it('should fetch all posts', async () => { 25 | const res = await request(app).get('/api/posts'); 26 | expect(res.statusCode).toEqual(200); 27 | expect(res.body).toHaveProperty('posts'); 28 | expect(res.body.posts).toHaveLength(1); 29 | }); 30 | 31 | it('should update a post', async () => { 32 | const res = await request(app) 33 | .put('/api/posts/1') 34 | .send({ 35 | userId: 1, 36 | title: 'updated title', 37 | content: 'Lorem ipsum', 38 | }); 39 | 40 | expect(res.statusCode).toEqual(200); 41 | expect(res.body).toHaveProperty('post'); 42 | expect(res.body.post).toHaveProperty('title', 'updated title'); 43 | }); 44 | 45 | it('should return status code 500 if db constraint is violated', async () => { 46 | const res = await request(app) 47 | .post('/api/posts') 48 | .send({ 49 | title: 'test is cool', 50 | content: 'Lorem ipsum', 51 | }); 52 | expect(res.statusCode).toEqual(500); 53 | expect(res.body).toHaveProperty('error'); 54 | }); 55 | 56 | it('should delete a post', async () => { 57 | const res = await request(app).delete('/api/posts/1'); 58 | expect(res.statusCode).toEqual(204); 59 | }); 60 | 61 | it('should respond with status code 404 if resource is not found', async () => { 62 | const postId = 1; 63 | const res = await request(app).get(`/api/posts/${postId}`); 64 | expect(res.statusCode).toEqual(404); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/sample.test.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Sample Test', () => { 3 | it('should test that true === true', () => { 4 | expect(true).toBe(true); 5 | }); 6 | }); --------------------------------------------------------------------------------