├── .dockerignore ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .nycrc ├── .travis.yml ├── Dockerfile ├── Procfile ├── Procfile.dev ├── README.md ├── docker-compose.yaml ├── heroku.yml ├── lectures ├── 01-javascript │ ├── 00-js-support.js │ ├── 01-strict-mode.js │ ├── 02-var-let-const.js │ ├── 03-objects.js │ ├── 04-this.js │ ├── 05-strict-operators.js │ ├── 06-prototypes-objects.js │ ├── 07-prototypes-function.js │ ├── 08-prototypes-classes.js │ ├── 09-closures.js │ ├── 10-async.js │ ├── 11-iterators-and-symbols.js │ ├── 12-generators.js │ ├── 13-homework.js │ ├── Node.js Nights - JavaScript.pdf │ ├── README.md │ └── topics.md ├── 02-nodejs │ ├── 00-event-loop.js │ ├── 01-callback.js │ ├── 02-promise.js │ ├── 03-async-await.js │ ├── 04-promisify.js │ ├── 05-emitter.js │ ├── 06-require.js │ ├── README.md │ └── custom-math │ │ ├── constants.js │ │ └── index.js ├── 03-servers │ ├── README.md │ ├── cluster.js │ ├── http-server.js │ └── tcp-server.js ├── 04-architecture │ ├── Node.js_Nights_Architecture.pdf │ └── README.md ├── 05-database │ ├── Node.js_Nights_Databases.pdf │ └── README.md ├── 06-testing │ ├── Node.js_Nights_Testing.pdf │ └── README.md ├── 07-deployment │ └── README.md ├── 08-workers-security │ ├── Node.js Nights - Security.pdf │ ├── Node.js Nights – Workers & Queues.pdf │ └── README.md └── 09-graphql │ ├── Node.js Nights - GraphQL.pdf │ ├── README.md │ └── gql-examples │ ├── 01-expensive-field.json │ ├── 02-hello-graphql.gql │ ├── 02-hello-graphql.json │ ├── 03-queries.gql │ ├── 04-aliases.gql │ ├── 04-aliases.json │ ├── 05-query-variables.gql │ ├── 05-query-variables.json │ ├── 06-fragments.gql │ ├── 07-directives-vars.json │ ├── 07-directives.gql │ ├── 08-mutation-vars.json │ └── 08-mutation.gql ├── package-lock.json ├── package.json ├── scripts └── generate-data.js ├── src ├── app.js ├── config │ ├── default.js │ ├── env │ │ ├── local.js │ │ ├── production.js │ │ └── test.js │ └── index.js ├── controllers │ ├── dogs.js │ └── users.js ├── database │ ├── index.js │ ├── knexfile.js │ ├── migrations │ │ ├── .keep │ │ ├── 20181007200209_create_users_table.js │ │ ├── 20181007214622_create_dogs_tables.js │ │ └── 20181119155602_add_verified_image_to_dogs.js │ ├── models │ │ ├── base.js │ │ ├── dog.js │ │ ├── index.js │ │ └── user.js │ └── seeds │ │ └── .keep ├── graphql │ ├── index.js │ ├── loaders │ │ └── users.js │ ├── resolvers │ │ ├── dogs.js │ │ └── users.js │ ├── schema.js │ └── types │ │ ├── dogs.gql │ │ └── users.gql ├── jobs │ └── verification.js ├── middleware │ ├── authentication.js │ └── errors.js ├── operations │ ├── dogs.js │ └── users.js ├── repositories │ ├── dogs.js │ └── users.js ├── routes │ └── index.js ├── services │ ├── dogapi.js │ └── rekognition.js ├── utils │ ├── crypto.js │ ├── errors.js │ └── logger.js ├── validations │ ├── index.js │ └── schemas │ │ ├── dogs.js │ │ └── users.js └── worker.js └── tests ├── helpers.js ├── integration ├── dogs │ └── create.spec.js └── users │ └── create.spec.js ├── mocha.opts └── unit ├── crypto.spec.js └── example.spec.js /.dockerignore: -------------------------------------------------------------------------------- 1 | *.log 2 | 3 | .git 4 | .nyc_output 5 | coverage 6 | node_modules 7 | lectures -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | AWS_ACCESS_KEY_ID= 2 | AWS_SECRET_ACCESS_KEY= 3 | AWS_REGION= 4 | AWS_S3_BUCKET_NAME= 5 | PORT=3001 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Put this file to the directory where your node.js code is located. This could be the root 4 | // directory, or a subdirectory if your project consists of both node.js and browser code. 5 | module.exports = { 6 | extends: [ 7 | '@strv/javascript/environments/nodejs/v10', 8 | '@strv/javascript/environments/nodejs/optional', 9 | '@strv/javascript/environments/mocha/recommended', 10 | '@strv/javascript/coding-styles/recommended', 11 | ], 12 | 13 | // As of ESLint 4.1, you no longer need to use separate, per-directory .eslintrc.js files and 14 | // instead control per-folder overrides from your central .eslintrc.js file using the overrides 15 | // array. 16 | // See the original blog post on the feature: 17 | // https://eslint.org/blog/2017/06/eslint-v4.1.0-released 18 | overrides: [{ 19 | files: [ 20 | 'test/**', 21 | ], 22 | 23 | rules: { 24 | 'func-names': 'off', 25 | }, 26 | }], 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Garbage 5 | *.log 6 | *~ 7 | *# 8 | .DS_Store 9 | .nyc_output 10 | coverage 11 | 12 | # IDE stuff 13 | .netbeans 14 | nbproject 15 | .idea 16 | .node_history 17 | *.sublime-* 18 | *.atom-* 19 | .vscode 20 | .tern-* 21 | .languagebabel 22 | jsconfig.json 23 | 24 | # Local configuration 25 | .env -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "reporter": [ 4 | "lcov", 5 | "text", 6 | "text-summary" 7 | ], 8 | "include": [ 9 | "src/**/*.js" 10 | ], 11 | "exclude": [ 12 | "src/config", 13 | "src/app.js", 14 | "src/database/migrations/*" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: required 3 | dist: trusty 4 | language: node_js 5 | node_js: 6 | - 11.0.0 7 | 8 | branches: 9 | only: 10 | - master 11 | 12 | cache: 13 | directories: 14 | - node_modules 15 | 16 | services: 17 | - docker 18 | 19 | env: 20 | global: 21 | - NODE_ENV=test 22 | 23 | install: 24 | - npm install 25 | 26 | before_script: 27 | - sudo service postgresql stop 28 | - docker ps 29 | - npm run infra 30 | - sleep 5 31 | - npm run db:migrate 32 | 33 | script: 34 | - npm run test:coverage 35 | 36 | after_success: 37 | - echo Upload code coverage 💪 38 | 39 | deploy: 40 | - provider: heroku 41 | skip_cleanup: true 42 | api_key: 43 | secure: "n0GT0Ohq2rHItRjaTUb28CcD/w59yVl9NtphqF2n5cbAr9ejIoyg8JS3p0RoaEtJuoPT+mCCyCl8WjVI/iiaHnC3OewvL8H7UyPYO2DKwHG9joYVcyUAc+jgb2A66bqFvmUbgpSYlKIZ2IbSUYfOtR5bBVHC7R5M7WAhP72p6hxEYAxLFFAcAZr0wEYNYkdOaLvLtBwuL5YEw3jBkEYs7MAzYVCY3rqc6kojzMGJNQKkH2iPpU3ITr5BnZ/LMR0q9XmwduG2t3wo54ktprcaEwn3CinLX/rZt5qrRBjd6nryzWSo3mDvbanqis9IGKw/mwSMDzeKqrq7VqeziMrk58AJVOVBGw+PFRa9tdPr+glQ7B1dXYCjKlWpdpiPfvZVG5K5WLBOTVEZYeLByuk6JWhLqmmbliCayY1dYhOmyuYX3cyNUTZrNEqhkggYRj8wbsLN9ARnC41np//Hm1CNL1DY1uLoi07gILVCqBh97bBZiYMZ1C9BcZwh9nqhqpoKw9q+8oENLa/t45Aq9y+6r98BraOIYVl6bikZVDCKE0IUPVdYVNnBNXGOCVdnyMgYv4thPIOISQfQ6bH+VBEYP5Lp2blX/ZTDDjO1WdZx6sDxWhwpwQ46jpMyMxcYn7VOAZffJ+nYaRgdpmI1VmiZEpyZGAuXnIFsnVpC31Mn5rU=" 44 | app: nodejs-nights-2018-app 45 | strategy: git 46 | on: 47 | branch: 48 | - master 49 | repo: strvcom/nodejs-nights-2018 50 | 51 | notifications: 52 | email: false -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11-alpine 2 | 3 | RUN apk add --no-cache --update g++ python2 make 4 | 5 | WORKDIR /app 6 | 7 | COPY package*.json /app/ 8 | 9 | RUN npm install --production 10 | 11 | COPY . /app/ 12 | 13 | ENV NODE_ENV=production 14 | 15 | RUN chown -R node:node /app/* 16 | USER node 17 | 18 | CMD node src/app -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: npm run db:migrate 2 | web: node src/app 3 | worker: node src/worker -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: node src/app | pino-pretty 2 | worker: node src/worker | pino-pretty -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | This repository serves as an online course for learning Node.js. 3 | Made by STRV, it’s here to help those who want to learn more about Node.js and backend to develop a backend API. 4 | 5 | #### Goal 6 | To give you a fundamental understanding of backend and Node.js. 7 | 8 | The presenters demonstrated the best practices in building Node.js applications—experience they gained by working on numerous projects. 9 | 10 | #### Source 11 | This online course was created by utilizing material from Node.js Nights—the free offline course with a focus on Node.js created by [STRV](https://www.strv.com/). 12 | 13 | #### Prerequisites 14 | This course requires at least junior-level knowledge of programming. (Experience with javascript, Node.js or backend itself is not necessary.) 15 | 16 | 17 | ## Contents 18 | 19 | This course contains 9 lectures. The first 3 lectures are general, while the other 6 focus on building one simple project from scratch. This allows us to demonstrate the full scope of practices. 20 | Each lecture contains a video recording of the presentation, with live coding and sample codes. 21 | The course focuses on understanding good architectural practices and project setups. Please keep in mind that for the purposes of the course, some information and approaches are simplified compared to big production app processes. This allows us to easily demonstrate fundamental patterns. 22 | 23 | 24 | ## Materials 25 | 26 | #### Branches 27 | - **Master** branch contains the final solution. 28 | - **Lecture branches (e.g. 01-javascript)** contains the part of the project that is covered in the given lecture. 29 | 30 | #### Lectures directory 31 | 32 | These [Lectures](https://github.com/strvcom/nodejs-nights-2018/tree/master/lectures) contain a brief theoretical overview of what was discussed in each given lecture. 33 | 34 | You can find the details of all lectures below: 35 | 36 | 1. [Javascript](https://github.com/strvcom/nodejs-nights-2018/tree/master/lectures/01-javascript) 37 | 2. [Node.js](https://github.com/strvcom/nodejs-nights-2018/tree/master/lectures/02-nodejs) 38 | 3. [Servers](https://github.com/strvcom/nodejs-nights-2018/tree/master/lectures/03-servers) 39 | 4. [Architecture](https://github.com/strvcom/nodejs-nights-2018/tree/master/lectures/04-architecture) 40 | 5. [Database](https://github.com/strvcom/nodejs-nights-2018/tree/master/lectures/05-database) 41 | 6. [Testing](https://github.com/strvcom/nodejs-nights-2018/tree/master/lectures/06-testing) 42 | 7. [Deployment](https://github.com/strvcom/nodejs-nights-2018/tree/master/lectures/07-deployment) 43 | 8. [Workers & Queues and Security](https://github.com/strvcom/nodejs-nights-2018/tree/master/lectures/08-workers-security) 44 | 9. [GraphQL](https://github.com/strvcom/nodejs-nights-2018/tree/master/lectures/09-graphql) 45 | 46 | #### Video recordings 47 | Recordings of all sessions can be found on the YouTube playlist below: 48 | 49 | [https://www.youtube.com/playlist?list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP](https://www.youtube.com/playlist?list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP) 50 | 51 | 52 | ## Used technologies 53 | #### Language & Runtime 54 | - Javascript. ES6 55 | - Node.js 11 56 | 57 | #### Framework 58 | - [Koa](https://github.com/koajs/koa) as web application framework 59 | 60 | #### Database 61 | - [PostgreSQL](https://www.postgresql.org/) as database 62 | - [Objection](https://github.com/sensepost/objection) as ORM 63 | - [Knex](https://github.com/tgriesser/knex) as query builder (for migrations) 64 | 65 | #### Testing 66 | - [Mocha](https://github.com/mochajs/mocha) as the most robust testing framework for Node.js. 67 | - [Sinon.js](https://sinonjs.org/) for mocking. 68 | 69 | #### Containerization 70 | - [Docker](https://www.docker.com/) as very popular and easy-to-use platform for local development and deployment. 71 | 72 | #### CI 73 | - [Travis](https://travis-ci.org/) as Continuous integration 74 | 75 | 76 | #### Speakers 77 | 1. Javascript - [Josef Zavisek](https://github.com/jzavisek) 78 | 2. Node.js - [Miroslav Andrysek](https://github.com/mandrysek) 79 | 3. Servers - [Miroslav Macik](https://github.com/miryn) 80 | 4. Architecture - [Jiri Erhart](https://github.com/snEk42) 81 | 5. Database - [Samuel Prado](https://github.com/skateonrails) 82 | 6. Testing - [David Ruzicka](https://github.com/ruzicka) 83 | 7. Deployment - [Juan Sanchez](https://github.com/jlsan92) 84 | 8. Workers & Queues and Security - [Jan Hovorka](https://github.com/honzahovorka), [Jiri Erhart](https://github.com/snEk42) 85 | 9. GraphQL - [Josef Zavisek](https://github.com/jzavisek) 86 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | 4 | database: 5 | image: sameersbn/postgresql:latest 6 | container_name: nodejs-nights-db 7 | environment: 8 | - DB_NAME=nodejs-nights-local,nodejs-nights-test 9 | - PG_TRUST_LOCALNET=true 10 | ports: 11 | - "5432:5432" 12 | 13 | redis: 14 | image: redis 15 | container_name: nodejs-nights-redis 16 | ports: 17 | - "6379:6379" 18 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile 4 | 5 | release: 6 | command: 7 | - npm run db:migrate 8 | image: web -------------------------------------------------------------------------------- /lectures/01-javascript/00-js-support.js: -------------------------------------------------------------------------------- 1 | // Try to run in: 2 | // Node.js 6.10.0 3 | // Node.js 10.11.0 4 | 5 | // You can use https://github.com/tj/n to switch nodejs versions 6 | 7 | async function test() { 8 | } 9 | -------------------------------------------------------------------------------- /lectures/01-javascript/01-strict-mode.js: -------------------------------------------------------------------------------- 1 | // 'use strict' 2 | 3 | const obj = { 4 | firstName: 'John' 5 | } 6 | 7 | Object.freeze(obj) 8 | obj.firstName = 'Mark' 9 | 10 | console.log(obj.firstName) 11 | -------------------------------------------------------------------------------- /lectures/01-javascript/02-var-let-const.js: -------------------------------------------------------------------------------- 1 | 2 | function test(condition) { 3 | if (condition) { 4 | // var name = 'John' 5 | // const name = 'John' 6 | let name = 'John' 7 | name = 'Mark' 8 | } 9 | 10 | console.log(name) 11 | } 12 | 13 | // Means a constant reference to the object 14 | const obj = { 15 | first: 'John' 16 | } 17 | 18 | obj.first = 'Mark' // This is OK 19 | obj = {} // This is NOT OK 20 | 21 | test(true) 22 | -------------------------------------------------------------------------------- /lectures/01-javascript/03-objects.js: -------------------------------------------------------------------------------- 1 | 2 | const person = { 3 | // Field 4 | firstName: 'John', 5 | // Nested object 6 | address: { 7 | street: 'Rohanske' 8 | }, 9 | // Method 10 | sayHello() { 11 | console.log('Hello') 12 | }, 13 | } 14 | 15 | Object.defineProperty(person, 'age', { 16 | // Whether it can be deleted or the configuration changed 17 | configurable: true, 18 | // Whether it will be visible in for-in loop 19 | enumerable: true, 20 | // Getter 21 | get() { 22 | return 30 23 | } 24 | }) 25 | 26 | for (const fieldName in person) { 27 | console.log(fieldName) 28 | } 29 | 30 | console.log(person.age) 31 | -------------------------------------------------------------------------------- /lectures/01-javascript/04-this.js: -------------------------------------------------------------------------------- 1 | 2 | const killer = { 3 | name: 'John', 4 | counter: 0, 5 | kill: function(who, where) { 6 | console.log(`I am ${this.name} and I will kill ${who} in ${where}.`) 7 | }, 8 | count: function() { 9 | setInterval(() => { 10 | console.log(this.counter++) 11 | }, 1000) 12 | } 13 | } 14 | 15 | // killer.kill('Mark', 'Paris') 16 | 17 | // const kill = killer.kill 18 | // kill('Mark', 'Paris') 19 | 20 | // killer.kill.call({ name: 'Brian' }, 'Mark', 'Paris') 21 | // killer.kill.apply({ name: 'Brian' }, ['Mark', 'Paris']) 22 | 23 | // const killBinded = killer.kill.bind({ name: 'Joe' }) 24 | // killBinded('Mark', 'Paris') 25 | 26 | killer.count() 27 | -------------------------------------------------------------------------------- /lectures/01-javascript/05-strict-operators.js: -------------------------------------------------------------------------------- 1 | 2 | console.log(Number('1') === 1) 3 | console.log('1' == 1) 4 | -------------------------------------------------------------------------------- /lectures/01-javascript/06-prototypes-objects.js: -------------------------------------------------------------------------------- 1 | 2 | // Inspect prototype chain of an object 3 | 4 | const parent = { 5 | name: 'Parent' 6 | } 7 | 8 | const child = Object.create(parent) 9 | child.age = 20 10 | child.name = 'Child' 11 | 12 | console.log(child.name) 13 | 14 | console.log(child.__proto__.__proto__ === Object.prototype) 15 | console.log(Object.getPrototypeOf(child) === parent) 16 | 17 | child.toString() 18 | 19 | // Inspect prototype chain of a function 20 | 21 | function test() { 22 | } 23 | 24 | console.log(Object.getPrototypeOf(test) === Function.prototype) 25 | console.log(Object.getOwnPropertyNames(Function.prototype)) 26 | console.log(Function.__proto__.__proto__ === Object.prototype) 27 | -------------------------------------------------------------------------------- /lectures/01-javascript/07-prototypes-function.js: -------------------------------------------------------------------------------- 1 | 2 | // Constructor function 3 | function Killer() { 4 | this.name = 'John' 5 | } 6 | 7 | // This object will be used as a prototype for newly created object 8 | const KillerProto = { 9 | kill() { 10 | console.log('Killing') 11 | } 12 | } 13 | 14 | // This reference is used when the function is used as a constructor function 15 | Killer.prototype = KillerProto 16 | 17 | const john = new Killer() 18 | console.log(Object.getPrototypeOf(john) === KillerProto) 19 | console.log(john.__proto__.__proto__ === Object.prototype) 20 | 21 | console.log(john.hasOwnProperty('kill')) 22 | 23 | // Null & undefined - there's a difference but we usually don't care that much 24 | 25 | console.log(undefined == null) 26 | console.log(undefined === null) 27 | 28 | const obj = { 29 | firstName: 'John', 30 | lastName: undefined, 31 | } 32 | 33 | console.log(obj.lastName) 34 | -------------------------------------------------------------------------------- /lectures/01-javascript/08-prototypes-classes.js: -------------------------------------------------------------------------------- 1 | 2 | // Classes are just a better syntax for constructor functions 3 | 4 | class Killer { 5 | kill() { 6 | console.log('Killing') 7 | } 8 | } 9 | 10 | class ShootingKiller extends Killer { 11 | shoot() { 12 | console.log('Shooting') 13 | } 14 | } 15 | 16 | const john = new ShootingKiller() 17 | 18 | console.log(john.__proto__ === ShootingKiller.prototype) 19 | console.log(john.__proto__.__proto__ === Killer.prototype) 20 | -------------------------------------------------------------------------------- /lectures/01-javascript/09-closures.js: -------------------------------------------------------------------------------- 1 | 2 | // Closure function can hold references to outer scope 3 | 4 | function getAnswerToEverythingFunc() { 5 | const result = 42 6 | return () => { 7 | console.log(`Answer is ${result}`) 8 | } 9 | } 10 | 11 | const giveMeAnswer = getAnswerToEverythingFunc() 12 | 13 | // Here, "result" variable cannot be garbage collected yet 14 | // even though getAnswerToEverythingFunc completed. That's 15 | // because of the closure reference. Beware of memory leaks. 16 | 17 | giveMeAnswer() 18 | -------------------------------------------------------------------------------- /lectures/01-javascript/10-async.js: -------------------------------------------------------------------------------- 1 | 2 | // ------ Callback style ------------------------------------------------------ 3 | 4 | /* 5 | 6 | function callApi(url, callback) { 7 | setTimeout(() => { 8 | // Error goes as a first parameter, always! This is a convention 9 | // used in all native Node.js modules. 10 | callback(null, `Result: ${url}`) 11 | }, 1000) 12 | } 13 | 14 | // This is callback hell 15 | callApi('api.google.com', (err, data) => { 16 | console.log(data) 17 | callApi('api.microsoft.com', (innerErr, innerData) => { 18 | console.log(innerData) 19 | callApi('api.apple.com', (subErr, subData) => { 20 | console.log(`${data} ${subData}`) 21 | }) 22 | }) 23 | }) 24 | 25 | */ 26 | 27 | // ------ Promise style ------------------------------------------------------- 28 | 29 | /* 30 | 31 | function callApi(url) { 32 | return new Promise((resolve, reject) => { 33 | setTimeout(() => { 34 | // if (somethingBadHappens) { 35 | // return reject(new Error('Some error')) 36 | // } 37 | resolve(`Result: ${url}`) 38 | }, 1000) 39 | }) 40 | } 41 | 42 | let result 43 | callApi('api.google.com') 44 | .then(data => { 45 | result = data 46 | console.log(data) 47 | return callApi('api.microsoft.com') 48 | }) 49 | .then(innerData => { 50 | console.log(innerData) 51 | return callApi('api.apple.com') 52 | }) 53 | .then(subData => { 54 | console.log(`${result} ${subData}`) 55 | }) 56 | .catch(err => { 57 | console.error(err, 'Something bad happened') 58 | }) 59 | 60 | */ 61 | 62 | // ------ Async await style --------------------------------------------------- 63 | 64 | function callApi(url) { 65 | return new Promise((resolve, reject) => { 66 | setTimeout(() => { 67 | resolve(`Result: ${url}`) 68 | }, 1000) 69 | }) 70 | } 71 | 72 | async function run() { 73 | 74 | const data = await callApi('api.google.com') 75 | // Context is automatically loaded here 76 | console.log(data) 77 | const innerData = await callApi('api.microsoft.com') 78 | console.log(innerData) 79 | const subData = await callApi('api.apple.com') 80 | console.log(`${data}, ${subData}`) 81 | 82 | // We can await all at once 83 | /* 84 | const [data, innerData, subData] = await Promise.all([ 85 | callApi('api.google.com'), 86 | callApi('api.microsoft.com'), 87 | callApi('api.apple.com') 88 | }) 89 | 90 | console.log(data) 91 | console.log(innerData) 92 | console.log(`${data}, ${subData}`) 93 | */ 94 | } 95 | 96 | // Result of an async function is Promise 97 | run() 98 | .then(() => console.log('All APIs called')) 99 | .catch(err => { 100 | console.log('Something bad happened') 101 | }) 102 | 103 | console.log('When will this happen?') 104 | -------------------------------------------------------------------------------- /lectures/01-javascript/11-iterators-and-symbols.js: -------------------------------------------------------------------------------- 1 | 2 | // Symbols are unique 3 | const symbolA = Symbol('A') 4 | const symbolB = Symbol('B') 5 | console.log(symbolA === symbolB) 6 | 7 | // Iterable collection has a special field with Symbol.iterator key 8 | const arr = [1, 2, 3, 4, 5] 9 | const iterator = arr[Symbol.iterator]() 10 | 11 | // Custom iterator - an object with single method "next" that returns 12 | // { value: ..., done: true/false } object on each call. 13 | function getIterator() { 14 | const colors = ['white', 'blue', 'red'] 15 | let currentIndex = 0 16 | return { 17 | next: function() { 18 | if (currentIndex < colors.length) { 19 | const currentValue = colors[currentIndex] 20 | currentIndex++ 21 | return { 22 | done: false, 23 | value: currentValue, 24 | } 25 | } 26 | 27 | return { done: true } 28 | } 29 | } 30 | } 31 | 32 | // If Symbol.iterator field is defined, we can use the object in for-of loop 33 | const myIterableObj = { 34 | [Symbol.iterator]: getIterator 35 | } 36 | 37 | for (const item of myIterableObj) { 38 | console.log(item) 39 | } 40 | -------------------------------------------------------------------------------- /lectures/01-javascript/12-generators.js: -------------------------------------------------------------------------------- 1 | 2 | // Generators are special functions that generate items. Caller of the generator can specify 3 | // how many iterations are needed. 4 | 5 | function * generateNumbers() { 6 | yield 1 7 | yield 2 8 | } 9 | 10 | // Generators can be combined together with yield* 11 | function * generateColors() { 12 | const value = yield 'red' 13 | yield* generateNumbers() 14 | yield 'blue' 15 | yield 'green' 16 | } 17 | 18 | // We can pass value back to the generator. This principle is used in redux-sagas library 19 | // (quite popular on frontend). 20 | const colorsIterator = generateColors() 21 | console.log(colorsIterator.next()) 22 | console.log(colorsIterator.next('value')) 23 | console.log(colorsIterator.next()) 24 | console.log(colorsIterator.next()) 25 | -------------------------------------------------------------------------------- /lectures/01-javascript/13-homework.js: -------------------------------------------------------------------------------- 1 | 2 | // Install with: 3 | // npm install request 4 | // npm install request-promise 5 | 6 | const request = require('request-promise') 7 | 8 | const BASE_URL = 'http://swapi.co/api' 9 | 10 | async function run() { 11 | const result = await request(`${BASE_URL}/people/1`) 12 | console.log(result) 13 | } 14 | 15 | run() 16 | -------------------------------------------------------------------------------- /lectures/01-javascript/Node.js Nights - JavaScript.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/nodejs-open-knowledge/582e74ca81567dfe45ab8f31a22f2ec605e1caff/lectures/01-javascript/Node.js Nights - JavaScript.pdf -------------------------------------------------------------------------------- /lectures/01-javascript/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript lecture summary 2 | 3 | - [Topics covered](./topics.md) 4 | - [Presentation](https://docs.google.com/presentation/d/1C1bWK_-J_7KjjXM2SxbeIRa4Uzxn7QfIbS6u_y6EDos/edit?usp=sharing) 5 | - [Video](https://www.youtube.com/watch?v=M3k3FneJmQU&list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP&index=2&t=0s) 6 | 7 | ## Homework 8 | 9 | ### Summary: 10 | Get names of all vehicles owned by `Luke Skywalker`. Use Starwars API to retrieve data (). 11 | 12 | ### Workflow: 13 | 1. Make a request to retrieve Luke Skywalker (`GET http://swapi.co/api/people/1`) 14 | 2. You will retrieve Luke detail with `vehicles` array 15 | 3. Then retrieve all vehicles by making requests to vehicle URL (e.g. `GET http://swapi.co/api/vehicles/14`) 16 | 4. Use `map` function () to retrieve vehicles names and dump them to the console 17 | 18 | ### Implementation details: 19 | 1. Install Node.js latest from 20 | 2. Implement workflow with callbacks 21 | - use `request` package to make requests 22 | - package documentation is here: 23 | - to install the package use `npm install request` (`npm` is Node Package Manager automatically installed with Node.js) 24 | 25 | 3. Implement workflow with promises 26 | - use `request-promise` package to make requests 27 | - to install the package use `npm install request request-promise` (`request-promise` package needs `request` package as well, it uses it internally) 28 | 29 | 4. Implement workflow with async await 30 | - `async await` works with promises so use the same `request-promise` package 31 | -------------------------------------------------------------------------------- /lectures/01-javascript/topics.md: -------------------------------------------------------------------------------- 1 | # JavaScript lecture 2 | 3 | ## A. Intro 4 | - Why would anyone use JavaScript 5 | - IDE - VSCode, WebStorm, Atom, Sublime 6 | - EcmaScript (ES5, ES6, ES7) and browser support 7 | - mention [Babel](https://babeljs.io/repl/) 8 | 9 | ## B. Install Node.js 10 | - install from web (LTS vs Current support) [Node.js](https://nodejs.org/en/) 11 | - version manager: 12 | - `npm install -g n` 13 | - `n latest` 14 | 15 | ## C. JavaScript 16 | ### 0. JS support 17 | - 18 | 19 | ### 1. `use strict` 20 | - undeclared variable demo 21 | - `Object.freeze()` demo 22 | - `--use_strict` flag 23 | 24 | ### 2. Functions & Variables 25 | - `var`, `let`, `const` 26 | 27 | ### 3. Objects 28 | - fields: 29 | - static name (key) 30 | - dynamic name 31 | - via `Object.defineProperty` 32 | - enumerable - visible in `for .. in` iterations 33 | - writable - getter, setter 34 | - configurable - true, when it can be deleted or property descriptor changed 35 | - via Symbols 36 | 37 | - `for .. in` loop 38 | - functions: 39 | - separate - defines own this 40 | - object functions 41 | - arrow functions 42 | - delete operator 43 | 44 | ### 4. `this` binding 45 | 46 | ### 5. strict operators `==` vs `===` 47 | 48 | ### 6. Prototypes 49 | - via Object.create 50 | - `for .. in` loop and `hasOwnProperty` 51 | - via constructor Function 52 | - via classes 53 | 54 | ### 7. Closures 55 | - a function with outer context 56 | - A closure is the combination of a function and the lexical environment within which that function was declared. 57 | 58 | ### 8. Async constructs 59 | - callbacks & callback hell 60 | - converting them to promises (with default callback param) 61 | - Promises 62 | - `async await` 63 | 64 | ### 9. Iterators & Symbols 65 | ### 10. Generators 66 | 67 | ## Resources 68 | - [Writable, enumerable, configurable](http://arqex.com/967/javascript-properties-enumerable-writable-configurable) 69 | - [Node version manager](https://github.com/tj/n) 70 | -------------------------------------------------------------------------------- /lectures/02-nodejs/00-event-loop.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function fn() { 4 | console.log('1.', 'synchronous function') 5 | } 6 | 7 | setTimeout(() => { 8 | console.log('3.', 'setTimeout with 0ms') 9 | }, 0) 10 | 11 | setTimeout(() => { 12 | console.log('5.', 'setTimeout with 2000ms/2s') 13 | }, 2000) 14 | 15 | setImmediate(() => { 16 | console.log('4.', 'setImmediate') 17 | }) 18 | 19 | process.nextTick(() => { 20 | console.log('2.', 'nextTick') 21 | }) 22 | 23 | fn() 24 | 25 | /** 26 | Result: 27 | 1. synchronous function 28 | 2. nextTick 29 | 3. setTimeout with 0ms 30 | 4. setImmediate 31 | 5. setTimeout with 2000ms/2s 32 | */ 33 | -------------------------------------------------------------------------------- /lectures/02-nodejs/01-callback.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Some asynchronous blocking method 4 | function getRequest(uri, cb) { 5 | setTimeout(() => { 6 | cb(null, `Hello from ${uri}`) 7 | }, 3000) 8 | } 9 | 10 | getRequest('api.google.com', (err, response) => { 11 | if (err) { 12 | console.error('Error:', err) 13 | return 14 | } 15 | 16 | console.log(response) 17 | 18 | return getRequest('api.azure.com', (err, response) => { 19 | if (err) { 20 | console.error('Error:', err) 21 | return 22 | } 23 | 24 | console.log(response) 25 | 26 | return getRequest('api.aws.com', (err, response) => { 27 | if (err) { 28 | console.error('Error:', err) 29 | return 30 | } 31 | 32 | console.log(response) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /lectures/02-nodejs/02-promise.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const API = { 4 | google: 'api.google.com', 5 | azure: 'api.azure.com', 6 | aws: 'api.aws.com', 7 | } 8 | 9 | // Some asynchronous blocking method 10 | function getRequest(uri) { 11 | return new Promise((resolve, reject) => { 12 | setTimeout(() => { 13 | return resolve(`Hello from ${uri}`) 14 | }, 3000) 15 | }) 16 | } 17 | 18 | getRequest(API.google).then(response => { 19 | console.log('Single request:', response) 20 | 21 | return getRequest(API.azure) 22 | }).then(response => { 23 | console.log('Single request:', response) 24 | 25 | return getRequest(API.aws) 26 | }).then(response => { 27 | console.log('Single request:', response) 28 | }).catch(err => { 29 | console.error('Error:', err) 30 | }) 31 | 32 | // You can send multiple requests at once with Promise.all 33 | function getAll() { 34 | return Promise.all([ 35 | getRequest(API.google), 36 | getRequest(API.azure), 37 | getRequest(API.aws), 38 | ]) 39 | } 40 | 41 | getAll().then(response => { 42 | for (const data of response) { 43 | console.log('Promise.all:', data) 44 | } 45 | }).catch(err => { 46 | console.error('Error:', err) 47 | }) 48 | -------------------------------------------------------------------------------- /lectures/02-nodejs/03-async-await.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const API = { 4 | google: 'api.google.com', 5 | azure: 'api.azure.com', 6 | aws: 'api.aws.com', 7 | } 8 | 9 | // Some asynchronous blocking method 10 | function getRequest(uri) { 11 | return new Promise((resolve, reject) => { 12 | setTimeout(() => { 13 | return resolve(`Hello from ${uri}`) 14 | }, 3000) 15 | }) 16 | } 17 | 18 | async function printData() { 19 | try { 20 | const data0 = await getRequest(API.google) 21 | console.log(data0) 22 | 23 | const data1 = await getRequest(API.azure) 24 | console.log(data1) 25 | 26 | const data2 = await getRequest(API.aws) 27 | console.log(data2) 28 | } catch (err) { 29 | console.error('Error:', err) 30 | } 31 | } 32 | 33 | printData().then() 34 | -------------------------------------------------------------------------------- /lectures/02-nodejs/04-promisify.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const util = require('util') 5 | 6 | const readFile = util.promisify(fs.readFile) 7 | 8 | async function readAllLines(file) { 9 | const data = await readFile(file) 10 | const lines = data.toString().trim().split('\n') 11 | 12 | return lines 13 | } 14 | 15 | async function run() { 16 | const lines = await readAllLines(__filename) 17 | lines.forEach((line, index) => { 18 | console.log(`${index}: ${line}`) 19 | }) 20 | } 21 | 22 | run() 23 | -------------------------------------------------------------------------------- /lectures/02-nodejs/05-emitter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events') 4 | const log = { 5 | info: console.log, 6 | error: console.error, 7 | } 8 | 9 | class TaskProcessor extends EventEmitter { 10 | run(task) { 11 | // this.emit('error', new Error('Something went teribly wrong.')) 12 | 13 | log.info('Before task execution.') 14 | console.time('execute') 15 | this.emit('begin') 16 | console.timeEnd('execute') 17 | task() 18 | this.emit('end') 19 | log.info('After task execution.') 20 | } 21 | } 22 | 23 | // Create instance 24 | const taskProcessor = new TaskProcessor() 25 | 26 | // Add listeners 27 | taskProcessor.addListener('begin', () => { 28 | for (let i = 0; i < 999999999; i++) {} 29 | log.info('Starting execution (from listener).') 30 | }) 31 | taskProcessor.addListener('end', () => log.info('Execution completed (from listener).')) 32 | taskProcessor.on('begin', () => log.info('Starting execution (from listener registered with on).')) 33 | 34 | const customListener = () => log.info('From custom listener.') 35 | taskProcessor.on('begin', customListener) 36 | taskProcessor.removeListener('begin', customListener) 37 | 38 | // taskProcessor.on('error', err => { 39 | // log.error(err, 'Unhandler error occurred.') 40 | // }) 41 | 42 | // process.on('uncaughtException', err => { 43 | // log.error(err, 'Unhandler exeption triggered.') 44 | // process.exit(1) 45 | // }) 46 | 47 | // Execute 48 | taskProcessor.run(() => log.info('Executing task 1.')) 49 | taskProcessor.run(() => log.info('Executing task 2.')) 50 | -------------------------------------------------------------------------------- /lectures/02-nodejs/06-require.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Finds ./custom-math/index.js file 4 | // because default loaded file is index.js 5 | // when your path to module is directory 6 | // require('./custom-math') === require('./custom-math/index.js') 7 | const customMath = require('./custom-math') 8 | 9 | // You can load single files as well 10 | const constants = require('./custom-math/constants') 11 | 12 | console.log('1 + 1 =', customMath.add(1, 1)) 13 | console.log('2 * 10 =', customMath.multiply(2, 10)) 14 | 15 | console.log('PI is', constants.PI) 16 | console.log('E is', constants.E) 17 | 18 | // Search paths for modules if you do not provide relative path 19 | console.log(module.paths) 20 | -------------------------------------------------------------------------------- /lectures/02-nodejs/README.md: -------------------------------------------------------------------------------- 1 | # Node.JS lecture summary 2 | 3 | * [Presentation](https://docs.google.com/presentation/d/14PalrqWD1lNJ3wi443abAxPkNZGTqFDxO3piZ0_SKws/edit?usp=sharing) 4 | * [Video](https://www.youtube.com/watch?v=2OssfjKdXt8&list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP&index=2) 5 | 6 | ## Topics 7 | 8 | ### Event loop 9 | * [Nodejs.org website explanation](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/) 10 | * [Philip Roberts](http://latentflip.com/loupe) - Simulator for event loop. It is mainly for frontend but behavior is similar 11 | 12 | ### Handling blocking operations 13 | * Callbacks - used a lot in Node.JS api 14 | * Promises (async/await) - Object accessible as Global object in Node 15 | * When method use only callbacks (old packages or nodejs api functions) you can promisify callbacks functions 16 | * `Bluebird` module 17 | * `util.promisify` from Node.JS api 18 | 19 | _How to view all Global objects:_ 20 | * _Write `node` in terminal and hit `ENTER`_ 21 | * _Hit two times `TAB`_ 22 | 23 | ### Packages 24 | 25 | #### Package managers 26 | * NPM - [npmjs.com](https://www.npmjs.com) 27 | * Yarn - [yarnpkg.com](https://yarnpkg.com) 28 | * You should use only one of them for your project but you can switch in the future if you want 29 | 30 | #### Your project setup 31 | * `npm init` 32 | * `yarn init` 33 | * Creates package.json - [Specification](https://docs.npmjs.com/files/package.json) 34 | * [Dependencies versions](https://docs.npmjs.com/misc/semver) 35 | 36 | #### Add new package 37 | * `npm i koa` 38 | * `npm i -D mocha` - for development dependency 39 | * `npm i -g eslint` - install package globally 40 | * `yarn add koa` 41 | * `yarn add --dev mocha` - for development dependency 42 | * `yarn global add eslint` - install package globally 43 | 44 | #### Remove package 45 | * `npm un sequlize` 46 | * `yarn remove sequlize` 47 | 48 | #### Check outdated packages 49 | * `npm outdated` 50 | * `yarn outdated` 51 | 52 | #### Update package 53 | * `npm upgrade koa` 54 | * `npm i koa@latest` - force upgrade to newest version 55 | * `yarn upgrade koa` 56 | * `yarn add koa@latest` - force upgrade to newest version 57 | 58 | #### Requiring 59 | * `const myPkg = require('./path/to/my/package')` - With relative or absolute path 60 | * `const myPkg = require('some-package')` - `console.log(module.paths)` you can check paths where node searches for packages 61 | * Node goes through the following sequence of steps when `require()` is used: 62 | * `Resolving`: To find the absolute path of the file. 63 | * `Loading:` To determine the type of the file content. 64 | * `Wrapping`: To give the file its private scope. This is what makes both the require and module objects local to every file we require. 65 | * `Evaluating`: This is what the Virtual Machine eventually does with the loaded code. 66 | * `Caching`: So that when we require this file again, we don’t go over all the steps another time. 67 | 68 | ### Events 69 | * `events` package - [Specification](https://nodejs.org/api/events.html) 70 | * `events` returns `EventEmitter` class 71 | * By default synchronous 72 | * `EventEmitter` in Node.JS are for example servers, streams, file operations, etc. 73 | -------------------------------------------------------------------------------- /lectures/02-nodejs/custom-math/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.PI = Math.PI 4 | 5 | exports.E = Math.E 6 | -------------------------------------------------------------------------------- /lectures/02-nodejs/custom-math/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | add(a, b) { 5 | return a + b 6 | }, 7 | 8 | multiply(a, b) { 9 | return a * b 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /lectures/03-servers/README.md: -------------------------------------------------------------------------------- 1 | # 3. Lecture summary 2 | 3 | This lecture covers 4 | - TCP server 5 | - HTTP server 6 | - REST API guidelines 7 | - Express.js 8 | - Koa.js 9 | - middleware 10 | - context 11 | - router 12 | 13 | Also basics of 14 | - Nodemon 15 | - linting code 16 | - logging 17 | - user input validation 18 | - Node.js clustering 19 | - process signals 20 | 21 | #### Links: 22 | - [Video](https://www.youtube.com/watch?v=ev_s0EyFO-s&list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP&index=3) 23 | - [Slides](https://docs.google.com/presentation/d/1rJOtXGaKL2s90MJCfhjr52E_fab8ww-4PDHFufu1n_Y/edit?usp=sharing) 24 | - [Homework](#homework) 25 | 26 | ## Servers introduction 27 | 28 | This lecture is all about servers. We start with an introduction to TCP and HTTP servers. 29 | 30 | ### Simple TCP server 31 | - build-in `net` package 32 | - instance of `EventEmitter` object 33 | - socket implements `DuplexStream` interface - read/write 34 | - `nc localhost 3000` 35 | 36 | ### Simple HTTP server 37 | - build-in `http` package 38 | - transfer-encoding 39 | - response is streamed, partial chucked responses 40 | - node can send chunks when they are ready instead of caching them in memory 41 | - terminating request is mandatory (`res.end`) 42 | - `curl localhost:3000` or Postman 43 | 44 | ### REST API guidelines 45 | - Representational state transfer 46 | - always go for HTTPS 47 | - use correct HTTP statuses 48 | - see `require('http').STATUS_CODES` 49 | - plural names (e.g. `/dogs/1`) 50 | - PUT vs PATCH (replace vs modify) 51 | 52 | ## Koa 53 | [Koa.js](https://koajs.com) is a minimal and flexible Node.js web application framework, developed by the creators of Express.js. It supports modern features like `async/await` out of the box. 54 | 55 | ### Basic Koa application 56 | Starting a Koa server is super simple: 57 | ```js 58 | const Koa = require('koa') 59 | const app = new Koa() 60 | 61 | app.use(ctx => { 62 | ctx.body = 'Hello World' 63 | }) 64 | 65 | app.listen(3000) 66 | ``` 67 | 68 | ### Middleware 69 | A Koa application is an object containing an array of middleware functions which are composed and executed in a stack-like manner upon request. 70 | 71 | ```js 72 | const app = new Koa() 73 | 74 | /* 75 | Use middleware 76 | */ 77 | app.use(koaCompress()) 78 | app.use(koaCors()) 79 | app.use(koaBody()) 80 | 81 | app.use(async (ctx, next) => { 82 | const start = Date.now() 83 | 84 | await next() 85 | 86 | const ms = Date.now() - start 87 | ctx.set('X-Response-Time', `${ms}ms`) 88 | }) 89 | ``` 90 | 91 | ### Context 92 | A Koa Context encapsulates node's request and response objects into a single object which provides many helpful methods for writing web applications and APIs. 93 | 94 | A Context is created per request, and is referenced in middleware as the receiver, or the ctx identifier, as shown in the following snippet: 95 | 96 | ```js 97 | app.use(async ctx => { 98 | ctx // is the Context 99 | ctx.request // is a Koa Request 100 | ctx.response // is a Koa Response 101 | }) 102 | ``` 103 | 104 | For more info see Koa documentation for the [Context](https://koajs.com/#context), [Request](https://koajs.com/#request), and [Response](https://koajs.com/#response) objects. 105 | 106 | ### Routing 107 | In order to keep things simple and implement routes in a Koa app, we will use middleware library [koa-router](https://github.com/alexmingoia/koa-router). 108 | 109 | ```js 110 | const Koa = require('koa') 111 | const Router = require('koa-router') 112 | 113 | const app = new Koa() 114 | const router = new Router() 115 | 116 | router 117 | .get('/', ctx => { 118 | ctx.body = 'Hello World' 119 | }) 120 | .get('/dogs', ctx => { 121 | ctx.body = dogs 122 | }) 123 | 124 | app 125 | .use(router.routes()) 126 | .use(router.allowedMethods()) // OPTIONS middleware 127 | ``` 128 | 129 | ## Useful stuff 130 | There are a lot of useful things we've learned during the lesson. 131 | 132 | ### Nodemon 133 | - restart your node application when file changes in the directory are detected 134 | - https://www.npmjs.com/package/nodemon 135 | - `nodemon .` 136 | 137 | ### Linter 138 | _A linter or lint refers to tools that analyze source code to flag programming errors, bugs, stylistic errors, and suspicious constructs._ 139 | 140 | - https://eslint.org 141 | - `.eslintrc.js` 142 | - Rule sets: 143 | - [STRV](https://www.npmjs.com/package/@strv/eslint-config-javascript) 144 | - [Airbnb](https://www.npmjs.com/package/eslint-config-airbnb-base) 145 | 146 | ### Logging 147 | Rather than using `console.log` you should use a proper logger. 148 | 149 | Recommended packages 150 | - [Bunyan](https://www.npmjs.com/package/bunyan) 151 | - [Pino](https://www.npmjs.com/package/pino) 152 | 153 | ### Input validation 154 | Validating user input manually is complicated you can easily miss some unwanted cases. But there are packages to the rescue. 155 | 156 | - [jsonschema](https://www.npmjs.com/package/jsonschema) 157 | - [Joi](https://www.npmjs.com/package/joi) 158 | 159 | ### Clustering in Node.js 160 | _A single instance of Node.js runs in a single thread. To take advantage of multi-core systems, the user will sometimes want to launch a cluster of Node.js processes to handle the load._ 161 | 162 | - build-in `cluster` module 163 | - or even simpler [Throng](https://www.npmjs.com/package/throng) package 164 | 165 | ### Process signals 166 | _The `process` object is a `global` that provides information about, and control over, the current Node.js process. As a global, it is always available to Node.js applications without using `require()`._ 167 | 168 | - instance of `EventEmitter` 169 | ```js 170 | process.on('exit', (code) => { 171 | console.log(`About to exit with code: ${code}`); 172 | }); 173 | ``` 174 | 175 | - SIGINT 176 | - SIGTERM 177 | 178 | ## Homework 179 | Create your own Koa server which supports all CRUD (**C**reate, **R**ead, **U**pdate, **D**elete) operations on a collection of dogs. 180 | 181 | You can write everything from scratch (_recommended_), or you can use this repository as a boilerplate. 182 | -------------------------------------------------------------------------------- /lectures/03-servers/cluster.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const cluster = require('cluster') 4 | const http = require('http') 5 | const os = require('os') 6 | 7 | const port = 3000 8 | const numCPUs = os.cpus().length 9 | 10 | /* eslint-disable no-console */ 11 | const log = { 12 | info: console.log, 13 | } 14 | /* eslint-enable no-console */ 15 | 16 | if (cluster.isMaster) { 17 | log.info(`Master ${process.pid} is running`) 18 | 19 | // Fork workers 20 | for (let i = 0; i < numCPUs; i++) { 21 | cluster.fork() 22 | } 23 | 24 | cluster.on('exit', worker => { 25 | log.info(`worker ${worker.process.pid} died`) 26 | }) 27 | } else { 28 | // Workers can share any TCP connection 29 | // In this case it is an HTTP server 30 | http.createServer((req, res) => { 31 | res.writeHead(200) 32 | 33 | setTimeout(() => res.end(`Hello world from worker #${process.pid}\n`), 500) 34 | }).listen(port) 35 | 36 | log.info(`Worker ${process.pid} started`) 37 | } 38 | -------------------------------------------------------------------------------- /lectures/03-servers/http-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Try with `curl localhost:3000` 4 | 5 | const http = require('http') 6 | 7 | const port = 3000 8 | 9 | /* eslint-disable no-console */ 10 | const log = { 11 | info: console.log, 12 | } 13 | /* eslint-enable no-console */ 14 | 15 | const server = http.createServer() 16 | 17 | server.on('request', (req, res) => { 18 | log.info('Incoming request', { 19 | headers: req.headers, 20 | url: req.url, 21 | }) 22 | 23 | res.writeHead(200, { 'Content-Type': 'text/plain' }) 24 | 25 | res.write('Hello from HTTP server.\n') 26 | res.write('First\n') 27 | setTimeout(() => res.write('Second\n'), 1000) 28 | setTimeout(() => res.write('Third\n'), 2000) 29 | setTimeout(() => res.end('Bye.\n'), 3000) 30 | }) 31 | 32 | server.listen(port, () => { 33 | log.info(`Server up and running on port ${port}.`) 34 | }) 35 | -------------------------------------------------------------------------------- /lectures/03-servers/tcp-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Try with `nc localhost 3000` 4 | 5 | const net = require('net') 6 | 7 | const port = 3000 8 | 9 | /* eslint-disable no-console */ 10 | const log = { 11 | info: console.log, 12 | } 13 | /* eslint-enable no-console */ 14 | 15 | const server = net.createServer() 16 | 17 | server.on('connection', socket => { 18 | log.info('Client connected.') 19 | socket.write('Hello client.\n') 20 | 21 | socket.on('data', data => { 22 | log.info('Incoming data:', data) 23 | log.info('Incoming string:', data.toString('utf8').replace(/\n$/u, '')) 24 | 25 | socket.write('Logged: ') 26 | socket.write(data) 27 | }) 28 | 29 | socket.on('end', () => { 30 | log.info('Client disconnected.') 31 | server.close(() => { 32 | log.info('Server closed.') 33 | }) 34 | }) 35 | 36 | // socket.setEncoding('utf8') 37 | }) 38 | 39 | server.listen(port, () => { 40 | const address = server.address() 41 | log.info(`Server listening at ${address.address}:${address.port}`) 42 | }) 43 | -------------------------------------------------------------------------------- /lectures/04-architecture/Node.js_Nights_Architecture.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/nodejs-open-knowledge/582e74ca81567dfe45ab8f31a22f2ec605e1caff/lectures/04-architecture/Node.js_Nights_Architecture.pdf -------------------------------------------------------------------------------- /lectures/04-architecture/README.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | #### Links: 4 | - [Video](https://www.youtube.com/watch?v=WTjDXXl8nNc&list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP&index=4) 5 | - [Slides](https://docs.google.com/presentation/d/1DSO94OQoig2PJSDWmhupqDoLVEApFeFX4Az0gkJ88G0/edit#slide=id.g40dd3244bc_0_2) 6 | - [Homework](#homework) 7 | 8 | Each API request typically goes through several phases when being handled. These phases would more or less be: 9 | 10 | 1. Request data parsing 11 | 2. Request data validation 12 | 3. Permissions validation 13 | 4. Business logic execution 14 | 5. Response data mapping 15 | 16 | Some of these phases may interleave or be omitted, but it's good to be able to identify them and separate them 17 | when our code becomes too complex to conveniently reason about. Different frameworks take different approaches 18 | on how to separate these phases. 19 | 20 | Some do so through inheriting from a base class which contains logic to run descendants methods in specified order 21 | typically referred to as class lifecycle. So the request handling phases are split into methods and the order 22 | of their execution is handled by the base class. This approach is quite common for UI components, but can also be effectively 23 | utilized on backend. 24 | 25 | Another approach to splitting request handling phases would be separating them out into folders and have a strict order 26 | in which they can be called. The division can be as follows. Each layer uses only the one directly underneath itself. 27 | 28 | 1. *routes*: definition of API endpoints (each route has it's own controller) 29 | 2. *controllers*: data parsing, data validation, calling operation(s), setting response 30 | 3. *operations*: business logic execution (doesn't know anything about the server request, handles oly the data it needs) 31 | 4. *repositories*: database calls, abstracts from used database and ORM 32 | 33 | ... rest was because of time constraints only presented during 4th lecture and didn't make it here. Most is written 34 | in the slides attached in this folder. 35 | 36 | # Homework 37 | 38 | Homework has two parts this time. First one is strongly recommended to get familiar with how stateless authentication would be done in Node.js. Second part is optional for those who really want to understand the project architecture we discussed. The reason it's optional is mainly because it does take considerable amount of time to complete. 39 | 40 | ## 1. Implement user sign in route 41 | 42 | The main branch of this repository now contains the code we wrote during the lecture (and it will until the start of the next lecture). *Please fork this repository and implement route for users sign in.* The new route should have following signature 43 | 44 | ```json 45 | POST /sessions/user 46 | { 47 | "email": "zaphod@beeblebrox.me", 48 | "password": "Password124!" 49 | } 50 | ``` 51 | 52 | To help you a little with the cryptography part, which we haven't had time to go through properly, I prepared the last crypto function you'll need for this task. Basically because we only store hashes of user passwords, we won't be able to compare the password we get from user on sign in directly to his password in database. As hashing is designed as one way process, we also won't be able to decrypt the hash we have in database. Therefore our only option here will be to hash the new acquired password and compare the hash to the one we have in database. We can use the `bcrypt` to do this, but have to remeber to peperify the plaintext password first, as we did with the stored password. To sum up, it's as easy as 53 | 54 | ```javascript 55 | function comparePasswords(plaintext, ciphertext) { 56 | return bcrypt.compare(pepperify(plaintext), ciphertext) 57 | } 58 | ``` 59 | 60 | The most important part of this task is to abide the responsibility division we discussed in the lecture. Good luck. 61 | 62 | ## 2. Refactor your current dogbook project 63 | 64 | 1. Refactor your current dogbook implementation to match the _routes_, _controllers_, _operations_, _repositories_ layered model. 65 | 2. Also don't forget to implement configuration, error handling and validation. 66 | 3. And if you're feeling lucky, try reimplementing authorization routes and middleware. 67 | 68 | If you're going to try this second optional homework, be prepared it will take you time. Also don't be afraid to heavily inspire yourself by the existing code in this branch. The objective here is to get your hands on how the parts of the system connect one to another, not to reinvent it from the ground up. Therefore even copying parts of files from this repository works fine for this task. Good luck to all you adventurous who will take me up on this challenge. 69 | -------------------------------------------------------------------------------- /lectures/05-database/Node.js_Nights_Databases.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/nodejs-open-knowledge/582e74ca81567dfe45ab8f31a22f2ec605e1caff/lectures/05-database/Node.js_Nights_Databases.pdf -------------------------------------------------------------------------------- /lectures/05-database/README.md: -------------------------------------------------------------------------------- 1 | # Databases 2 | 3 | This lecture was meant to talk about databases and how to setup the database connection to your project. For this lecture purpose, we are using [PostgreSQL](https://www.postgresql.org/download/) - a Relational Database Management System (RDBMS) - and we are using the [Objection.js](http://vincit.github.io/objection.js/) module to query the data and create all table structures. 4 | 5 | In this repository, I tried to separate each step in a different commit so, if you want to check how the project evolves, you can check the sequence of commits on this branch. 6 | 7 | #### Links: 8 | - [Video](https://www.youtube.com/watch?v=rjLyRJT5QCk&list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP&index=5) 9 | - [Slides](./Node.js_Nights_Databases.pdf) 10 | - [Homework](#homework) 11 | 12 | # Homework 13 | 14 | The homework for this week will consist basically into setting a database and using it in your project. 15 | 16 | ## 1. Set a database for your project 17 | 18 | You can download PostgreSQL [here](https://www.postgresql.org/download/) or download and install Docker from [this link](https://store.docker.com/search?type=edition&offering=community) and use a PostgreSQL image like [this one](https://hub.docker.com/r/sameersbn/postgresql/). 19 | After setting up your system and creating the database (if needed), you can use this branch as a reference on how to create a connection to the database. 20 | 21 | ## 2. Setup your ORM 22 | 23 | As you can see in this repository, we are already using [Objection.js](http://vincit.github.io/objection.js/) in this branch. You can use the `src/config/default.js` and `src/databases/index.js` here as an example to setup your connection. 24 | Also, don't forget to follow the Objection.js [installation](http://vincit.github.io/objection.js/#installation) steps to ensure that you have installed all your dependencies. 25 | 26 | ## 3. Create your tables 27 | 28 | In this repository you will find the database structure inside the `src/database/migrations` folder. Those migrations are created using [Knex](https://knexjs.org/) module (this module is a dependency to use Objection.js) and, in a nutshell, they will be translated to SQL commands with the table representation. 29 | To create a migration you need to use the following command: 30 | ```shell 31 | NODE_ENV=local ./node_modules/.bin/knex migrate:make NAME_OF_THE_MIGRATION_FILE --knexfile ./src/database/knexfile.js 32 | ``` 33 | If you don't want to create the migration, you can create the tables directly to the database. You can use the [pgAdmin](https://www.pgadmin.org/download/) tool to query and admin the database and run the following query to create the `users` and `dogs` tables: 34 | ```sql 35 | CREATE TABLE public.users 36 | ( 37 | id SERIAL PRIMARY KEY, 38 | email character varying(255) NOT NULL, 39 | name character varying(255) NOT NULL, 40 | password character varying(255) NOT NULL, 41 | disabled boolean, 42 | created_at timestamp with time zone, 43 | updated_at timestamp with time zone, 44 | CONSTRAINT users_email_unique UNIQUE (email) 45 | ); 46 | 47 | CREATE TABLE public.dogs 48 | ( 49 | id SERIAL PRIMARY KEY, 50 | name character varying(255) NOT NULL, 51 | breed character varying(255) NOT NULL, 52 | birth_year integer NOT NULL, 53 | photo text, 54 | user_id integer, 55 | created_at timestamp with time zone, 56 | updated_at timestamp with time zone, 57 | CONSTRAINT dogs_user_id_foreign FOREIGN KEY (user_id) 58 | REFERENCES public.users (id) MATCH SIMPLE 59 | ON UPDATE NO ACTION 60 | ON DELETE CASCADE 61 | ); 62 | ``` 63 | 64 | ## 3. Update the repository modules 65 | 66 | Now that you have your ORM working in the project and your database and tables were created, you can start to update the code to query the database. Feel free to use this repository as a reference to guide you through your changes. 67 | 68 | ## 4. [OPTIONAL] Setup a different ORM 69 | 70 | This is an optional step, but it will help you get a better understanding of how useful are the abstractions that we did in previous lessons. 71 | I would suggest for you to try to setup and work with [Sequelize](http://docs.sequelizejs.com/) as it's also one of the "big players" when we talk about ORMs for Node.js. 72 | 73 | 74 | # Useful Links 75 | 76 | - PostgreSQL download: [https://www.postgresql.org/download/](https://www.postgresql.org/download/) 77 | - Docker download: [https://store.docker.com/search?type=edition&offering=community](https://store.docker.com/search?type=edition&offering=community) 78 | - PostgreSQL Docker image: [https://hub.docker.com/r/sameersbn/postgresql/](https://hub.docker.com/r/sameersbn/postgresql/) 79 | - pgAdmin download: [https://www.pgadmin.org/download/](https://www.pgadmin.org/download/) 80 | - Objection.js documentation: [http://vincit.github.io/objection.js/](http://vincit.github.io/objection.js/) 81 | - Knex Migrations documentation: [https://knexjs.org/#Migrations](https://knexjs.org/#Migrations) 82 | -------------------------------------------------------------------------------- /lectures/06-testing/Node.js_Nights_Testing.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/nodejs-open-knowledge/582e74ca81567dfe45ab8f31a22f2ec605e1caff/lectures/06-testing/Node.js_Nights_Testing.pdf -------------------------------------------------------------------------------- /lectures/06-testing/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Testing is absolutely crucial for any serious software development. Even the simplest projects, in order to work properly, 4 | need to be tested thoroughly. Otherwise such a projects will be riddled with bugs that you won't be even aware of (no kidding). 5 | 6 | You can try to test your software by manually running it and observing how it behaves. However, you won't get far with 7 | this approach. As your project will grow, manual testing quickly becomes unmanageable. 8 | 9 | 10 | #### Links: 11 | - [Video](https://www.youtube.com/watch?v=Aykxg9loDjE&list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP&index=6) 12 | - [Slides](./Node.js_Nights_Testing.pdf) 13 | - [Homework](#homework) 14 | 15 | ## Testing Frameworks 16 | Luckily there are tools to help you with automatization of your tests. Currently there are two dominant test frameworks 17 | to choose from: 18 | 19 | - [Mocha](https://mochajs.org/) - Battle-tested framework. Works best with [Chai](https://www.chaijs.com/). 20 | - [Jest](https://jestjs.io/) - New framework from Facebook. Has some interesting features and also provides 21 | mocking and coverage so you don't need to install any additional dependencies. 22 | 23 | 24 | ## Tests 25 | 26 | In API we use mostly two kinds of tests: 27 | 28 | - **Unit tests** - to verify functionality of individual isolated components 29 | - **Integration tests** - to verify how these components works together. Most notably, we are testing whole endpoints. 30 | 31 | There's also **End-to-end** testing, but in terms of API we are not using them 32 | 33 | ## Coverage 34 | 35 | Coverage is telling us how much of our codebase we are actually testing. This is very important metric as 36 | without it we can never be sure if our tests are actually covering enough of our codebase. 37 | 38 | If you are using Mocha as your testing framework, try to add [nyc](https://www.npmjs.com/package/nyc) to your 39 | project. Jest users doesn't need to install anything, just enable coverage generation thorough CLI param 40 | or in options. 41 | 42 | ## Mocking 43 | 44 | Sooner or later you'll run into a problem that using some actual services during tests are very impractical. 45 | For example when you'll be testing your payment endpoints you don't want to actually make any payments. In 46 | fact you don't want to make any network requests to 3rd party services. 47 | 48 | To solve that situation, you can use imitation of some part of the code that fakes the response. This allows 49 | you to test actual feature you want to test in isolation from other parts that you are don't want to test. 50 | 51 | If you are using Mocha, you can try [Sinon](https://sinonjs.org/).Again, Jest users can use jests built-in 52 | mocking features. 53 | 54 | 55 | # Homework 56 | 57 | ## 1. Improve coverage 58 | Improve coverage of file `src/operations/users.js` so that only one 59 | line (doesn't matter which one) is not covered (coverage 97.14%, 34 out of 35 lines covered). 60 | 61 | To be able to do that, you're gonna need to write several more tests. Study tests in `tests/integration/users/create.spec.js` as well as in 62 | `tests/integration/dogs/create.spec.js`. This should get you enough 63 | information to carry out this task easily. 64 | 65 | If that last uncovered line bugs you and you would like to score extra credit, try to improve coverage as much as reaching 66 | 100%. Hint: To do so, you'll need to find and fix a bug that creeped 67 | into our code during one of the previous lessons. Bug should get revealed by a test you'll write. 68 | 69 | ## 2. Optional 70 | 71 | Look at Jest library and try to reimplement at least some of the tests using this framework instead of Mocha. 72 | -------------------------------------------------------------------------------- /lectures/07-deployment/README.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | - [Presentation](https://docs.google.com/presentation/d/13fmv92YSw5MTSJXsqd7SIMBV5juefxKfO0Rzh4qfaY8/edit?usp=sharing) 4 | - [Video](https://www.youtube.com/watch?v=Jb1ztje-CQE&list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP&index=7) 5 | 6 | # Topics 7 | 8 | ## Software Deployment 9 | 10 | Process that consists in different activities 11 | 12 | - Pull/Push from a version control system. 13 | - Build artifacts, compilation, update dependencies. 14 | - Check health status. If something failed, rollback to previous healthy deployment 15 | - Specify release versions. 16 | 17 | Deployments could have different named environments like: development, pre-production, production 18 | 19 | ## Deployment Strategies 20 | 21 | - On-premise data centers 22 | - You need to take care of all hardware, networking, maintenance. 23 | - Upgrade the obsolete parts. 24 | - There should be a good reason to go this way, eg: Licensing, Credit cards information. 25 | 26 | - Cloud 27 | - Ready to use provisioned hardware/software/configuration. 28 | - Provide different kind of services within different models. 29 | - Different pay models. 30 | - Different providers. 31 | 32 | ## Cloud 33 | 34 | - Infrastructure as a Service (IaaS) 35 | - AWS, Azure, Google Cloud, DigitalOcean 36 | - Platform as a Service (PaaS) 37 | - __Heroku__, AWS Beanstalk, AWS Fargate 38 | - Software as a Service (SaaS) 39 | - Dropbox, Slack, Google Apps 40 | - Serverless 41 | - Function as a Service (FaaS): AWS Lambda, Google Cloud Functions 42 | - Backend as a Service (BaaS): Firebase 43 | 44 | # Heroku 45 | 46 | - On top of AWS 47 | - Architecture 48 | - Dynos: Application containers 49 | - Stacks: Operating system + built dependencies. Always Ubuntu. 50 | - Add-ons: Databases, Logs consumers, Monitoring tools... 51 | - Deployments using Git 52 | 53 | # Continuous Delivery 54 | 55 | - Process that consist on keep your application on a delivery-ready state 56 | - Comes along with Automation strategies/services 57 | - Continuous Integration 58 | - Build/Compilation succeed 59 | - Tests succeed 60 | - Continuous Deployment 61 | - Script/Service that makes deployments 62 | 63 | # Homework 64 | 65 | ## 1. Code coverage 66 | 67 | So far the code coverage is only being generated and that's it. Let's make a better use of them! 68 | This activity consists on attaching the code coverage to the CD pipeline by uploading them to a service like [CodeCov](https://codecov.io/). 69 | 70 | Replace the `after_success` step in the TravisCI config file with the proper command to upload. 71 | 72 | Hint: As you will be uploading data to a remote service there should be a way to authenticate against it (just like Heroku deployments) so make sure you have the `"token"` included in the TravisCI config file __encrypted__ 73 | 74 | ## 2. (Optional) Dockerfile for Development 75 | 76 | Currently we have the `Dockerfile` only for production environments and final releases. To make completely sure that new features we develop will run exactly as in a production environment, we should have a Docker-ized development environment and integrate it to the current `docker-compose.yml` configuration so when we run `npm run infra` we provision not only the database but the API itself, all running inside Docker. 77 | 78 | Hint: Check out [Docker's volumes](https://docs.docker.com/storage/volumes/). This allows us to share and link directories between the Host machine and the Docker container, eg: Link the APP's code to the container so there's no need to re-build the image every time the code changes! -------------------------------------------------------------------------------- /lectures/08-workers-security/Node.js Nights - Security.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/nodejs-open-knowledge/582e74ca81567dfe45ab8f31a22f2ec605e1caff/lectures/08-workers-security/Node.js Nights - Security.pdf -------------------------------------------------------------------------------- /lectures/08-workers-security/Node.js Nights – Workers & Queues.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/nodejs-open-knowledge/582e74ca81567dfe45ab8f31a22f2ec605e1caff/lectures/08-workers-security/Node.js Nights – Workers & Queues.pdf -------------------------------------------------------------------------------- /lectures/08-workers-security/README.md: -------------------------------------------------------------------------------- 1 | # 8. Lecture summary 2 | 3 | ## Links: 4 | - [Video](https://www.youtube.com/watch?v=mgRMdcFBQ9U&list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP&index=8) 5 | - [Slides](https://github.com/strvcom/nodejs-nights-2018/blob/master/lectures/08-workers-security/Node.js%20Nights%20%E2%80%93%20Workers%20%26%20Queues.pdf) 6 | 7 | 8 | ## Workers & Queues 9 | 10 | This lecture covers 11 | - Workers 12 | - Queues 13 | - Background jobs 14 | - Scheduling 15 | 16 | 17 | -------------------------------------------------------------------------------- /lectures/09-graphql/Node.js Nights - GraphQL.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/nodejs-open-knowledge/582e74ca81567dfe45ab8f31a22f2ec605e1caff/lectures/09-graphql/Node.js Nights - GraphQL.pdf -------------------------------------------------------------------------------- /lectures/09-graphql/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 9. GraphQL 3 | 4 | ## Links 5 | 6 | * [Presentation](https://docs.google.com/presentation/d/1H0dKAiMofkCGX01YjEDJ3KdKePbTNjzGKCC1n67qJQc/edit?usp=sharing) 7 | * [Video](https://www.youtube.com/watch?v=RNqreGmK3gs&list=PLfX7tWavkVjBVmmZOU5sWuyutpekJ6KNP&index=9) 8 | 9 | ## Theoretical part 10 | * Motivation 11 | * What's wrong with REST 12 | * GraphQL Language 13 | * Queries 14 | * Arguments 15 | * Variables 16 | * Aliases 17 | * Directives 18 | * Fragments 19 | * Inline Fragments 20 | * Mutations 21 | 22 | ## Practical part 23 | 1. `npm install apollo-server-koa graphql merge-graphql-schemas -S` 24 | 2. Init graphql server 25 | 3. Create typeDefs 26 | 4. Create resolvers 27 | 5. Implement queries 28 | * dogs: [Dog] 29 | * dog(id: Int!): Dog 30 | * login mutation 31 | 6. Authorization & context 32 | 7. Data loader 33 | 8. Engine 34 | ``` 35 | apollo schema:publish --endpoint http://localhost:3001/graphql --key="" 36 | ``` 37 | 38 | ## Other Resources 39 | - [Language](https://graphql.org/learn/queries/) 40 | - [Public GraphQL APIs](https://github.com/APIs-guru/graphql-apis) 41 | - [GraphQL Tools](https://github.com/chentsulin/awesome-graphql) 42 | - [Countries GraphQL](https://countries.trevorblades.com/) 43 | - [GraphQL Voyager](https://apis.guru/graphql-voyager/) 44 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/01-expensive-field.json: -------------------------------------------------------------------------------- 1 | { 2 | "books": [ 3 | { 4 | "id": 1, 5 | "name": "Yellow", 6 | "genre": "Fiction", 7 | "numberOfAwards": 8 8 | } 9 | ] 10 | } 11 | 12 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/02-hello-graphql.gql: -------------------------------------------------------------------------------- 1 | query { 2 | dogs { 3 | id 4 | name 5 | breed 6 | birthYear 7 | photo 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/02-hello-graphql.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "dogs": [ 4 | { 5 | "id": 1, 6 | "name": "Belle", 7 | "breed": "beagle", 8 | "birthYear": 2018, 9 | "photo": "https://images.dog.ceo/n02088364_10731.jpg" 10 | }, 11 | { 12 | "id": 2, 13 | "name": "Dollie", 14 | "breed": "terrier", 15 | "birthYear": 2006, 16 | "photo": "https://images.dog.ceo/n02093754_3442.jpg" 17 | } 18 | ] 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/03-queries.gql: -------------------------------------------------------------------------------- 1 | query getDogs { 2 | dogs { 3 | id 4 | name 5 | breed 6 | birthYear 7 | photo 8 | user { 9 | id 10 | name 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/04-aliases.gql: -------------------------------------------------------------------------------- 1 | query getDogs { 2 | belle: dog(id:1) { 3 | id 4 | name 5 | breed 6 | birthYear 7 | photo 8 | } 9 | dollie: dog(id: 2) { 10 | id 11 | name 12 | breed 13 | birthYear 14 | photo 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/04-aliases.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "belle": { 4 | "id": 1, 5 | "name": "Belle", 6 | "breed": "beagle", 7 | "birthYear": 2018, 8 | "photo": "https://images.dog.ceo/n02088364_10731.jpg" 9 | }, 10 | "dollie": { 11 | "id": 2, 12 | "name": "Dollie", 13 | "breed": "terrier", 14 | "birthYear": 2006, 15 | "photo": "https://images.dog.ceo/n02093754_3442.jpg" 16 | } 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/05-query-variables.gql: -------------------------------------------------------------------------------- 1 | query getDogs($breed: String!) { 2 | dogs(filter: { breed: $breed }) { 3 | id 4 | name 5 | breed 6 | birthYear 7 | photo 8 | user { 9 | id 10 | name 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/05-query-variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "breed": "husky" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/06-fragments.gql: -------------------------------------------------------------------------------- 1 | query getDogs { 2 | belle: dog(id:1) { 3 | ...DogFields 4 | } 5 | dollie: dog(id: 2) { 6 | ...DogFields 7 | } 8 | } 9 | 10 | fragment DogFields on Dog { 11 | id 12 | name 13 | breed 14 | birthYear 15 | photo 16 | } 17 | 18 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/07-directives-vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "withUser": true 3 | } 4 | 5 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/07-directives.gql: -------------------------------------------------------------------------------- 1 | query getDogs($withUser: Boolean!) { 2 | dogs { 3 | id 4 | name 5 | breed 6 | birthYear 7 | photo 8 | user @include (if: $withUser) { 9 | id 10 | name 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/08-mutation-vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "name": "Dollie", 4 | "breed": "terrier", 5 | "birthYear": 2018 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /lectures/09-graphql/gql-examples/08-mutation.gql: -------------------------------------------------------------------------------- 1 | mutation createDog($input: CreateDogInput!) { 2 | createDog(input: $input) { 3 | id 4 | name 5 | breed 6 | birthYear 7 | photo 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-nights-v2", 3 | "version": "1.0.0", 4 | "description": "Dogbook API", 5 | "engines": { 6 | "node": "~11.0.0", 7 | "npm": "~6.4.1" 8 | }, 9 | "scripts": { 10 | "test": "NODE_ENV=test npx mocha --opts ./tests/mocha.opts ./tests", 11 | "test:coverage": "NODE_ENV=test npx nyc mocha --opts ./tests/mocha.opts ./tests", 12 | "start": "node src/app.js", 13 | "dev": "nodemon --watch src --ext js --exec 'nf start -j Procfile.dev || exit 1'", 14 | "lint": "eslint .", 15 | "snyk": "snyk auth $SNYK_TOKEN && snyk test", 16 | "migrate:make": "knex migrate:make --knexfile ./src/database/knexfile.js", 17 | "db:migrate": "knex migrate:latest --knexfile ./src/database/knexfile.js", 18 | "db:rollback": "knex migrate:rollback --knexfile ./src/database/knexfile.js", 19 | "infra": "docker-compose up -d", 20 | "infra:stop": "docker-compose down" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/strvcom/nodejs-nights-2018.git" 25 | }, 26 | "author": "STRV Backend Crew", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/strvcom/nodejs-nights-2018/issues" 30 | }, 31 | "homepage": "https://github.com/strvcom/nodejs-nights-2018#readme", 32 | "devDependencies": { 33 | "@strv/eslint-config-javascript": "^9.2.0", 34 | "chai": "^4.2.0", 35 | "chance": "^1.0.18", 36 | "eslint": "^5.14.1", 37 | "foreman": "^3.0.1", 38 | "mocha": "^6.0.1", 39 | "nodemon": "^1.18.10", 40 | "nyc": "^13.3.0", 41 | "pino-pretty": "^2.5.0", 42 | "sinon": "^7.2.4", 43 | "snyk": "^1.134.2", 44 | "supertest-koa-agent": "^0.3.2" 45 | }, 46 | "dependencies": { 47 | "apollo-server-koa": "^2.4.6", 48 | "aws-sdk": "^2.409.0", 49 | "bcrypt": "^3.0.4", 50 | "bluebird": "^3.5.3", 51 | "bull": "^3.7.0", 52 | "dataloader": "^1.4.0", 53 | "dotenv": "^6.2.0", 54 | "graphql": "^14.1.1", 55 | "jsonschema": "^1.2.4", 56 | "jsonwebtoken": "^8.5.0", 57 | "kcors": "^2.2.2", 58 | "knex": "^0.16.3", 59 | "koa": "^2.7.0", 60 | "koa-body": "^4.0.8", 61 | "koa-compress": "^3.0.0", 62 | "koa-helmet": "^4.0.0", 63 | "koa-router": "^7.4.0", 64 | "merge-graphql-schemas": "^1.5.8", 65 | "node-fetch": "^2.3.0", 66 | "objection": "^1.6.2", 67 | "pg": "^7.8.1", 68 | "pino": "^5.11.1", 69 | "ramda": "^0.26.1" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scripts/generate-data.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-process-exit */ 2 | 3 | 'use strict' 4 | 5 | const R = require('ramda') 6 | const Chance = require('chance') 7 | const userOperations = require('../src/operations/users') 8 | const dogOperations = require('../src/operations/dogs') 9 | 10 | const chance = new Chance() 11 | 12 | const USER_COUNT = 3 13 | const DOGS_PER_USER_MIN = 3 14 | const DOGS_PER_USER_MAX = 5 15 | 16 | const generateUser = () => { 17 | const userData = { 18 | name: chance.name(), 19 | email: chance.email({ domain: 'example.com' }), 20 | password: chance.word({ length: 8 }), 21 | } 22 | return userOperations.signUp(userData) 23 | } 24 | 25 | const generateDog = userId => { 26 | const dogData = { 27 | name: chance.first(), 28 | breed: chance.pickone(['husky', 'bulldog', 'terrier', 'beagle']), 29 | birthYear: parseInt(chance.year({ min: 2000, max: 2017 })), 30 | userId, 31 | } 32 | return dogOperations.createDog(dogData) 33 | } 34 | 35 | const generate = async () => { 36 | const results = await Promise.all(R.times(async () => { 37 | const numOfDogs = chance.integer({ min: DOGS_PER_USER_MIN, max: DOGS_PER_USER_MAX }) 38 | const user = await generateUser() 39 | const dogs = await Promise.all(R.times(() => generateDog(user.id), numOfDogs)) 40 | return { user, dogs } 41 | }, USER_COUNT)) 42 | return results 43 | } 44 | 45 | generate() 46 | .then(results => { 47 | console.log(results) 48 | process.exit(0) 49 | }) 50 | .catch(console.error) 51 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Koa = require('koa') 4 | const koaBody = require('koa-body') 5 | const koaCompress = require('koa-compress') 6 | const koaHelmet = require('koa-helmet') 7 | const koaCors = require('kcors') 8 | const router = require('./routes') 9 | const config = require('./config') 10 | const log = require('./utils/logger') 11 | const { addGraphQL } = require('./graphql') 12 | 13 | const services = { 14 | server: null, 15 | } 16 | 17 | const app = new Koa() 18 | 19 | app.use(koaHelmet()) 20 | app.use(koaCompress()) 21 | app.use(koaCors()) 22 | app.use(koaBody()) 23 | 24 | app.use(router) 25 | addGraphQL(app) 26 | 27 | // Define start method 28 | app.start = async () => { 29 | log.info('Starting app…') 30 | 31 | // Start any services here: 32 | // e.g. database connection. 33 | 34 | services.server = await new Promise((resolve, reject) => { 35 | const listen = app.listen(config.server.port, err => err ? reject(err) : resolve(listen)) 36 | }) 37 | } 38 | 39 | // Define app shutdown 40 | app.stop = async () => { 41 | log.info('Stopping app…') 42 | 43 | // Stop everything now. 44 | // e.g. close database connection 45 | 46 | await services.server.close() 47 | } 48 | 49 | // Start app 50 | if (require.main === module) { 51 | app.start() 52 | .then(() => log.info(`App is running on port ${config.server.port}`)) 53 | .catch(err => log.error(err)) 54 | } 55 | 56 | process.once('SIGINT', () => app.stop()) 57 | process.once('SIGTERM', () => app.stop()) 58 | 59 | module.exports = app 60 | -------------------------------------------------------------------------------- /src/config/default.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const pkg = require('../../package') 4 | 5 | module.exports = env => ({ 6 | env, 7 | appName: pkg.name, 8 | version: pkg.version, 9 | server: { 10 | port: process.env.PORT || 3001, 11 | bodyParser: { 12 | patchKoa: true, 13 | urlencoded: true, 14 | text: false, 15 | json: true, 16 | multipart: false, 17 | }, 18 | cors: { 19 | origin: '*', 20 | exposeHeaders: [ 21 | 'Authorization', 22 | 'Content-Language', 23 | 'Content-Length', 24 | 'Content-Type', 25 | 'Date', 26 | 'ETag', 27 | ], 28 | maxAge: 3600, 29 | }, 30 | }, 31 | auth: { 32 | secret: process.env.AUTH_SECRET || 'htfq4o3bcyriq4wyvtcbyrwqv3fy53bprogc', 33 | saltRounds: 10, 34 | createOptions: { 35 | expiresIn: 60 * 60, 36 | algorithm: 'HS256', 37 | issuer: `com.strv.nodejs-nights.${env}`, 38 | }, 39 | verifyOptions: { 40 | algorithm: 'HS256', 41 | issuer: `com.strv.nodejs-nights.${env}`, 42 | }, 43 | }, 44 | logger: { 45 | enabled: true, 46 | stdout: true, 47 | minLevel: 'debug', 48 | }, 49 | database: { 50 | client: 'pg', 51 | connection: process.env.DATABASE_URL 52 | || 'postgres://postgres@localhost:5432/nodejs-nights-local', 53 | pool: { 54 | min: process.env.DATABASE_POOL_MIN || 0, 55 | max: process.env.DATABASE_POOL_MAX || 5, 56 | }, 57 | }, 58 | aws: { 59 | s3: { 60 | bucketName: process.env.AWS_S3_BUCKET_NAME, 61 | }, 62 | rekognition: { 63 | minConfidence: 90, 64 | }, 65 | }, 66 | jobs: { 67 | redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379', 68 | }, 69 | apolloEngineApiKey: process.env.APOLLO_ENGINE_API_KEY, 70 | }) 71 | -------------------------------------------------------------------------------- /src/config/env/local.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | hostname: 'http://localhost:3000', 5 | } 6 | -------------------------------------------------------------------------------- /src/config/env/production.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | hostname: 'https://nodejs-nights-2018-app.herokuapp.com', 5 | } 6 | -------------------------------------------------------------------------------- /src/config/env/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | hostname: 'http://localhost:3000', 5 | logger: { 6 | enabled: false, 7 | stdout: true, 8 | minLevel: 'error', 9 | }, 10 | database: { 11 | connection: 'postgres://postgres@localhost:5432/nodejs-nights-test', 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 'use strict' 3 | 4 | const env = process.env.NODE_ENV || 'local' 5 | 6 | // Load process.env variables from .env file (when developing locally) 7 | // !! Do not move these lines, config variables have to be loaded before default config is loaded. 8 | if (env === 'local') { 9 | require('dotenv').config({ silent: false }) 10 | } 11 | 12 | const R = require('ramda') 13 | 14 | // We need dynamic requires here to ensure that .env is loaded beforehand 15 | const envConfigPath = `./env/${env}` 16 | const envConfig = require(envConfigPath) 17 | const defaultConfig = require('./default')(env) 18 | 19 | // Override default values with values from environment config 20 | const resultConfig = R.mergeDeepRight(defaultConfig, envConfig) 21 | 22 | module.exports = resultConfig 23 | -------------------------------------------------------------------------------- /src/controllers/dogs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { validate } = require('./../validations') 4 | const operations = require('./../operations/dogs') 5 | const schemas = require('./../validations/schemas/dogs') 6 | 7 | async function getAll(ctx) { 8 | ctx.body = await operations.getAll() 9 | } 10 | 11 | async function getById(ctx) { 12 | const input = { 13 | id: parseInt(ctx.params.id), 14 | } 15 | validate(schemas.dogId, input) 16 | ctx.body = await operations.getById(input) 17 | } 18 | 19 | async function createDog(ctx) { 20 | const input = { 21 | name: ctx.request.body.name, 22 | breed: ctx.request.body.breed, 23 | birthYear: parseInt(ctx.request.body.birthYear), 24 | photo: ctx.request.body.photo, 25 | } 26 | validate(schemas.dog, input) 27 | const response = await operations.createDog(input) 28 | ctx.status = 201 29 | ctx.body = response 30 | } 31 | 32 | module.exports = { 33 | getAll, 34 | getById, 35 | createDog, 36 | } 37 | -------------------------------------------------------------------------------- /src/controllers/users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { validate } = require('../validations') 4 | const operations = require('../operations/users') 5 | const schema = require('../validations/schemas/users') 6 | 7 | async function login(ctx) { 8 | const input = { 9 | email: ctx.request.body.email, 10 | password: ctx.request.body.password, 11 | } 12 | validate(schema.login, input) 13 | ctx.body = await operations.login(input) 14 | } 15 | 16 | async function signUp(ctx) { 17 | const input = { 18 | name: ctx.request.body.name, 19 | email: ctx.request.body.email, 20 | password: ctx.request.body.password, 21 | } 22 | validate(schema.signUp, input) 23 | const user = await operations.signUp(input) 24 | ctx.status = 201 25 | ctx.body = user 26 | } 27 | 28 | module.exports = { 29 | login, 30 | signUp, 31 | } 32 | -------------------------------------------------------------------------------- /src/database/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const objection = require('objection') 4 | // -- Knex/PG issue: https://github.com/tgriesser/knex/issues/927 5 | const pg = require('pg') 6 | 7 | pg.types.setTypeParser(20, 'text', parseInt) 8 | pg.types.setTypeParser(1700, 'text', parseFloat) 9 | // -- end -- 10 | const knexLib = require('knex') 11 | const R = require('ramda') 12 | const config = require('../config') 13 | const knexEnvConfig = require('./knexfile')[config.env] 14 | 15 | const knexConfig = R.mergeDeepWith({}, knexEnvConfig, objection.knexSnakeCaseMappers()) 16 | const knex = knexLib(knexConfig) 17 | 18 | const Model = objection.Model 19 | Model.knex(knex) 20 | const transaction = objection.transaction 21 | 22 | function connect() { 23 | // Knex does not have an explicit `.connect()` method so we issue a query and consider the 24 | // connection to be open once we get the response back. 25 | return knex.raw('select 1 + 1') 26 | } 27 | 28 | function close() { 29 | return knex.destroy() 30 | } 31 | 32 | module.exports = { 33 | Model, 34 | knex, 35 | transaction, 36 | connect, 37 | close, 38 | } 39 | -------------------------------------------------------------------------------- /src/database/knexfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const R = require('ramda') 4 | const config = require('../config') 5 | 6 | const staticDatabaseConfig = { 7 | migrations: { 8 | directory: './migrations', 9 | }, 10 | seeds: { 11 | directory: './seeds', 12 | }, 13 | } 14 | 15 | const databaseConfig = R.mergeDeepLeft(config.database, staticDatabaseConfig) 16 | 17 | module.exports = { 18 | [config.env]: databaseConfig, 19 | } 20 | -------------------------------------------------------------------------------- /src/database/migrations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/nodejs-open-knowledge/582e74ca81567dfe45ab8f31a22f2ec605e1caff/src/database/migrations/.keep -------------------------------------------------------------------------------- /src/database/migrations/20181007200209_create_users_table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: knex => knex.schema.createTable('users', table => { 5 | table.increments('id').primary() 6 | table.string('email').unique().notNullable() 7 | table.string('name').notNullable() 8 | table.string('password').notNullable() 9 | table.boolean('disabled') 10 | table.timestamps() 11 | }), 12 | 13 | down: knex => knex.schema.dropTableIfExists('users'), 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/database/migrations/20181007214622_create_dogs_tables.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: knex => knex.schema.createTable('dogs', table => { 5 | table.increments('id').primary() 6 | table.string('name').notNullable() 7 | table.string('breed').notNullable() 8 | table.integer('birth_year').notNullable() 9 | table.text('photo') 10 | table.integer('user_id') 11 | table.foreign('user_id').references('users.id').onDelete('CASCADE') 12 | table.timestamps() 13 | }), 14 | 15 | down: knex => knex.schema.dropTableIfExists('dogs'), 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/database/migrations/20181119155602_add_verified_image_to_dogs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: knex => knex.schema.alterTable('dogs', table => { 5 | table.boolean('photo_verified').notNull().default(false) 6 | }), 7 | 8 | down: knex => knex.schema.alterTable('dogs', table => { 9 | table.dropColumn('photo_verified') 10 | }), 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/database/models/base.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const database = require('../index') 4 | 5 | class Base extends database.Model { 6 | $beforeInsert() { 7 | this.updatedAt = this.createdAt = this.createdAt || new Date() 8 | } 9 | 10 | $beforeUpdate() { 11 | this.updatedAt = new Date() 12 | } 13 | } 14 | 15 | module.exports = Base 16 | -------------------------------------------------------------------------------- /src/database/models/dog.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Base = require('./base') 4 | 5 | class Dog extends Base { 6 | static get tableName() { 7 | return 'dogs' 8 | } 9 | 10 | static get jsonSchema() { 11 | return { 12 | type: 'object', 13 | required: [ 14 | 'name', 15 | 'breed', 16 | 'birthYear', 17 | ], 18 | 19 | properties: { 20 | id: { 21 | type: 'integer', 22 | }, 23 | name: { 24 | type: 'string', 25 | }, 26 | breed: { 27 | type: 'string', 28 | }, 29 | birthYear: { 30 | type: 'integer', 31 | }, 32 | photo: { 33 | type: 'string', 34 | }, 35 | userId: { 36 | type: 'integer', 37 | }, 38 | }, 39 | } 40 | } 41 | } 42 | 43 | module.exports = Dog 44 | -------------------------------------------------------------------------------- /src/database/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const User = require('./user') 4 | const Dog = require('./dog') 5 | 6 | module.exports = { 7 | User, 8 | Dog, 9 | } 10 | -------------------------------------------------------------------------------- /src/database/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const R = require('ramda') 4 | const Base = require('./base') 5 | 6 | class User extends Base { 7 | static get tableName() { 8 | return 'users' 9 | } 10 | 11 | static get jsonSchema() { 12 | return { 13 | type: 'object', 14 | required: [ 15 | 'email', 16 | 'name', 17 | 'password', 18 | ], 19 | 20 | properties: { 21 | id: { 22 | type: 'integer', 23 | }, 24 | email: { 25 | type: 'string', 26 | }, 27 | name: { 28 | type: 'string', 29 | }, 30 | password: { 31 | type: 'string', 32 | }, 33 | disabled: { 34 | type: 'boolean', 35 | }, 36 | }, 37 | } 38 | } 39 | 40 | toJSON() { 41 | return R.omit([ 42 | 'password', 43 | 'updatedAt', 44 | ], super.toJSON()) 45 | } 46 | } 47 | 48 | module.exports = User 49 | -------------------------------------------------------------------------------- /src/database/seeds/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strvcom/nodejs-open-knowledge/582e74ca81567dfe45ab8f31a22f2ec605e1caff/src/database/seeds/.keep -------------------------------------------------------------------------------- /src/graphql/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ApolloServer } = require('apollo-server-koa') 4 | const config = require('../config') 5 | const { getAuthPayload } = require('../middleware/authentication') 6 | const errors = require('../utils/errors') 7 | const logger = require('../utils/logger') 8 | const { typeDefs, resolvers } = require('./schema') 9 | const UserDataLoader = require('./loaders/users') 10 | 11 | const defaultPlaygroundConfig = { 12 | settings: { 13 | 'editor.theme': 'light', 14 | 'editor.cursorShape': 'line', 15 | 'editor.fontSize': 16, 16 | }, 17 | } 18 | 19 | function formatError(err) { 20 | const isDevelopment = ['local', 'test', 'development'].includes(config.env) 21 | 22 | logger.error(err) 23 | return { 24 | name: err.name, 25 | stacktrace: isDevelopment && err.extensions.exception.stacktrace, 26 | } 27 | } 28 | 29 | async function makeContext(app) { 30 | // Load authorized user from the authorization header 31 | const data = await getAuthPayload(app.ctx.header.authorization) 32 | 33 | // Create context - can be used in resolvers 34 | return { 35 | user: data && data.user, 36 | requireAuth: () => { 37 | if (!data) { 38 | throw new errors.UnauthorizedError() 39 | } 40 | }, 41 | loaders: { 42 | users: new UserDataLoader(), 43 | }, 44 | } 45 | } 46 | 47 | function addGraphQL(app) { 48 | const enableIntrospection = config.env !== 'production' 49 | const engineConfig = config.apolloEngineApiKey 50 | ? { apiKey: config.apolloEngineApiKey } 51 | : false 52 | 53 | // Create Apollo server 54 | const server = new ApolloServer({ 55 | typeDefs, 56 | resolvers, 57 | debug: enableIntrospection, 58 | introspection: enableIntrospection, 59 | playground: enableIntrospection ? defaultPlaygroundConfig : false, 60 | context: makeContext, 61 | formatError, 62 | engine: engineConfig, 63 | }) 64 | 65 | // Apply Apollo middleware 66 | server.applyMiddleware({ 67 | app, 68 | path: '/graphql', 69 | }) 70 | } 71 | 72 | module.exports = { 73 | addGraphQL, 74 | } 75 | -------------------------------------------------------------------------------- /src/graphql/loaders/users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const DataLoader = require('dataloader') 4 | const R = require('ramda') 5 | const operations = require('../../operations/users') 6 | 7 | class UserDataLoader extends DataLoader { 8 | constructor() { 9 | super(async userIds => { 10 | const users = await operations.getByIds(userIds) 11 | const userIndex = R.indexBy(user => user.id, users) 12 | return userIds.map(userId => userIndex[userId]) 13 | }) 14 | } 15 | } 16 | 17 | module.exports = UserDataLoader 18 | -------------------------------------------------------------------------------- /src/graphql/resolvers/dogs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const operations = require('../../operations/dogs') 4 | // const userOperations = require('../../operations/users') 5 | 6 | module.exports = { 7 | Query: { 8 | dog: (root, args, ctx) => { 9 | ctx.requireAuth() 10 | return operations.getById({ id: args.id }) 11 | }, 12 | dogs: (root, args, ctx) => { 13 | ctx.requireAuth() 14 | return operations.getAll() 15 | }, 16 | }, 17 | Dog: { 18 | // Computed field 19 | age: dog => dog.age || new Date().getFullYear() - dog.birthYear, 20 | 21 | // Issue: N+1 query 22 | // user: dog => dog.userId ? userOperations.getById(dog.userId) : null, 23 | 24 | // Issue: solution 25 | user: (dog, args, ctx) => dog.userId 26 | ? ctx.loaders.users.load(dog.userId) 27 | : null, 28 | 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /src/graphql/resolvers/users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const operations = require('../../operations/users') 4 | 5 | module.exports = { 6 | Mutation: { 7 | login: (root, args) => operations.login(args.input), 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /src/graphql/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { fileLoader, mergeResolvers, mergeTypes } = require('merge-graphql-schemas') 5 | 6 | // Load type definitions 7 | // all:true = merge types with the same name 8 | const typeDefsFiles = path.join(__dirname, './types/*.gql') 9 | const typeDefs = mergeTypes(fileLoader(typeDefsFiles), { all: true }) 10 | 11 | // Load resolvers 12 | const resolverFiles = path.join(__dirname, './resolvers/*.js') 13 | const resolvers = mergeResolvers(fileLoader(resolverFiles)) 14 | 15 | module.exports = { 16 | typeDefs, 17 | resolvers, 18 | } 19 | -------------------------------------------------------------------------------- /src/graphql/types/dogs.gql: -------------------------------------------------------------------------------- 1 | type User { 2 | id: Int! 3 | name: String! 4 | } 5 | 6 | type Dog { 7 | id: Int! 8 | name: String! 9 | breed: String! 10 | birthYear: Int! 11 | photo: String 12 | age: Int! 13 | user: User 14 | } 15 | 16 | type Query { 17 | dog(id: Int!): Dog 18 | dogs: [Dog] 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/graphql/types/users.gql: -------------------------------------------------------------------------------- 1 | 2 | input LoginInput { 3 | email: String! 4 | password: String! 5 | } 6 | 7 | type LoginPayload { 8 | id: Int! 9 | email: String! 10 | accessToken: String! 11 | } 12 | 13 | type Mutation { 14 | login(input: LoginInput!): LoginPayload! 15 | } 16 | -------------------------------------------------------------------------------- /src/jobs/verification.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Bull = require('bull') 4 | const config = require('../config') 5 | const log = require('../utils/logger') 6 | const rekognition = require('../services/rekognition') 7 | const operations = require('../operations/dogs') 8 | 9 | const queue = new Bull('dog verification', { redis: config.jobs.redisUrl }) 10 | 11 | const execute = () => { 12 | log.info('Verification job started') 13 | 14 | queue.process(async job => { 15 | const { id, url } = job.data 16 | 17 | const dog = await operations.updateDog(id, { 18 | photoVerified: await rekognition.isDogRecognized(url), 19 | }) 20 | 21 | log.info({ dog }) 22 | }) 23 | } 24 | 25 | const add = (id, url) => { 26 | queue.add({ id, url }) 27 | } 28 | 29 | module.exports = { 30 | execute, 31 | add, 32 | } 33 | -------------------------------------------------------------------------------- /src/middleware/authentication.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const operations = require('../operations/users') 4 | const { validate } = require('../validations') 5 | const schemas = require('../validations/schemas/users') 6 | const errors = require('../utils/errors') 7 | 8 | async function getAuthPayload(authorization) { 9 | const parsedHeader = parseHeader(authorization) 10 | if (!parsedHeader 11 | || !parsedHeader.value 12 | || !parsedHeader.scheme 13 | || parsedHeader.scheme.toLowerCase() !== 'jwt' 14 | ) { 15 | return null 16 | } 17 | const input = { jwtToken: parsedHeader.value } 18 | validate(schemas.jwtToken, input) 19 | const data = await operations.verifyTokenPayload(input) 20 | return data 21 | } 22 | 23 | async function authenticate(ctx, next) { 24 | if (!ctx) { 25 | throw new Error('Context has to be defined') 26 | } 27 | 28 | const data = await getAuthPayload(ctx.header.authorization) 29 | if (!data) { 30 | throw new errors.UnauthorizedError() 31 | } 32 | 33 | if (ctx.response && data.loginTimeout) { 34 | ctx.set('Login-timeout', data.loginTimeout) 35 | } 36 | ctx.state.user = data.user 37 | return next() 38 | } 39 | 40 | function parseHeader(hdrValue) { 41 | if (!hdrValue || typeof hdrValue !== 'string') { 42 | return null 43 | } 44 | const matches = hdrValue.match(/(\S+)\s+(\S+)/u) 45 | return matches && { 46 | scheme: matches[1], 47 | value: matches[2], 48 | } 49 | } 50 | 51 | module.exports = { 52 | getAuthPayload, 53 | authenticate, 54 | } 55 | -------------------------------------------------------------------------------- /src/middleware/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('../config') 4 | const appErrors = require('../utils/errors') 5 | const logger = require('../utils/logger') 6 | 7 | async function handleErrors(ctx, next) { 8 | try { 9 | return await next() 10 | } catch (err) { 11 | let responseError = err 12 | if (!(err instanceof appErrors.AppError)) { 13 | // This should never happen, log appropriately 14 | logger.error(err) 15 | responseError = new appErrors.InternalServerError() 16 | } 17 | // Prepare error response 18 | const isDevelopment = ['local', 'test', 'development'].includes(config.env) 19 | ctx.status = responseError.status 20 | ctx.body = { 21 | type: responseError.type, 22 | message: responseError.message, 23 | stack: isDevelopment && responseError.stack, 24 | } 25 | return true 26 | } 27 | } 28 | 29 | function handleNotFound() { 30 | throw new appErrors.NotFoundError() 31 | } 32 | 33 | module.exports = { 34 | handleErrors, 35 | handleNotFound, 36 | } 37 | -------------------------------------------------------------------------------- /src/operations/dogs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const dogApi = require('../services/dogapi') 4 | const errors = require('../utils/errors') 5 | const verificationJob = require('../jobs/verification') 6 | const dogRepository = require('./../repositories/dogs') 7 | 8 | async function needDog(id) { 9 | const dog = await dogRepository.findById(id) 10 | if (!dog) { 11 | throw new errors.NotFoundError() 12 | } 13 | 14 | return dog 15 | } 16 | 17 | function getAll() { 18 | return dogRepository.findAll() 19 | } 20 | 21 | function getById(input) { 22 | return needDog(input.id) 23 | } 24 | 25 | async function createDog(input) { 26 | if (!input.photo) { 27 | input.photo = await dogApi.getRandomBreedImage(input.breed) 28 | } 29 | 30 | // For the sake of simplicity, we are not checking if photo is still null at this point. 31 | const dog = await dogRepository.create(input) 32 | 33 | verificationJob.add(dog.id, input.photo) 34 | 35 | return dog 36 | } 37 | 38 | async function updateDog(id, input) { 39 | const dog = await needDog(id) 40 | 41 | return dogRepository.patchById(dog.id, input) 42 | } 43 | 44 | module.exports = { 45 | getAll, 46 | getById, 47 | createDog, 48 | updateDog, 49 | } 50 | -------------------------------------------------------------------------------- /src/operations/users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const userRepository = require('../repositories/users') 4 | const errors = require('./../utils/errors') 5 | const logger = require('./../utils/logger') 6 | const crypto = require('./../utils/crypto') 7 | 8 | async function login(input) { 9 | logger.info({ input }, 'login start') 10 | const user = await userRepository.findByEmail(input.email.toLowerCase()) 11 | if (!user) { 12 | throw new errors.UnauthorizedError() 13 | } 14 | const verified = await crypto.comparePasswords(input.password, user.password) 15 | if (!verified || user.disabled) { 16 | throw new errors.UnauthorizedError() 17 | } 18 | const accessToken = await crypto.generateAccessToken(user.id) 19 | logger.info('login end') 20 | return { 21 | id: user.id, 22 | email: user.email, 23 | accessToken, 24 | } 25 | } 26 | 27 | async function signUp(input) { 28 | logger.info({ input }, 'signUp start') 29 | const user = { 30 | name: input.name, 31 | email: input.email.toLowerCase(), 32 | password: await crypto.hashPassword(input.password), 33 | disabled: false, 34 | } 35 | 36 | const existingUser = await userRepository.findByEmail(user.email) 37 | 38 | if (existingUser) { 39 | throw new errors.ConflictError('User already exists.') 40 | } 41 | const createdUser = await userRepository.create(user) 42 | createdUser.accessToken = await crypto.generateAccessToken(createdUser.id) 43 | logger.info('signUp end') 44 | return createdUser 45 | } 46 | 47 | async function verifyTokenPayload(input) { 48 | logger.info({ input }, 'verifyTokenPayload') 49 | const jwtPayload = await crypto.verifyAccessToken(input.jwtToken) 50 | const now = Date.now() 51 | if (!jwtPayload || !jwtPayload.exp || now >= jwtPayload.exp * 1000) { 52 | throw new errors.UnauthorizedError() 53 | } 54 | 55 | const userId = parseInt(jwtPayload.userId) 56 | const user = userRepository.findById(userId) 57 | if (!user || user.disabled) { 58 | throw new errors.UnauthorizedError() 59 | } 60 | logger.info('verifyTokenPayload') 61 | return { 62 | user, 63 | loginTimeout: jwtPayload.exp * 1000, 64 | } 65 | } 66 | 67 | async function getById(id) { 68 | const user = await userRepository.findById(id) 69 | if (!user) { 70 | throw new errors.NotFoundError() 71 | } 72 | 73 | return user 74 | } 75 | 76 | async function getByIds(ids) { 77 | const users = await userRepository.findByIds(ids) 78 | return users 79 | } 80 | 81 | module.exports = { 82 | login, 83 | signUp, 84 | verifyTokenPayload, 85 | getById, 86 | getByIds, 87 | } 88 | -------------------------------------------------------------------------------- /src/repositories/dogs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Dog } = require('../database/models') 4 | 5 | /** 6 | * Returns all records 7 | * @returns {Promisse} 8 | */ 9 | function findAll() { 10 | return Dog.query() 11 | } 12 | 13 | /** 14 | * Return record by id 15 | * @param {Number} id record id 16 | * @return {Promise} 17 | */ 18 | function findById(id) { 19 | return Dog.query() 20 | .findById(id) 21 | } 22 | 23 | /** 24 | * Create record 25 | * @param {Object} attributes Dog object 26 | * @param {String} attributes.name Dog name 27 | * @param {String} attributes.breed Dog breed 28 | * @param {Date} attributes.birthYear Dog birth year 29 | * @param {String} attributes.photo Dog photo 30 | * @param {Number} attributes.userId Dog owner id 31 | * @return {Promise} 32 | */ 33 | async function create(attributes) { 34 | const dog = await Dog.query() 35 | .insertAndFetch(attributes) 36 | 37 | return dog 38 | } 39 | 40 | /** 41 | * Create record 42 | * @param {Number} id Dog id 43 | * @param {Object} attributes Dog object 44 | * @param {String} attributes.name Dog name 45 | * @param {String} attributes.breed Dog breed 46 | * @param {Date} attributes.birthYear Dog birth year 47 | * @param {String} attributes.photo Dog photo 48 | * @param {Number} attributes.userId Dog owner id 49 | * @param {Boolean} attributes.photoVerified Dog photo contains dog 50 | * 51 | * @return {Promise} 52 | */ 53 | function patchById(id, attributes) { 54 | return Dog.query() 55 | .patchAndFetchById(id, attributes) 56 | } 57 | 58 | module.exports = { 59 | findAll, 60 | findById, 61 | create, 62 | patchById, 63 | } 64 | -------------------------------------------------------------------------------- /src/repositories/users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { User } = require('../database/models') 4 | 5 | /** 6 | * Returns all records 7 | * @return {Promise} 8 | */ 9 | function findAll() { 10 | return User.query() 11 | } 12 | 13 | /** 14 | * Find user by id 15 | * @param {Number} id User id 16 | * @return {Promise} 17 | */ 18 | function findById(id) { 19 | return User.query() 20 | .findById(id) 21 | } 22 | 23 | /** 24 | * Find users by ids provided 25 | * @param {Number[]} ids User ids 26 | * @return {Promise} 27 | */ 28 | function findByIds(ids) { 29 | return User.query() 30 | .where('id', 'in', ids) 31 | } 32 | 33 | 34 | /** 35 | * Find user by email 36 | * @param {String} email User email 37 | * @return {Promise} 38 | */ 39 | function findByEmail(email) { 40 | return User.query() 41 | .where('email', email) 42 | .first() 43 | } 44 | 45 | function patchById(id, data) { 46 | return User.query().patch(data).where({ id }) 47 | } 48 | 49 | /** 50 | * Create a user 51 | * @param {Object} attributes User attributes 52 | * @param {String} attributes.email User email 53 | * @param {String} attributes.name User name 54 | * @param {String} attributes.password User password 55 | * @param {boolean} attributes.disabled User disabled flag 56 | * @return {Promise} 57 | */ 58 | async function create(attributes) { 59 | const user = await User.query() 60 | .insertAndFetch(attributes) 61 | 62 | return user 63 | } 64 | 65 | module.exports = { 66 | patchById, 67 | findAll, 68 | findById, 69 | findByIds, 70 | findByEmail, 71 | create, 72 | } 73 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('koa-router') 4 | const { handleErrors, handleNotFound } = require('../middleware/errors') 5 | const { authenticate } = require('../middleware/authentication') 6 | const dogs = require('../controllers/dogs') 7 | const users = require('../controllers/users') 8 | 9 | const router = new Router() 10 | router.use(handleErrors) 11 | 12 | router.post('/session/user', users.login) 13 | router.post('/users', users.signUp) 14 | 15 | router.get('/dogs', authenticate, dogs.getAll) 16 | router.get('/dogs/:id', authenticate, dogs.getById) 17 | router.post('/dogs', authenticate, dogs.createDog) 18 | 19 | router.use(handleNotFound) 20 | 21 | module.exports = router.routes() 22 | -------------------------------------------------------------------------------- /src/services/dogapi.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fetch = require('node-fetch') 4 | 5 | module.exports = { 6 | getRandomBreedImage: async breed => { 7 | const response = await fetch(`https://dog.ceo/api/breed/${breed.toLowerCase()}/images/random`) 8 | const json = await response.json() 9 | if (json.status === 'error') { 10 | return null 11 | } 12 | return json.message 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/services/rekognition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const url = require('url') 4 | const AWS = require('aws-sdk') 5 | const Bluebird = require('bluebird') 6 | const R = require('ramda') 7 | const awsConfig = require('../config').aws 8 | 9 | const rekognition = new AWS.Rekognition() 10 | const detectLabels = Bluebird.promisify(rekognition.detectLabels, { context: rekognition }) 11 | 12 | const pathFromUrl = photoUrl => url.parse(photoUrl).pathname 13 | const getS3KeyFromPathname = pathName => pathName.replace(/^\/|\/$/gu, '') 14 | 15 | const PROP_NAME = 'Name' 16 | const PROP_VALUE = 'Dog' 17 | 18 | const getLabels = photoUrl => { 19 | const params = { 20 | Image: { 21 | S3Object: { 22 | Bucket: awsConfig.s3.bucketName, 23 | Name: getS3KeyFromPathname(pathFromUrl(photoUrl)), 24 | }, 25 | }, 26 | } 27 | 28 | return detectLabels(params) 29 | } 30 | 31 | module.exports = { 32 | isDogRecognized: async photoUrl => { 33 | const labelsResponse = await getLabels(photoUrl) 34 | const dogLabel = R.find(R.propEq(PROP_NAME, PROP_VALUE))(labelsResponse.Labels) 35 | 36 | return Boolean(dogLabel && dogLabel.Confidence > awsConfig.rekognition.minConfidence) 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/crypto.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const util = require('util') 4 | const crypto = require('crypto') 5 | const bcrypt = require('bcrypt') 6 | const jwt = require('jsonwebtoken') 7 | const config = require('../config') 8 | 9 | const jwtSign = util.promisify(jwt.sign) 10 | const jwtVerify = util.promisify(jwt.verify) 11 | 12 | module.exports = { 13 | generateAccessToken(userId) { 14 | const payload = { userId } 15 | return jwtSign(payload, config.auth.secret, config.auth.createOptions) 16 | }, 17 | 18 | async verifyAccessToken(accessToken) { 19 | try { 20 | // Don't return directly for catch block to work properly 21 | const data = await jwtVerify(accessToken, config.auth.secret, config.auth.verifyOptions) 22 | return data 23 | } catch (err) { 24 | if (err instanceof jwt.JsonWebTokenError || err instanceof SyntaxError) { 25 | return null 26 | } 27 | throw err 28 | } 29 | }, 30 | 31 | hashPassword(password) { 32 | return bcrypt.hash(peperify(password), config.auth.saltRounds) 33 | }, 34 | 35 | comparePasswords(plaintext, ciphertext) { 36 | return bcrypt.compare(peperify(plaintext), ciphertext) 37 | }, 38 | } 39 | 40 | function peperify(password) { 41 | return crypto.createHmac('sha1', config.auth.secret) 42 | .update(password) 43 | .digest('hex') 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/errors.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file, max-len */ 2 | 3 | 'use strict' 4 | 5 | const logger = require('./logger') 6 | 7 | class AppError extends Error { 8 | constructor(message, type, status) { 9 | super() 10 | Error.captureStackTrace(this, this.constructor) 11 | this.name = this.constructor.name 12 | this.type = type 13 | this.message = message 14 | this.status = status 15 | const stack = this.stack ? this.stack.split('\n') : this.stack 16 | logger.error({ 17 | error: { 18 | name: this.name, 19 | message: this.message, 20 | type, 21 | stack: stack && stack.length > 2 ? `${stack[0]} ${stack[1]}` : stack, 22 | }, 23 | }) 24 | } 25 | } 26 | 27 | /** 28 | * @apiDefine ValidationError 29 | * @apiError BadRequest The input request data are invalid. 30 | * @apiErrorExample {json} BadRequest 31 | * HTTP/1.1 400 BadRequest 32 | * { 33 | * "type": "BAD_REQUEST", 34 | * "message": "Invalid or missing request data." 35 | * } 36 | */ 37 | class ValidationError extends AppError { 38 | constructor(message, errors) { 39 | super(message || 'Invalid or missing request data.', 'BAD_REQUEST', 400) 40 | this.errors = errors 41 | } 42 | } 43 | 44 | /** 45 | * @apiDefine NotFoundError 46 | * @apiError NotFound Requested resource not found. 47 | * @apiErrorExample {json} NotFound 48 | * HTTP/1.1 404 NotFound 49 | * { 50 | * "type": "NOT_FOUND", 51 | * "message": "Resource not found." 52 | * } 53 | */ 54 | class NotFoundError extends AppError { 55 | constructor(message) { 56 | super( 57 | message || 'Resource not found.', 58 | 'NOT_FOUND', 59 | 404, 60 | ) 61 | } 62 | } 63 | 64 | /** 65 | * @apiDefine UnauthorizedError 66 | * @apiError Unauthorized Server denied access to requested resource. 67 | * @apiErrorExample {json} Unauthorized 68 | * HTTP/1.1 401 Unauthorized 69 | * { 70 | * "type": "UNAUTHORIZED", 71 | * "message": "Site access denied." 72 | * } 73 | */ 74 | class UnauthorizedError extends AppError { 75 | constructor(message) { 76 | super(message || 'Site access denied.', 'UNAUTHORIZED', 401) 77 | } 78 | } 79 | 80 | /** 81 | * @apiDefine IdleTimeoutError 82 | * @apiError IdleTimeout Server denied access to requested resource. 83 | * @apiErrorExample {json} IdleTimeout 84 | * HTTP/1.1 401 IdleTimeout 85 | * { 86 | * "type": "IDLE_TIMEOUT", 87 | * "message": "Site access denied." 88 | * } 89 | */ 90 | class IdleTimeoutError extends AppError { 91 | constructor(message) { 92 | super(message || 'Site access denied.', 'IDLE_TIMEOUT', 401) 93 | } 94 | } 95 | 96 | /** 97 | * @apiDefine ConflictError 98 | * @apiError Conflict The request could not be completed due to a conflict with the current state of the resource. 99 | * @apiErrorExample {json} Conflict 100 | * HTTP/1.1 409 Conflict 101 | * { 102 | * "type": "CONFLICT", 103 | * "message": "The request could not be completed due to a conflict with the current state of the resource." 104 | * } 105 | */ 106 | class ConflictError extends AppError { 107 | constructor(message) { 108 | super( 109 | message || 'The request could not be completed due to a conflict with the current state of the resource.', 110 | 'CONFLICT', 111 | 409, 112 | ) 113 | } 114 | } 115 | 116 | /** 117 | * @apiDefine InternalServerError 118 | * @apiError (Error 5xx) InternalServerError Something went wrong. 119 | * @apiErrorExample {json} InternalServerError 120 | * HTTP/1.1 500 InternalServerError 121 | * { 122 | * "type": "INTERNAL_SERVER", 123 | * "message": "Something went wrong. Please try again later or contact support." 124 | * } 125 | */ 126 | class InternalServerError extends AppError { 127 | constructor(message) { 128 | super( 129 | message || 'Something went wrong. Please try again later or contact support.', 130 | 'INTERNAL_SERVER', 131 | 500, 132 | ) 133 | } 134 | } 135 | 136 | module.exports = { 137 | AppError, 138 | ValidationError, 139 | NotFoundError, 140 | UnauthorizedError, 141 | IdleTimeoutError, 142 | ConflictError, 143 | InternalServerError, 144 | } 145 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const pino = require('pino') 4 | const app = require('../../package.json') 5 | const config = require('../config') 6 | 7 | module.exports = pino({ 8 | name: app.name, 9 | level: config.logger.minLevel, 10 | enabled: config.logger.enabled, 11 | }) 12 | -------------------------------------------------------------------------------- /src/validations/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const jsonschema = require('jsonschema') 4 | const errors = require('../utils/errors') 5 | const logger = require('../utils/logger') 6 | 7 | function validate(schema, inputData) { 8 | const validator = new jsonschema.Validator() 9 | schema.additionalProperties = false 10 | const validationErrors = validator.validate(inputData, schema).errors 11 | if (validationErrors.length > 0) { 12 | logger.info(validationErrors) 13 | throw new errors.ValidationError() 14 | } 15 | } 16 | 17 | module.exports = { 18 | validate, 19 | } 20 | -------------------------------------------------------------------------------- /src/validations/schemas/dogs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const dogId = { 4 | type: 'Object', 5 | required: true, 6 | properties: { 7 | id: { type: 'integer', required: true, min: 1, max: 100000 }, 8 | }, 9 | } 10 | 11 | const dog = { 12 | type: 'Object', 13 | required: true, 14 | properties: { 15 | name: { type: 'string', required: true }, 16 | breed: { type: 'string', required: true }, 17 | birthYear: { type: 'number' }, 18 | photo: { type: 'string', format: 'url' }, 19 | }, 20 | } 21 | 22 | module.exports = { 23 | dogId, 24 | dog, 25 | } 26 | -------------------------------------------------------------------------------- /src/validations/schemas/users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const jwtToken = { 4 | type: 'Object', 5 | required: true, 6 | properties: { 7 | jwtToken: { type: 'string', required: true }, 8 | }, 9 | } 10 | 11 | const login = { 12 | type: 'Object', 13 | required: true, 14 | properties: { 15 | email: { type: 'string', required: true, format: 'email', maxLength: 80 }, 16 | password: { type: 'string', required: true, minLength: 8, maxLength: 80 }, 17 | }, 18 | } 19 | 20 | const signUp = { 21 | type: 'Object', 22 | required: true, 23 | properties: { 24 | name: { type: 'string', required: true, pattern: '^[A-Za-z. -]+$', maxLength: 80 }, 25 | email: { type: 'string', required: true, format: 'email', maxLength: 80 }, 26 | password: { type: 'string', required: true, minLength: 8, maxLength: 80 }, 27 | }, 28 | } 29 | 30 | module.exports = { 31 | jwtToken, 32 | login, 33 | signUp, 34 | } 35 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const log = require('./utils/logger') 4 | const verificationJob = require('./jobs/verification') 5 | 6 | const worker = {} 7 | 8 | // Define start method 9 | worker.start = () => { 10 | log.info('Starting worker…') 11 | 12 | // Start jobs here: 13 | verificationJob.execute() 14 | } 15 | 16 | // Define worker shutdown 17 | worker.stop = () => { 18 | log.info('Stopping worker…') 19 | } 20 | 21 | // Start worker 22 | if (require.main === module) { 23 | worker.start() 24 | } 25 | 26 | process.once('SIGINT', () => worker.stop()) 27 | process.once('SIGTERM', () => worker.stop()) 28 | 29 | module.exports = worker 30 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { knex } = require('../src/database') 4 | 5 | const query = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname='public';" 6 | 7 | const ignoreTableNames = [ 8 | 'knex_migrations', 9 | 'knex_migrations_lock', 10 | ] 11 | 12 | module.exports = { 13 | resetDb: async () => { 14 | const tableNames = (await knex.raw(query)) 15 | .rows 16 | .map(table => table[Object.keys(table)[0]]) 17 | .filter(tableName => !ignoreTableNames.includes(tableName)) 18 | .map(tableName => `"${tableName}"`) 19 | 20 | return knex.raw(`TRUNCATE ${tableNames.join()} RESTART IDENTITY CASCADE`) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /tests/integration/dogs/create.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const request = require('supertest-koa-agent') 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | const app = require('../../../src/app') 7 | const { resetDb } = require('../../helpers') 8 | const dogApi = require('../../../src/services/dogapi') 9 | const rekognition = require('../../../src/services/rekognition') 10 | 11 | const sandbox = sinon.createSandbox() 12 | 13 | describe('Dogs', () => { 14 | beforeEach(resetDb) 15 | 16 | context('POST /dogs', () => { 17 | let userToken 18 | 19 | beforeEach(async () => { 20 | const res = await request(app) 21 | .post('/users') 22 | .send({ 23 | email: 'mail@sfs.cz', 24 | name: 'david', 25 | password: '11111111', 26 | }) 27 | .expect(201) 28 | 29 | userToken = res.body.accessToken 30 | 31 | sandbox.stub(dogApi, 'getRandomBreedImage') 32 | .returns(Promise.resolve('http://domain.com/image.jpg')) 33 | 34 | sandbox.stub(rekognition, 'isDogRecognized') 35 | .returns(Promise.resolve(true)) 36 | }) 37 | 38 | it('responds with newly created dog', async () => { 39 | const dogData = { 40 | name: 'Azor', 41 | breed: 'chihuahua', 42 | birthYear: 2000, 43 | } 44 | 45 | const res = await request(app) 46 | .post('/dogs') 47 | .set('Authorization', `jwt ${userToken}`) 48 | .send(dogData) 49 | .expect(201) 50 | 51 | expect(res.body).to.deep.include({ 52 | ...dogData, 53 | }) 54 | 55 | expect(res.body.photo).to.be.a('string') 56 | 57 | expect(res.body.photo).to.not.be.empty // eslint-disable-line no-unused-expressions 58 | 59 | expect(res.body).to.have.all.keys([ 60 | 'name', 61 | 'createdAt', 62 | 'updatedAt', 63 | 'userId', 64 | 'id', 65 | 'breed', 66 | 'birthYear', 67 | 'photo', 68 | 'photoVerified', 69 | ]) 70 | }) 71 | 72 | afterEach(() => sandbox.restore()) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /tests/integration/users/create.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const request = require('supertest-koa-agent') 4 | const { expect } = require('chai') 5 | const app = require('../../../src/app') 6 | const { resetDb } = require('../../helpers') 7 | const usersRepository = require('../../../src/repositories/users') 8 | 9 | describe('Users', () => { 10 | beforeEach(resetDb) 11 | 12 | context('POST /users', () => { 13 | const userData = { 14 | email: 'david@gmail.com', 15 | name: 'david', 16 | } 17 | 18 | it('responds with newly created user', async () => { 19 | const res = await request(app) 20 | .post('/users') 21 | .send({ 22 | ...userData, 23 | password: '11111111', 24 | }) 25 | .expect(201) 26 | 27 | expect(res.body).to.deep.include({ 28 | ...userData, 29 | disabled: false, 30 | id: 1, 31 | }) 32 | 33 | expect(res.body).to.have.all.keys([ 34 | 'name', 35 | 'email', 36 | 'disabled', 37 | 'createdAt', 38 | 'id', 39 | 'accessToken', 40 | ]) 41 | }) 42 | 43 | it('responds with error when not all required attributes are in body', async () => { 44 | const res = await request(app) 45 | .post('/users') 46 | .send({}) 47 | .expect(400) 48 | 49 | expect(res.body).includes.keys([ 50 | 'message', 51 | 'type', 52 | ]) 53 | }) 54 | 55 | it('responds with error when email is already taken', async () => { 56 | await usersRepository.create({ 57 | ...userData, 58 | password: '111', 59 | }) 60 | 61 | const res = await request(app) 62 | .post('/users') 63 | .send({ 64 | ...userData, 65 | password: '11111111', 66 | }) 67 | .expect(409) 68 | 69 | expect(res.body).includes.keys([ 70 | 'message', 71 | 'type', 72 | ]) 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /tests/mocha.opts: -------------------------------------------------------------------------------- 1 | --colors 2 | --recursive 3 | --timeout 5000 4 | --check-leaks 5 | --globals level 6 | --exit 7 | -------------------------------------------------------------------------------- /tests/unit/crypto.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const crypto = require('../../src/utils/crypto') 5 | 6 | describe('generateAccessToken', () => { 7 | it('generateAccessToken should generate a valid token', async () => { 8 | const userId = 2 9 | const token = await crypto.generateAccessToken(userId) 10 | 11 | const decodedPayload = await crypto.verifyAccessToken(token) 12 | expect(decodedPayload).to.deep.include({ userId }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | 5 | 6 | function multiplyBy4(num) { 7 | return num * 4 8 | } 9 | 10 | describe('example', () => { 11 | it('multiplyBy4', () => { 12 | const result = multiplyBy4(2) 13 | const expected = 8 14 | assert.strictEqual(result, expected) 15 | }) 16 | }) 17 | --------------------------------------------------------------------------------