diff --git a/.gitignore b/.gitignore index ad46b30..c475476 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +#idea files +*.idea + # Runtime data pids *.pid @@ -59,3 +62,4 @@ typings/ # next.js build output .next +/package-lock.json diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..5c98b42 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/README.md b/README.md index ae2ad33..1ca2fff 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,12 @@ Here is an example of creating a pull request from a fork of a repository: ## NOTE If you are not storing the data in-memory, please commit a sql dump file of the database schema to the repo. Please add a note of where this file is located in this `README.md` if the sql dump is not located at the root of the repo. Your submission will be **DISCARDED** if you **DO NOT** commit a sql dump file for your implementation, if it uses a database. + + +## BEFORE TO START +Please update db.env file with your db credentials + +After run +```` +npm start +```` diff --git a/db.env b/db.env new file mode 100644 index 0000000..674b96c --- /dev/null +++ b/db.env @@ -0,0 +1,5 @@ +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_USER=admin +DB_PASS=admin +DB_NAME=todos diff --git a/dbModel.js b/dbModel.js new file mode 100644 index 0000000..f57808b --- /dev/null +++ b/dbModel.js @@ -0,0 +1,34 @@ +const Sequelize = require('sequelize'); +const Todo = require('./model/Todo'); +env = require('dotenv').config({ path: './db.env' }); + +// instance of sequelize ORM +const sequelize = + new Sequelize( + 'postgres://'+ + env.parsed.DB_USER+':'+ + env.parsed.DB_PASS+'@'+ + env.parsed.DB_HOST+':'+ + env.parsed.DB_PORT+'/'+ + env.parsed.DB_NAME + ); + +let todoModel = new Todo(sequelize); + +connectToDB = async() => { + try{ + await sequelize.authenticate(); + console.log('Connection has been established successfully.'); + } catch (err) { + console.error('Unable to connect to the database:', err); + } +}; + +connectToDB(); + +const dbModel = { + // add others model here... + Todo: todoModel +}; + +module.exports = dbModel; diff --git a/dump.sql b/dump.sql new file mode 100644 index 0000000..55e0582 --- /dev/null +++ b/dump.sql @@ -0,0 +1,94 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 11.3 +-- Dumped by pg_dump version 11.2 + +-- Started on 2020-03-08 14:52:31 CET + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- TOC entry 3183 (class 1262 OID 16494) +-- Name: todos; Type: DATABASE; Schema: -; Owner: admin +-- + +CREATE DATABASE todos WITH TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'C' LC_CTYPE = 'C'; + + +ALTER DATABASE todos OWNER TO admin; + +\connect todos + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_with_oids = false; + +-- +-- TOC entry 196 (class 1259 OID 16549) +-- Name: todo; Type: TABLE; Schema: public; Owner: admin +-- + +CREATE TABLE public.todo ( + id character varying NOT NULL, + description character varying NOT NULL, + "createdAt" character varying NOT NULL, + completed boolean NOT NULL, + priority integer NOT NULL +); + + +ALTER TABLE public.todo OWNER TO admin; + +-- +-- TOC entry 3177 (class 0 OID 16549) +-- Dependencies: 196 +-- Data for Name: todo; Type: TABLE DATA; Schema: public; Owner: admin +-- + +COPY public.todo (id, description, "createdAt", completed, priority) FROM stdin; +56e29274-ce4f-4206-9f68-f7779e273de5 Learn Apollo 2020-03-08 13:21:10.044 +00:00 f 2 +ae1119cf-35c7-4bdd-a92a-1140791d350f Learn graphQL 2020-03-08 13:31:30.580 +00:00 f 1 +\. + + +-- +-- TOC entry 3055 (class 2606 OID 16556) +-- Name: todo todo_pkey; Type: CONSTRAINT; Schema: public; Owner: admin +-- + +ALTER TABLE ONLY public.todo + ADD CONSTRAINT todo_pkey PRIMARY KEY (id); + + +-- Completed on 2020-03-08 14:52:31 CET + +-- +-- PostgreSQL database dump complete +-- + +-- Completed on 2020-03-08 14:52:31 CET + +-- +-- PostgreSQL database cluster dump complete +-- + diff --git a/index.js b/index.js new file mode 100644 index 0000000..95edfbd --- /dev/null +++ b/index.js @@ -0,0 +1,24 @@ +const { ApolloServer } = require('apollo-server'); +const typeDefs = require('./schema'); +const queries = require('./query/todoQuery'); +const mutations = require('./mutation/todoMutation'); + +const resolvers = { + Query: { + getTodos: queries.getTodos, + getCompletedTodos: queries.getCompletedTodos + }, + Mutation: { + createTodo: mutations.createTodo, + updateTodo: mutations.updateTodo, + markTodoAsCompleted: mutations.markTodoAsCompleted, + deleteTodo: mutations.deleteTodo + } +}; + +const server = new ApolloServer({ typeDefs, resolvers }); + +// The `listen` method launches a web server. +server.listen().then(({ url }) => { + console.log(`🚀 Server ready at ${url}`); +}); diff --git a/model/Todo.js b/model/Todo.js new file mode 100644 index 0000000..44c89e3 --- /dev/null +++ b/model/Todo.js @@ -0,0 +1,26 @@ +const Sequelize = require('sequelize'); + +class Todo { + constructor(sequelize){ + return sequelize.define('todo', { + // attributes + description: { + type: Sequelize.STRING, + allowNull: false + }, + completed: { + type: Sequelize.BOOLEAN, + allowNull: false + }, + priority: { + type: Sequelize.SMALLINT, + allowNull: false + } + }, { + updatedAt: false, + tableName: "todo" + }) + } +} + +module.exports = Todo; diff --git a/mutation/todoMutation.js b/mutation/todoMutation.js new file mode 100644 index 0000000..5644e96 --- /dev/null +++ b/mutation/todoMutation.js @@ -0,0 +1,98 @@ +const dbModel = require('../dbModel'); +const genericError = require('../shared/genericError'); +const { v4: uuidv4 } = require('uuid'); + +/** + * List of all Mutation relatives to graphQL Schema + */ + +// Create a todoItem +const createTodo = async (_, args) => { + const todo = { + id: uuidv4(), + description: args.description, + createdAt: new Date(), + completed: false, + priority: (args.priority >= 0) ? args.priority : 1 + }; + try{ + await dbModel.Todo.create(todo); + return { + success: true, + data: todo + } + }catch (e) { + return genericError; + } +}; + +// Update a description or priority of todoItem by id of item +const updateTodo = async (_, args) => { + try{ + let fieldToUpdate = {}; + if(args.description){ + fieldToUpdate.description = args.description + } + if(args.priority && args.priority > 0){ + fieldToUpdate.priority = args.priority; + } + await dbModel.Todo.update(fieldToUpdate, + { + where:{ + id: args.id + } + }); + return { + success: true + } + } catch (e) { + return genericError; + } + +}; + +// Update a completed as true of todoItem by id of item +const markTodoAsCompleted = async (_, args) => { + try{ + await dbModel.Todo.update({completed: true}, + { + where:{ + id: args.id + } + }); + return { + success: true + } + } catch (e) { + return genericError; + } + +}; + +// delete a todoItem by id of item +const deleteTodo = async (_, args) => { + try{ + await dbModel.Todo.destroy( + { + where:{ + id: args.id + } + }); + return { + success: true + } + } catch (e) { + return genericError; + } + +}; + +const mutations = { + // Add others mutations here... + createTodo, + updateTodo, + markTodoAsCompleted, + deleteTodo +}; + +module.exports = mutations; diff --git a/package.json b/package.json new file mode 100644 index 0000000..951b066 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "graphql-example", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "apollo-server": "^2.11.0", + "dotenv": "^8.2.0", + "graphql": "^14.6.0", + "pg": "^7.18.2", + "pg-hstore": "^2.3.3", + "sequelize": "^5.21.5", + "sqlite3": "^4.1.1", + "uuid": "^7.0.2" + } +} diff --git a/query/todoQuery.js b/query/todoQuery.js new file mode 100644 index 0000000..d59fe07 --- /dev/null +++ b/query/todoQuery.js @@ -0,0 +1,48 @@ +const dbModel = require('../dbModel'); +const genericError = require('../shared/genericError'); + +/** + * List of all Query relatives to graphQL Schema + */ + +// Returns all todos, possible ordered it +const getTodos = async (_, args) => { + let todos = []; + try{ + if(args.sortBy && args.direction){ + todos = await dbModel.Todo.findAll({ + order: [ + [args.sortBy, args.direction] + ] + }) + } else { + todos = await dbModel.Todo.findAll(); + } + + } catch (e) { + return genericError + } + + return todos; +}; + +// Returns all completed todos +const getCompletedTodos = async (_, args) => { + try{ + return await dbModel.Todo.findAll({ + where:{ + completed: true + } + }); + } catch (e) { + return genericError + } +}; + +const queries = { + // Add others queries here... + getTodos, + getCompletedTodos +}; + +module.exports = queries; diff --git a/schema.js b/schema.js new file mode 100644 index 0000000..fef1633 --- /dev/null +++ b/schema.js @@ -0,0 +1,75 @@ +const { gql } = require('apollo-server'); + +const typeDefs = gql` + # Comments in GraphQL strings (such as this one) start with the hash (#) symbol. + + # This "Todo" type defines the queryable fields for every todo in our data source. + type Todo { + id: String! + description: String! + createdAt: String + completed: Boolean + priority: Int + } + + type TodoMutationResponse { + success: Boolean! + errorMessage: String + data: Todo + } + + # This "Enum" represents the properties which can use to order Todo list. + enum PropertyToOrderBy{ + priority + createdAt + completed + } + + #This "Enum" represents the direction of sorted + enum Direction{ + ASC + DESC + } + + type Query { + """ + getTodos query returns an array of zero or more Todos. + it is also possible to return the ordered list, by default getTodos return an un-ordered list. + """ + getTodos(sortBy: PropertyToOrderBy, direction: Direction): [Todo] + + + """ + getCompletedTodos query returns an array of zero or more of Todos with completed = true + """ + getCompletedTodos: [Todo] + } + + type Mutation { + """ + createTodo mutation: possible to add a todo item + """ + createTodo(description: String!, priority: Int): TodoMutationResponse + + + """ + updateTodo mutation: possible to modify a todo item + """ + updateTodo(id: String!, description: String, priority: Int): TodoMutationResponse + + + """ + markTodoAsCompleted mutation: set todo as completed = true + """ + markTodoAsCompleted(id: String!): TodoMutationResponse + + + """ + deleteTodo mutation: remove a todo from [Todo] + """ + deleteTodo(id: String!): TodoMutationResponse + + } +`; + +module.exports = typeDefs; diff --git a/shared/genericError.js b/shared/genericError.js new file mode 100644 index 0000000..4309d26 --- /dev/null +++ b/shared/genericError.js @@ -0,0 +1,5 @@ +const genericError = { + success: false, + errorMessage: 'Ops something went wrong!' +}; +module.exports = genericError; diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..c919226 --- /dev/null +++ b/test.sh @@ -0,0 +1,9 @@ +curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"query{\n getTodos(sortBy:priority, direction: ASC){\n description\n priority\n \n }\n}\n"}' --compressed +curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"query{\n getTodos{\n description\n priority\n \n }\n}\n"}' --compressed +curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"query{\n getTodos(sortBy:createdAt, direction: DESC){\n description\n priority\n \n }\n}\n"}' --compressed +curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"query{\n getCompletedTodos{\n description\n priority\n \tcompleted\n }\n}\n"}' --compressed +curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"mutation{\n createTodo(description:\"Test\", priority: 1){\n success,\n errorMessage\n data{\n description\n }\n }\n}\n"}' --compressed +curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"mutation{\n createTodo(description:\"Test without priority\"){\n success,\n errorMessage\n data{\n description\n }\n }\n}\n"}' --compressed +curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"mutation{\n updateTodo(id:\"56e29274-ce4f-4206-9f68-f7779e273de5\", priority: 5){\n success,\n errorMessage\n data{\n description\n }\n }\n}\n"}' --compressed +curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"mutation{\n markTodoAsCompleted(id:\"56e29274-ce4f-4206-9f68-f7779e273de5\"){\n success,\n errorMessage\n data{\n description\n }\n }\n}\n"}' --compressed +curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"mutation{\n deleteTodo(id:\"56e29274-ce4f-4206-9f68-f7779e273de5\"){\n success,\n errorMessage\n data{\n description\n }\n }\n}\n"}' --compressed