├── .editorconfig ├── .env.sample ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── Swag ├── Info.yaml ├── MainSwag.yaml ├── Paths │ ├── Admin │ │ ├── Admin.responses.yaml │ │ └── Admin.yaml │ ├── Contributors │ │ ├── Contributors.responses.yaml │ │ └── Contributors.yaml │ ├── FAQs │ │ ├── FAQs.responses.yaml │ │ └── FAQs.yaml │ ├── History │ │ ├── History.responses.yaml │ │ └── History.yaml │ └── Videos │ │ ├── Videos.responses.yaml │ │ └── Videos.yaml ├── README.md ├── Schema.yaml ├── Servers.yaml └── Tags.yaml ├── deploy.sh ├── docs ├── APIs.json ├── architecture │ ├── design-decisions.md │ └── index.md └── index.md ├── now.json ├── package.json ├── src ├── api │ ├── admin │ │ ├── admin.controller.js │ │ ├── admin.routes.js │ │ └── admin.test.js │ ├── contributors │ │ ├── contributors.controller.js │ │ ├── contributors.routes.js │ │ └── contributors.test.js │ ├── docs │ │ ├── docs.controller.js │ │ ├── docs.routes.js │ │ ├── docs.test.js │ │ └── versions.html │ ├── faqs │ │ ├── faqs.controller.js │ │ ├── faqs.model.js │ │ ├── faqs.routes.js │ │ └── faqs.test.js │ ├── history │ │ ├── history.controller.js │ │ ├── history.model.js │ │ ├── history.routes.js │ │ └── history.test.js │ └── videos │ │ ├── videos.controller.js │ │ ├── videos.model.js │ │ ├── videos.routes.js │ │ └── videos.test.js ├── app.js ├── app.test.js ├── config │ └── index.js ├── helpers │ ├── databaseConnection.js │ ├── fetchData.js │ └── testHelpers.js ├── index.js ├── middlewares │ └── index.js ├── public │ ├── swagger-ui-bundle.js │ └── swagger-ui-standalone-preset.js ├── routes.js └── tasks │ ├── openCoverage.js │ └── seed │ ├── localDevSeed.js │ ├── seed.js │ ├── seedFaq.js │ ├── seedHistory.js │ └── seedVideos.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | MONGO_URI=mongodb://localhost:27017/codinggardencommunity 3 | TEST_MONGO_URI=mongodb://localhost:27017/codinggardencommunity-test 4 | ADMIN_SECRET=keyboardcat 5 | YOUTUBE_API_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaa 6 | YOUTUBE_CHANNEL_ID=UCLNgu_OupwoeESgtab33CCw 7 | PORT=3000 8 | HOST=0.0.0.0 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/public/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb-base', 'prettier'], 3 | plugins: ['jest', 'promise', 'prettier'], 4 | env: { 5 | 'jest/globals': true 6 | }, 7 | rules: { 8 | 'promise/prefer-await-to-then': 'error', 9 | 'prettier/prettier': 'error' 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # npm lock file 64 | package-lock.json 65 | 66 | # vscode sttings files 67 | .vscode/ 68 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 200, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "trailingComma": "es5", 9 | "arrowParens": "avoid", 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | notifications: 5 | email: false 6 | node_js: 7 | - stable 8 | cache: yarn 9 | before_install: 10 | - curl -o- -L https://yarnpkg.com/install.sh | bash 11 | - export PATH="$HOME/.yarn/bin:$PATH" 12 | stages: 13 | - test 14 | - name: deploy 15 | # Don't deploy on tags or forked repositories 16 | if: type = push AND fork = false 17 | jobs: 18 | include: 19 | ### START TEST STAGE ### 20 | - stage: test 21 | script: yarn run lint && yarn run test 22 | ### END TEST STAGE ### 23 | ### START DEPLOY STAGE ### 24 | - stage: deploy 25 | name: "Deploy to now.sh" 26 | if: branch = develop OR branch = staging OR branch = master 27 | script: skip 28 | deploy: 29 | provider: script 30 | script: yarn run deploy 31 | skip_cleanup: true 32 | on: 33 | # We already filter the job above, so deployment can run on every branch that matches the if above 34 | all_branches: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2019 Coding Garden Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![GitHub package.json version](https://img.shields.io/github/package-json/v/CodingGardenCommunity/app-backend.svg) ![Travis (.org) branch](https://img.shields.io/travis/CodingGardenCommunity/app-backend/develop.svg) ![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/CodingGardenCommunity/app-backend.svg) ![GitHub contributors](https://img.shields.io/github/contributors/CodingGardenCommunity/app-backend.svg) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/CodingGardenCommunity/app-backend.svg) 2 | 3 | # Coding Garden Community App API 4 | 5 | This repository contains the source files and documentation for the API of the 6 | Coding Garden Community App. For more information about the Community App 7 | please visit the [App Wiki](https://github.com/CodingGardenCommunity/app-wiki/wiki). 8 | 9 | ## Running the API locally 10 | 11 | > **NOTE:** If you find yourself in some trouble going through this, reach out to us directly on our [Discord server](https://discord.gg/bPBuk3N). 12 | 13 | ### Prerequisites 14 | 15 | 1. **NodeJS:**
16 | Please install [NodeJS >= 10.15.0](https://nodejs.org/en/download/). If you already have it, you're good to go. 17 | 18 | 1. **Yarn:**
19 | Visit [Yarn download page](https://yarnpkg.com/en/docs/install). Select your Operating system and follow the instructions. It's as easy as eating a 🍰. 20 | 21 | 1. **EditorConfig:**
22 | Please visit [EditorConfig](https://editorconfig.org/) -> `Download a Plugin` section and scroll through to see if you need to install an additional Plugin/Extension for your code editor or IDE. If your IDE needs one, you should be able to find a link to that plugin/extension on that page. 23 | 24 | This prerequisite is directly related to: [`.editorconfig`](https://github.com/CodingGardenCommunity/app-backend/blob/develop/.editorconfig) in the root directory of this project. 25 | 26 | **_More About EditorConfig:_**
27 | EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. The EditorConfig project consists of a file format for defining coding styles and a collection of text editor plugins that enable editors to read the file format and adhere to defined styles. EditorConfig files are easily readable and they work nicely with version control systems. 28 | 29 | --- 30 | 31 | Once you have the [Prerequisites](#prerequisites) covered: 32 | 33 | 1. [Clone](https://help.github.com/articles/cloning-a-repository/) this repository from GitHub onto your local computer. 34 | 35 | ```sh 36 | $ git clone https://github.com/CodingGardenCommunity/app-backend.git 37 | ``` 38 | 39 | 1. Navigate into the project folder and install all of its necessary dependencies with Yarn. 40 | 41 | ```sh 42 | $ cd app-backend 43 | $ yarn install 44 | ``` 45 | 46 | 1. Install MongoDB and make sure it's running 47 | 48 | - For Mac OSX with [homebrew](http://brew.sh/): `brew install mongodb` then `brew services start mongodb` 49 | - For Windows and Linux: [MongoDB Installation](https://docs.mongodb.com/manual/installation/) 50 | 51 | 1. Make a copy of `.env.sample` and rename it to `.env`. You can do so with this simple command: 52 | 53 | > **NOTE:** If you are using Windows Command Prompt, you need to use `copy` instead of `cp`.
54 | 55 | ```sh 56 | $ cp .env.sample .env 57 | ``` 58 | 59 | You don't need to change any values in `.env` file. The default values work well for development purposes. 60 | 61 | 1. Once you have MongoDB and `.env` file ready, seed the local database by running: 62 | 63 | ```sh 64 | $ yarn run seed 65 | ``` 66 | 67 | 1. To make sure everything is setup properly, run tests. 68 | 69 | ```sh 70 | $ yarn run test 71 | ``` 72 | 73 | If all tests pass, we can safely conclude that setup is complete and its working as expected. 🙌 Wooh!!
74 | If not, don't worry. We are together on this mission!! Reach out to us on our [Discord server](https://discord.gg/bPBuk3N). 75 | 76 | 1. Once that's done, tap your back. You are ready to start contributing 😃
77 | You can run - 78 | 79 | ```sh 80 | $ yarn run dev 81 | ``` 82 | 83 | to start the server. 84 | 85 | You can now visit to view the APIs. 86 | 87 | Further, checkout [package.json](https://github.com/CodingGardenCommunity/app-backend/blob/develop/package.json) file to learn about (more) available scripts/commands. 88 | 89 | Happy coding! 🥂 90 | 91 | 92 | ## Dependencies used 93 | 94 | - [Express](https://expressjs.com/): Web application framework designed for building web applications and APIs. 95 | - [mongoose](https://mongoosejs.com/): Schema based object modeling for mongoDB. 96 | - [joi](https://github.com/hapijs/joi): Object schema validation library. 97 | - [CORS](https://github.com/expressjs/cors): Express middleware to enable CORS functionalities. 98 | - [dotenv](https://github.com/motdotla/dotenv): For setting environment variables. 99 | - [Swagger UI Express](https://github.com/scottie1984/swagger-ui-express): Auto-generated API docs, based on a swagger.json file. 100 | - [ESLint](https://eslint.org/): Code linter. Analyses the code for potential errors. 101 | - This project uses ESLint in conjunction with another dependency called "eslint-config-airbnb" for implementing the [airbnb set of rules](https://github.com/airbnb/javascript) helping to write clean javascript. 102 | - [Jest](https://jestjs.io/): For testing. 103 | -------------------------------------------------------------------------------- /Swag/Info.yaml: -------------------------------------------------------------------------------- 1 | Info: 2 | version: v1 3 | title: CodingGarden Community App APIs 4 | description: 'This documentation contains all the information for the backend API of the Coding Garden Community App.

5 | **Version:** `v1`.
6 | **Find other versions [here](/docs/versions).**' 7 | contact: 8 | name: The CodingGarden Community 9 | url: 'https://codinggarden.community/' 10 | license: 11 | name: The MIT License 12 | url: 'https://opensource.org/licenses/MIT' 13 | -------------------------------------------------------------------------------- /Swag/MainSwag.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | $ref: './Info.yaml#/Info' 4 | servers: 5 | $ref: './Servers.yaml#/Servers' 6 | tags: 7 | $ref: './Tags.yaml#/Tags' 8 | paths: 9 | /contributors: 10 | get: 11 | $ref: './Paths/Contributors/Contributors.yaml#/AllContributors' 12 | 13 | /contributors/{id}: 14 | get: 15 | $ref: './Paths/Contributors/Contributors.yaml#/SingleContributor' 16 | 17 | /faqs: 18 | get: 19 | $ref: './Paths/FAQs/FAQs.yaml#/AllFAQs' 20 | post: 21 | $ref: './Paths/FAQs/FAQs.yaml#/AddFAQ' 22 | 23 | /faqs/{id}: 24 | get: 25 | $ref: './Paths/FAQs/FAQs.yaml#/SingleFAQ' 26 | delete: 27 | $ref: './Paths/FAQs/FAQs.yaml#/DeleteFAQ' 28 | 29 | /history: 30 | get: 31 | $ref: './Paths/History/History.yaml#/EntireHistory' 32 | 33 | /history/{id}: 34 | get: 35 | $ref: './Paths/History/History.yaml#/SingleMilestone' 36 | 37 | /videos: 38 | get: 39 | $ref: './Paths/Videos/Videos.yaml#/AllVideos' 40 | 41 | /videos/{id}: 42 | get: 43 | $ref: './Paths/Videos/Videos.yaml#/SingleVideo' 44 | 45 | /admin/seed: 46 | post: 47 | $ref: './Paths/Admin/Admin.yaml#/Seed' 48 | -------------------------------------------------------------------------------- /Swag/Paths/Admin/Admin.responses.yaml: -------------------------------------------------------------------------------- 1 | # POST /admin/seed responses 2 | Seed: 3 | 200: 4 | description: 'text/plain response: OK' 5 | content: 6 | text/plain: 7 | schema: 8 | type: String 9 | example: OK 10 | 401: 11 | $ref: '../../Schema.yaml#/UnAuthorized' 12 | 500: 13 | $ref: '../../Schema.yaml#/InternalServerError' 14 | -------------------------------------------------------------------------------- /Swag/Paths/Admin/Admin.yaml: -------------------------------------------------------------------------------- 1 | # POST /admin/seed 2 | Seed: 3 | tags: 4 | - Admin 5 | summary: Populate DB 6 | operationId: seed 7 | parameters: 8 | - name: X-Admin-Secret 9 | in: header 10 | schema: 11 | type: string 12 | 13 | responses: 14 | 200: 15 | $ref: 'Admin.responses.yaml#/Seed/200' 16 | 401: 17 | $ref: 'Admin.responses.yaml#/Seed/401' 18 | 500: 19 | $ref: 'Admin.responses.yaml#/Seed/500' 20 | -------------------------------------------------------------------------------- /Swag/Paths/Contributors/Contributors.responses.yaml: -------------------------------------------------------------------------------- 1 | # GET /contributors responses 2 | AllContributors: 3 | 200: 4 | description: An array of contributor objects 5 | content: 6 | application/json: 7 | schema: 8 | type: array 9 | items: 10 | $ref: '../../Schema.yaml#/Contributor' 11 | 500: 12 | $ref: '../../Schema.yaml#/InternalServerError' 13 | 14 | # GET /contributors/{id} responses 15 | SingleContributor: 16 | 200: 17 | description: A contributor object 18 | content: 19 | application/json: 20 | schema: 21 | type: array 22 | items: 23 | $ref: '../../Schema.yaml#/Contributor' 24 | 404: 25 | description: Contributor not found. 26 | content: 27 | application/json: 28 | schema: 29 | $ref: '../../Schema.yaml#/Error' 30 | example: 31 | message: There is no contributor with the ID that you requested 32 | status: 404 33 | stack: "RangeError: There is no contributor with the ID that you requested." 34 | 500: 35 | $ref: '../../Schema.yaml#/InternalServerError' 36 | 37 | -------------------------------------------------------------------------------- /Swag/Paths/Contributors/Contributors.yaml: -------------------------------------------------------------------------------- 1 | # GET /contributors 2 | AllContributors: 3 | tags: 4 | - Contributors 5 | summary: Returns list of Contributors 6 | operationId: getAllContributors 7 | description: This end point returns list of kind contributors of CodingGarden Community App in long term active support order. 8 | responses: 9 | 200: 10 | $ref: 'Contributors.responses.yaml#/AllContributors/200' 11 | 500: 12 | $ref: 'Contributors.responses.yaml#/AllContributors/500' 13 | 14 | # GET /contributors/{id} 15 | SingleContributor: 16 | tags: 17 | - Contributors 18 | summary: Returns one contributor object 19 | operationId: getSingleContributorById 20 | description: This end point returns single contributor of CodingGarden Community App based on `id` 21 | parameters: 22 | - name: id 23 | in: path 24 | required: true 25 | description: Contributor `id`. It is same as GitHub `username` 26 | schema: 27 | type: string 28 | example: w3cj 29 | responses: 30 | 200: 31 | $ref: 'Contributors.responses.yaml#/SingleContributor/200' 32 | 404: 33 | $ref: 'Contributors.responses.yaml#/SingleContributor/404' 34 | 500: 35 | $ref: 'Contributors.responses.yaml#/SingleContributor/500' 36 | 37 | -------------------------------------------------------------------------------- /Swag/Paths/FAQs/FAQs.responses.yaml: -------------------------------------------------------------------------------- 1 | # GET /faqs responses 2 | AllFAQs: 3 | 200: 4 | description: An array of FAQ objects 5 | content: 6 | application/json: 7 | schema: 8 | type: array 9 | items: 10 | $ref: '../../Schema.yaml#/FAQ' 11 | 500: 12 | $ref: '../../Schema.yaml#/InternalServerError' 13 | 14 | # POST /faqs responses 15 | AddFAQ: 16 | 200: 17 | description: Added new FAQ entry 18 | content: 19 | application/json: 20 | schema: 21 | properties: 22 | status: 23 | type: number 24 | example: 200 25 | message: 26 | type: string 27 | example: 'FAQ with ID: 5d52b3c020c9d50a98953447 has been added successfully to the DB.' 28 | 401: 29 | $ref: '../../Schema.yaml#/UnAuthorized' 30 | 404: 31 | description: Invalid input 32 | content: 33 | application/json: 34 | schema: 35 | $ref: '../../Schema.yaml#/Error' 36 | example: 37 | message: Make sure your request includes a question and answer. 38 | status: 404 39 | stack: 'ReferenceError: Make sure your request includes a question and answer.' 40 | 500: 41 | $ref: '../../Schema.yaml#/InternalServerError' 42 | 43 | # GET /faqs/{id} responses 44 | SingleFAQ: 45 | 200: 46 | description: One FAQ object 47 | content: 48 | application/json: 49 | schema: 50 | type: array 51 | items: 52 | $ref: '../../Schema.yaml#/FAQ' 53 | 404: 54 | description: Requested FAQ item doesn't exist. 55 | content: 56 | application/json: 57 | schema: 58 | $ref: '../../Schema.yaml#/Error' 59 | example: 60 | message: The requested ID does not exist. 61 | status: 404 62 | stack: 'ReferenceError: The requested ID does not exist.' 63 | 500: 64 | $ref: '../../Schema.yaml#/InternalServerError' 65 | 66 | # DELETE /faqs/{id} responses 67 | DeleteFAQ: 68 | 200: 69 | description: FAQ entry deleted. 70 | content: 71 | application/json: 72 | schema: 73 | properties: 74 | status: 75 | type: number 76 | example: 200 77 | message: 78 | type: string 79 | example: FAQ removed successfully from DB. 80 | 401: 81 | $ref: '../../Schema.yaml#/UnAuthorized' 82 | 404: 83 | description: Could not find FAQ with that `id` 84 | content: 85 | application/json: 86 | schema: 87 | properties: 88 | message: 89 | type: string 90 | example: There is no FAQ to delete with that ID. 91 | status: 92 | type: number 93 | example: 404 94 | stack: 95 | type: string 96 | example: 'ReferenceError: There is no FAQ to delete with that ID.' 97 | 500: 98 | $ref: '../../Schema.yaml#/InternalServerError' 99 | -------------------------------------------------------------------------------- /Swag/Paths/FAQs/FAQs.yaml: -------------------------------------------------------------------------------- 1 | # GET /faqs 2 | AllFAQs: 3 | tags: 4 | - FAQs 5 | summary: Returns list of FAQs 6 | operationId: getAllFAQs 7 | description: This end point returns the list/collection of frequently asked questions(with answers) on/about CodingGarden Community and CJ. 8 | responses: 9 | 200: 10 | $ref: 'FAQs.responses.yaml#/AllFAQs/200' 11 | 500: 12 | $ref: 'FAQs.responses.yaml#/AllFAQs/500' 13 | 14 | # POST /faqs 15 | AddFAQ: 16 | tags: 17 | - FAQs 18 | summary: Add a new FAQ 19 | operationId: addFAQ 20 | parameters: 21 | - name: X-Admin-Secret 22 | in: header 23 | schema: 24 | type: string 25 | requestBody: 26 | description: FAQ object that needs to be added. 27 | content: 28 | application/json: 29 | schema: 30 | required: 31 | - question 32 | - answer 33 | properties: 34 | question: 35 | type: string 36 | answer: 37 | type: string 38 | example: 39 | question: Is this just another question? 40 | answer: Yes, this is just another answer. 41 | responses: 42 | 200: 43 | $ref: 'FAQs.responses.yaml#/AddFAQ/200' 44 | 401: 45 | $ref: 'FAQs.responses.yaml#/AddFAQ/401' 46 | 404: 47 | $ref: 'FAQs.responses.yaml#/AddFAQ/404' 48 | 500: 49 | $ref: 'FAQs.responses.yaml#/AddFAQ/500' 50 | 51 | # GET /faqs/{id} 52 | SingleFAQ: 53 | tags: 54 | - FAQs 55 | summary: Returns one FAQ object 56 | operationId: getSingleFaqById 57 | description: This end point returns only one FAQ(with answer) based on `id` from the collection of FAQs on/about CodingGarden Community and CJ. 58 | parameters: 59 | - name: id 60 | in: path 61 | required: true 62 | description: Unique identifier for each FAQs. Refer Schema for an example. 63 | schema: 64 | type: string 65 | example: 5d526753bf8d2f24f4f12236 66 | responses: 67 | 200: 68 | $ref: 'FAQs.responses.yaml#/SingleFAQ/200' 69 | 404: 70 | $ref: 'FAQs.responses.yaml#/SingleFAQ/404' 71 | 500: 72 | $ref: 'FAQs.responses.yaml#/SingleFAQ/500' 73 | 74 | # DELETE /faqs/{id} 75 | DeleteFAQ: 76 | tags: 77 | - FAQs 78 | summary: Delete a FAQ 79 | description: This end-point removes a FAQ entry based on provided `id` 80 | operationId: deleteFAQ 81 | parameters: 82 | - name: X-Admin-Secret 83 | in: header 84 | schema: 85 | type: string 86 | - name: id 87 | in: path 88 | description: FAQ id to delete 89 | required: true 90 | schema: 91 | type: string 92 | example: 5d528db4091476273cce7d9d 93 | responses: 94 | 200: 95 | $ref: 'FAQs.responses.yaml#/DeleteFAQ/200' 96 | 401: 97 | $ref: 'FAQs.responses.yaml#/DeleteFAQ/401' 98 | 404: 99 | $ref: 'FAQs.responses.yaml#/DeleteFAQ/404' 100 | 500: 101 | $ref: 'FAQs.responses.yaml#/DeleteFAQ/500' 102 | -------------------------------------------------------------------------------- /Swag/Paths/History/History.responses.yaml: -------------------------------------------------------------------------------- 1 | # GET /history responses 2 | EntireHistory: 3 | 200: 4 | description: An array of milestone objects 5 | content: 6 | application/json: 7 | schema: 8 | type: array 9 | items: 10 | $ref: '../../Schema.yaml#/Milestone' 11 | 500: 12 | $ref: '../../Schema.yaml#/InternalServerError' 13 | 14 | # GET /history/{id} responses 15 | SingleMilestone: 16 | 200: 17 | description: A milestone object 18 | content: 19 | application/json: 20 | schema: 21 | type: array 22 | items: 23 | $ref: '../../Schema.yaml#/Milestone' 24 | 404: 25 | description: 'Error: Not Found' 26 | content: 27 | application/json: 28 | schema: 29 | $ref: '../../Schema.yaml#/Error' 30 | example: 31 | message: Invalid History ID. 32 | status: 404 33 | stack: "RangeError: Invalid History ID." 34 | 500: 35 | $ref: '../../Schema.yaml#/InternalServerError' 36 | 37 | -------------------------------------------------------------------------------- /Swag/Paths/History/History.yaml: -------------------------------------------------------------------------------- 1 | # GET /history 2 | EntireHistory: 3 | tags: 4 | - History 5 | summary: Returns list of milestones. 6 | operationId: getEntireHistory 7 | description: This end point returns list of milestones that CodingGarden Community has crossed over a long time ago in a galaxy far far away.. 8 | responses: 9 | 200: 10 | $ref: 'History.responses.yaml#/EntireHistory/200' 11 | 500: 12 | $ref: 'History.responses.yaml#/EntireHistory/500' 13 | 14 | # GET /history/{id} 15 | SingleMilestone: 16 | tags: 17 | - History 18 | summary: Returns single milestone object 19 | operationId: getSingleMilestoneById 20 | description: This end point returns single Milestone event of CodingGarden Community based on `id` as they happened. True story. 21 | parameters: 22 | - name: id 23 | in: path 24 | required: true 25 | description: Milestone `id`. 26 | schema: 27 | type: string 28 | example: 5d53a0cfbb779e2988d67d10 29 | responses: 30 | 200: 31 | $ref: 'History.responses.yaml#/SingleMilestone/200' 32 | 404: 33 | $ref: 'History.responses.yaml#/SingleMilestone/404' 34 | 500: 35 | $ref: 'History.responses.yaml#/SingleMilestone/500' 36 | 37 | -------------------------------------------------------------------------------- /Swag/Paths/Videos/Videos.responses.yaml: -------------------------------------------------------------------------------- 1 | # GET /videos responses 2 | AllVideos: 3 | 200: 4 | description: An array of video objects 5 | content: 6 | application/json: 7 | schema: 8 | type: array 9 | items: 10 | $ref: '../../Schema.yaml#/Video' 11 | 500: 12 | $ref: '../../Schema.yaml#/InternalServerError' 13 | 14 | # GET /videos/{id} responses 15 | SingleVideo: 16 | 200: 17 | description: A video object 18 | content: 19 | application/json: 20 | schema: 21 | type: array 22 | items: 23 | $ref: '../../Schema.yaml#/Video' 24 | 404: 25 | description: 'Error: Not Found' 26 | content: 27 | application/json: 28 | schema: 29 | $ref: '../../Schema.yaml#/Error' 30 | example: 31 | message: Invalid Video ID. 32 | status: 404 33 | stack: 'RangeError: Invalid Video ID.' 34 | 500: 35 | $ref: '../../Schema.yaml#/InternalServerError' 36 | -------------------------------------------------------------------------------- /Swag/Paths/Videos/Videos.yaml: -------------------------------------------------------------------------------- 1 | # GET /videos 2 | AllVideos: 3 | tags: 4 | - Videos 5 | summary: Returns list of Videos. 6 | operationId: getAllVideos 7 | description: This endpoint returns a list of all the videos from The Coding Garden YouTube channel. 8 | responses: 9 | 200: 10 | $ref: 'Videos.responses.yaml#/AllVideos/200' 11 | 500: 12 | $ref: 'Videos.responses.yaml#/AllVideos/500' 13 | 14 | # GET /videos/{id} 15 | SingleVideo: 16 | tags: 17 | - Videos 18 | summary: Returns single video object 19 | operationId: getSingleVideoById 20 | description: This endpoint returns single video from The Coding Garden YouTube channel based on `id`. 21 | parameters: 22 | - name: id 23 | in: path 24 | required: true 25 | description: Video `id`. 26 | schema: 27 | type: string 28 | example: 5d53a0cfbb779e2988d67d22 29 | responses: 30 | 200: 31 | $ref: 'Videos.responses.yaml#/SingleVideo/200' 32 | 404: 33 | $ref: 'Videos.responses.yaml#/SingleVideo/404' 34 | 500: 35 | $ref: 'Videos.responses.yaml#/SingleVideo/500' 36 | -------------------------------------------------------------------------------- /Swag/README.md: -------------------------------------------------------------------------------- 1 | **How to consume API Docs?**
2 | Run Server:
3 | `npm run dev` 4 | 5 | Then visit `/docs/`
6 | Example: `http://localhost:3000/docs/v1` 7 | 8 | If you don't know the version availability, simply visit - ex: `http://localhost:3000/docs/` for versions list. 9 | 10 | > **Note:** If you are on local server, make sure the domain-name is actually `localhost` and not `127.0.0.1` to(avoid `cross-origin` blocks) use `Try it out` feature. 11 | 12 | --- 13 | 14 | **How to build or update API Docs?**
15 | Following details/documentation/procedure is for building API Documentation for anyone updating API docs in the future. 16 | 17 | **Dependencies:**
18 | 19 | 1. `swagger-ui-watcher`
20 | Install this globally:
21 | `npm i -g swagger-ui-watcher` 22 | 23 | `swagger-ui-watcher ./Swag/MainSwag.yaml` to watch and reload Doc changes while building. 24 | 25 | 1. `swagger-ui-express`
26 | This package is available as main project dependency. No need to install it separately. 27 | 28 | **Details:**
29 | `./Swag` is the root directory of modular Swagger YAML files.
30 | `./Swag/MainSwag.yaml` is the file where all other doc files combine with the help of `$ref`s. 31 | 32 | **Finally, to build Doc:**
33 | `npm run buildAPIDoc // > swagger-ui-watcher ./Swag/MainSwag.yaml --bundle=./docs/APIs.json`
34 | This generates `APIs.json` file at `./docs/` directory.
35 | 36 | **Once that's done, update the version if needed:**
37 | `src/api/docs/doc.routes.js // Version route - Rename the JSON files to reflect their versions etc.`
38 | `src/api/docs/versions.html // Version list` 39 | 40 | `./src/app.js` `require`s `./docs/APIs.json` as the main Swagger file to serve API Docs in browser. 41 | 42 | --- 43 | -------------------------------------------------------------------------------- /Swag/Schema.yaml: -------------------------------------------------------------------------------- 1 | Error: 2 | type: object 3 | properties: 4 | message: 5 | type: string 6 | status: 7 | type: number 8 | stack: 9 | type: string 10 | 11 | InternalServerError: 12 | description: 'Error: Internal Server Error' 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | message: 19 | type: string 20 | status: 21 | type: number 22 | stack: 23 | type: string 24 | example: 25 | message: Request to failed 26 | status: 500 27 | stack: 'FetchError: request to failed' 28 | 29 | UnAuthorized: 30 | description: 'You are not authorized to perform this task.' 31 | content: 32 | application/json: 33 | schema: 34 | $ref: '#/Error' 35 | example: 36 | message: Un-Authorized 37 | status: 401 38 | stack: 'Error: Un-Authorized' 39 | 40 | Contributor: 41 | type: object 42 | properties: 43 | "type": 44 | type: string 45 | example: contributor 46 | id: 47 | type: string 48 | example: w3cj 49 | attributes: 50 | type: object 51 | properties: 52 | username: 53 | type: string 54 | example: w3cj 55 | name: 56 | type: string 57 | example: CJ 58 | image: 59 | type: string 60 | example: 'https://avatars1.githubusercontent.com/u/14241866' 61 | countryCode: 62 | type: string 63 | example: USA 64 | active: 65 | type: boolean 66 | example: true 67 | joined: 68 | type: string 69 | example: '2018-12-16' 70 | teamIds: 71 | type: array 72 | items: 73 | type: number 74 | example: 75 | - 0 76 | - 1 77 | - 2 78 | - 3 79 | - 4 80 | 81 | FAQ: 82 | type: object 83 | properties: 84 | "type": 85 | type: string 86 | example: faq 87 | id: 88 | type: string 89 | example: 5d526753bf8d2f24f4f12236 90 | attributes: 91 | required: 92 | - question 93 | - answer 94 | type: object 95 | properties: 96 | question: 97 | type: string 98 | example: What break timer do you use? 99 | answer: 100 | type: string 101 | example: It's called Time Out by Dejal. It is only available for Mac. For Windows, checkout Eye Leo. I have it setup for a 10 second micro break every 10 minutes and a 5 minute break every 60 minutes. 102 | createdAt: 103 | type: string 104 | example: 2019-08-13T07:31:31.879Z 105 | updatedAt: 106 | type: string 107 | example: 2019-08-13T07:31:31.879Z 108 | 109 | Milestone: 110 | type: object 111 | required: 112 | - type 113 | properties: 114 | "type": 115 | type: string 116 | example: history 117 | id: 118 | type: string 119 | example: 5d53a0cfbb779e2988d67d10 120 | attributes: 121 | required: 122 | - name 123 | - date 124 | - description 125 | type: object 126 | properties: 127 | "type": 128 | type: string 129 | example: video 130 | name: 131 | type: string 132 | example: First video! 133 | videoID: 134 | type: string 135 | example: WYa47JkZH_U&t=552s 136 | date: 137 | type: string 138 | example: '2016-12-14T00:00:00.000Z' 139 | description: 140 | type: string 141 | example: The description of the video... 142 | url: 143 | type: string 144 | example: https://www.youtube.com/watch?v=WYa47JkZH_U&t=552s 145 | thumbnail: 146 | type: string 147 | example: https://i.ytimg.com/vi/WYa47JkZH_U/hqdefault.jpg?sqp=-oaymwEZCPYBEIoBSFXyq4qpAwsIARUAAIhCGAFwAQ==&rs=AOn4CLAXBaYYlSuEcmhpAH712ajkjMcOxA 148 | createdAt: 149 | type: string 150 | example: '2019-08-14T05:49:03.844Z' 151 | updatedAt: 152 | type: string 153 | example: '2019-08-14T05:49:03.844Z' 154 | 155 | Video: 156 | type: object 157 | required: 158 | - type 159 | properties: 160 | "type": 161 | type: string 162 | example: video 163 | id: 164 | type: string 165 | example: 5d53a0cfbb779e2988d67d10 166 | attributes: 167 | required: 168 | - name 169 | - date 170 | - description 171 | type: object 172 | properties: 173 | "type": 174 | type: string 175 | example: video 176 | name: 177 | type: string 178 | example: First video! 179 | videoID: 180 | type: string 181 | example: WYa47JkZH_U&t=552s 182 | date: 183 | type: string 184 | example: '2016-12-14T00:00:00.000Z' 185 | description: 186 | type: string 187 | example: The description of the video... 188 | url: 189 | type: string 190 | example: https://www.youtube.com/watch?v=WYa47JkZH_U&t=552s 191 | thumbnail: 192 | type: string 193 | example: https://i.ytimg.com/vi/WYa47JkZH_U/hqdefault.jpg?sqp=-oaymwEZCPYBEIoBSFXyq4qpAwsIARUAAIhCGAFwAQ==&rs=AOn4CLAXBaYYlSuEcmhpAH712ajkjMcOxA 194 | createdAt: 195 | type: string 196 | example: '2019-08-14T05:49:03.844Z' 197 | updatedAt: 198 | type: string 199 | example: '2019-08-14T05:49:03.844Z' 200 | -------------------------------------------------------------------------------- /Swag/Servers.yaml: -------------------------------------------------------------------------------- 1 | Servers: 2 | - url: 'http://localhost:3000/' 3 | description: Local server 4 | - url: 'https://api-dev.codinggarden.community/' 5 | description: Development server 6 | - url: 'https://api.codinggarden.community/' 7 | description: Production server 8 | -------------------------------------------------------------------------------- /Swag/Tags.yaml: -------------------------------------------------------------------------------- 1 | Tags: 2 | - name: Contributors 3 | - name: FAQs 4 | - name: History 5 | - name: Videos 6 | - name: Admin 7 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function usage() { 4 | echo "Usage: $(basename "$0") [option...] {development|staging|production}" >&2 5 | echo 6 | echo " Coding Garden Community App API deployment script" 7 | echo " Deploys the API to the specified environment on now.sh" 8 | echo 9 | echo " -h, --help Show this message" 10 | echo " -n, --now-token Specify the now token. (or set environment variable \$NOW_TOKEN)" 11 | echo " -e, --node-env Specify the node environemt. (or set environment variable \$NODE_ENV)" 12 | echo " -m, --mongo-uri Specify the mongo uri. (or set environment variable \$MONGO_URI)" 13 | echo " -a, --alias Specify the deploy alias. (or set environment variable \$DEPLOY_ALIAS)" 14 | echo 15 | 16 | exit 1 17 | } 18 | 19 | while : 20 | do 21 | case "$1" in 22 | -h|--help) 23 | usage 24 | exit 0 25 | ;; 26 | -n|--now-token) 27 | # TODO: validate input length and chars 28 | NOW_TOKEN="$2" 29 | shift 2 30 | ;; 31 | -m|--mongo-uri) 32 | # TODO: validate input length and chars 33 | MONGO_URI="$2" 34 | shift 2 35 | ;; 36 | -e|--node-env) 37 | # TODO: validate input length and chars 38 | NODE_ENV="$2" 39 | shift 2 40 | ;; 41 | -a|--alias) 42 | # TODO: validate input length and chars 43 | DEPLOY_ALIAS="$2" 44 | shift 2 45 | ;; 46 | --) 47 | shift 48 | break 49 | ;; 50 | -*) 51 | echo "Error: Unknown option: $1" >&2 52 | echo 53 | usage 54 | exit 1 55 | ;; 56 | *) 57 | break 58 | ;; 59 | esac 60 | done 61 | 62 | if [ -z "$NOW_TOKEN" ]; then 63 | echo "Error: NOW_TOKEN is not set via environment variable or as argument" 64 | echo 65 | usage 66 | exit 1 67 | fi 68 | 69 | if [ "$1" ]; then 70 | env=$1 71 | elif [ -n "$TRAVIS_BRANCH" ]; then 72 | case "$TRAVIS_BRANCH" in 73 | develop) 74 | env=development 75 | ;; 76 | staging) 77 | env=staging 78 | ;; 79 | master) 80 | env=production 81 | ;; 82 | *) 83 | echo "Missing or invalid environment." 84 | usage 85 | exit 1 86 | ;; 87 | esac 88 | fi 89 | 90 | case "$env" in 91 | development) 92 | if [ -z "$NODE_ENV" ]; then 93 | NODE_ENV=development 94 | fi 95 | if [ -z "$DEPLOY_ALIAS" ]; then 96 | DEPLOY_ALIAS=api-dev.codinggarden.community 97 | fi 98 | if [ -z "$MONGO_URI" ]; then 99 | MONGO_URI=@community-app-db-dev 100 | fi 101 | ;; 102 | staging) 103 | if [ -z "$NODE_ENV" ]; then 104 | NODE_ENV=development 105 | fi 106 | if [ -z "$DEPLOY_ALIAS" ]; then 107 | DEPLOY_ALIAS=api-staging.codinggarden.community 108 | fi 109 | if [ -z "$MONGO_URI" ]; then 110 | MONGO_URI=@community-app-db-staging 111 | fi 112 | ;; 113 | production) 114 | if [ -z "$NODE_ENV" ]; then 115 | NODE_ENV=production 116 | fi 117 | if [ -z "$DEPLOY_ALIAS" ]; then 118 | DEPLOY_ALIAS=api.codinggarden.community 119 | fi 120 | if [ -z "$MONGO_URI" ]; then 121 | MONGO_URI=@community-app-db-prod 122 | fi 123 | ;; 124 | *) 125 | echo "Missing or invalid environment." 126 | usage 127 | exit 1 128 | ;; 129 | esac 130 | 131 | if [ -z "$MONGO_URI" ]; then 132 | echo "Error: MONGO_URI is not set via environment variable or as argument" 133 | echo 134 | usage 135 | exit 1 136 | fi 137 | 138 | if [ -z "$NOW_TOKEN" ]; then 139 | echo "Error: NOW_TOKEN is not set via environment variable or as argument" 140 | echo 141 | usage 142 | exit 1 143 | fi 144 | 145 | echo "Deploying to $env environment with alias $DEPLOY_ALIAS" 146 | 147 | DEPLOYMENT_URL=$(npx now --token "$NOW_TOKEN" deploy -e NODE_ENV="$NODE_ENV" -e MONGO_URI="$MONGO_URI" -e ADMIN_SECRET=@community-api-admin-secret) 148 | npx now --token "$NOW_TOKEN" alias $DEPLOYMENT_URL $DEPLOY_ALIAS -------------------------------------------------------------------------------- /docs/APIs.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "version": "v1", 5 | "title": "CodingGarden Community App APIs", 6 | "description": "This documentation contains all the information for the backend API of the Coding Garden Community App.

**Version:** `v1`.
**Find other versions [here](/docs/versions).**", 7 | "contact": { 8 | "name": "The CodingGarden Community", 9 | "url": "https://codinggarden.community/" 10 | }, 11 | "license": { 12 | "name": "The MIT License", 13 | "url": "https://opensource.org/licenses/MIT" 14 | } 15 | }, 16 | "servers": [ 17 | { 18 | "url": "http://localhost:3000/", 19 | "description": "Local server" 20 | }, 21 | { 22 | "url": "https://api-dev.codinggarden.community/", 23 | "description": "Development server" 24 | }, 25 | { 26 | "url": "https://api.codinggarden.community/", 27 | "description": "Production server" 28 | } 29 | ], 30 | "tags": [ 31 | { 32 | "name": "Contributors" 33 | }, 34 | { 35 | "name": "FAQs" 36 | }, 37 | { 38 | "name": "History" 39 | }, 40 | { 41 | "name": "Videos" 42 | }, 43 | { 44 | "name": "Admin" 45 | } 46 | ], 47 | "paths": { 48 | "/contributors": { 49 | "get": { 50 | "tags": [ 51 | "Contributors" 52 | ], 53 | "summary": "Returns list of Contributors", 54 | "operationId": "getAllContributors", 55 | "description": "This end point returns list of kind contributors of CodingGarden Community App in long term active support order.", 56 | "responses": { 57 | "200": { 58 | "description": "An array of contributor objects", 59 | "content": { 60 | "application/json": { 61 | "schema": { 62 | "type": "array", 63 | "items": { 64 | "type": "object", 65 | "properties": { 66 | "type": { 67 | "type": "string", 68 | "example": "contributor" 69 | }, 70 | "id": { 71 | "type": "string", 72 | "example": "w3cj" 73 | }, 74 | "attributes": { 75 | "type": "object", 76 | "properties": { 77 | "username": { 78 | "type": "string", 79 | "example": "w3cj" 80 | }, 81 | "name": { 82 | "type": "string", 83 | "example": "CJ" 84 | }, 85 | "image": { 86 | "type": "string", 87 | "example": "https://avatars1.githubusercontent.com/u/14241866" 88 | }, 89 | "countryCode": { 90 | "type": "string", 91 | "example": "USA" 92 | }, 93 | "active": { 94 | "type": "boolean", 95 | "example": true 96 | }, 97 | "joined": { 98 | "type": "string", 99 | "example": "2018-12-16" 100 | }, 101 | "teamIds": { 102 | "type": "array", 103 | "items": { 104 | "type": "number" 105 | }, 106 | "example": [ 107 | 0, 108 | 1, 109 | 2, 110 | 3, 111 | 4 112 | ] 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | }, 122 | "500": { 123 | "description": "Error: Internal Server Error", 124 | "content": { 125 | "application/json": { 126 | "schema": { 127 | "type": "object", 128 | "properties": { 129 | "message": { 130 | "type": "string" 131 | }, 132 | "status": { 133 | "type": "number" 134 | }, 135 | "stack": { 136 | "type": "string" 137 | } 138 | }, 139 | "example": { 140 | "message": "Request to failed", 141 | "status": 500, 142 | "stack": "FetchError: request to failed" 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | }, 151 | "/contributors/{id}": { 152 | "get": { 153 | "tags": [ 154 | "Contributors" 155 | ], 156 | "summary": "Returns one contributor object", 157 | "operationId": "getSingleContributorById", 158 | "description": "This end point returns single contributor of CodingGarden Community App based on `id`", 159 | "parameters": [ 160 | { 161 | "name": "id", 162 | "in": "path", 163 | "required": true, 164 | "description": "Contributor `id`. It is same as GitHub `username`", 165 | "schema": { 166 | "type": "string", 167 | "example": "w3cj" 168 | } 169 | } 170 | ], 171 | "responses": { 172 | "200": { 173 | "description": "A contributor object", 174 | "content": { 175 | "application/json": { 176 | "schema": { 177 | "type": "array", 178 | "items": { 179 | "type": "object", 180 | "properties": { 181 | "type": { 182 | "type": "string", 183 | "example": "contributor" 184 | }, 185 | "id": { 186 | "type": "string", 187 | "example": "w3cj" 188 | }, 189 | "attributes": { 190 | "type": "object", 191 | "properties": { 192 | "username": { 193 | "type": "string", 194 | "example": "w3cj" 195 | }, 196 | "name": { 197 | "type": "string", 198 | "example": "CJ" 199 | }, 200 | "image": { 201 | "type": "string", 202 | "example": "https://avatars1.githubusercontent.com/u/14241866" 203 | }, 204 | "countryCode": { 205 | "type": "string", 206 | "example": "USA" 207 | }, 208 | "active": { 209 | "type": "boolean", 210 | "example": true 211 | }, 212 | "joined": { 213 | "type": "string", 214 | "example": "2018-12-16" 215 | }, 216 | "teamIds": { 217 | "type": "array", 218 | "items": { 219 | "type": "number" 220 | }, 221 | "example": [ 222 | 0, 223 | 1, 224 | 2, 225 | 3, 226 | 4 227 | ] 228 | } 229 | } 230 | } 231 | } 232 | } 233 | } 234 | } 235 | } 236 | }, 237 | "404": { 238 | "description": "Contributor not found.", 239 | "content": { 240 | "application/json": { 241 | "schema": { 242 | "type": "object", 243 | "properties": { 244 | "message": { 245 | "type": "string" 246 | }, 247 | "status": { 248 | "type": "number" 249 | }, 250 | "stack": { 251 | "type": "string" 252 | } 253 | } 254 | }, 255 | "example": { 256 | "message": "There is no contributor with the ID that you requested", 257 | "status": 404, 258 | "stack": "RangeError: There is no contributor with the ID that you requested." 259 | } 260 | } 261 | } 262 | }, 263 | "500": { 264 | "description": "Error: Internal Server Error", 265 | "content": { 266 | "application/json": { 267 | "schema": { 268 | "type": "object", 269 | "properties": { 270 | "message": { 271 | "type": "string" 272 | }, 273 | "status": { 274 | "type": "number" 275 | }, 276 | "stack": { 277 | "type": "string" 278 | } 279 | }, 280 | "example": { 281 | "message": "Request to failed", 282 | "status": 500, 283 | "stack": "FetchError: request to failed" 284 | } 285 | } 286 | } 287 | } 288 | } 289 | } 290 | } 291 | }, 292 | "/faqs": { 293 | "get": { 294 | "tags": [ 295 | "FAQs" 296 | ], 297 | "summary": "Returns list of FAQs", 298 | "operationId": "getAllFAQs", 299 | "description": "This end point returns the list/collection of frequently asked questions(with answers) on/about CodingGarden Community and CJ.", 300 | "responses": { 301 | "200": { 302 | "description": "An array of FAQ objects", 303 | "content": { 304 | "application/json": { 305 | "schema": { 306 | "type": "array", 307 | "items": { 308 | "type": "object", 309 | "properties": { 310 | "type": { 311 | "type": "string", 312 | "example": "faq" 313 | }, 314 | "id": { 315 | "type": "string", 316 | "example": "5d526753bf8d2f24f4f12236" 317 | }, 318 | "attributes": { 319 | "required": [ 320 | "question", 321 | "answer" 322 | ], 323 | "type": "object", 324 | "properties": { 325 | "question": { 326 | "type": "string", 327 | "example": "What break timer do you use?" 328 | }, 329 | "answer": { 330 | "type": "string", 331 | "example": "It's called Time Out by Dejal. It is only available for Mac. For Windows, checkout Eye Leo. I have it setup for a 10 second micro break every 10 minutes and a 5 minute break every 60 minutes." 332 | }, 333 | "createdAt": { 334 | "type": "string", 335 | "example": "2019-08-13T07:31:31.879Z" 336 | }, 337 | "updatedAt": { 338 | "type": "string", 339 | "example": "2019-08-13T07:31:31.879Z" 340 | } 341 | } 342 | } 343 | } 344 | } 345 | } 346 | } 347 | } 348 | }, 349 | "500": { 350 | "description": "Error: Internal Server Error", 351 | "content": { 352 | "application/json": { 353 | "schema": { 354 | "type": "object", 355 | "properties": { 356 | "message": { 357 | "type": "string" 358 | }, 359 | "status": { 360 | "type": "number" 361 | }, 362 | "stack": { 363 | "type": "string" 364 | } 365 | }, 366 | "example": { 367 | "message": "Request to failed", 368 | "status": 500, 369 | "stack": "FetchError: request to failed" 370 | } 371 | } 372 | } 373 | } 374 | } 375 | } 376 | }, 377 | "post": { 378 | "tags": [ 379 | "FAQs" 380 | ], 381 | "summary": "Add a new FAQ", 382 | "operationId": "addFAQ", 383 | "parameters": [ 384 | { 385 | "name": "X-Admin-Secret", 386 | "in": "header", 387 | "schema": { 388 | "type": "string" 389 | } 390 | } 391 | ], 392 | "requestBody": { 393 | "description": "FAQ object that needs to be added.", 394 | "content": { 395 | "application/json": { 396 | "schema": { 397 | "required": [ 398 | "question", 399 | "answer" 400 | ], 401 | "properties": { 402 | "question": { 403 | "type": "string" 404 | }, 405 | "answer": { 406 | "type": "string" 407 | } 408 | } 409 | }, 410 | "example": { 411 | "question": "Is this just another question?", 412 | "answer": "Yes, this is just another answer." 413 | } 414 | } 415 | } 416 | }, 417 | "responses": { 418 | "200": { 419 | "description": "Added new FAQ entry", 420 | "content": { 421 | "application/json": { 422 | "schema": { 423 | "properties": { 424 | "status": { 425 | "type": "number", 426 | "example": 200 427 | }, 428 | "message": { 429 | "type": "string", 430 | "example": "FAQ with ID: 5d52b3c020c9d50a98953447 has been added successfully to the DB." 431 | } 432 | } 433 | } 434 | } 435 | } 436 | }, 437 | "401": { 438 | "description": "You are not authorized to perform this task.", 439 | "content": { 440 | "application/json": { 441 | "schema": { 442 | "type": "object", 443 | "properties": { 444 | "message": { 445 | "type": "string" 446 | }, 447 | "status": { 448 | "type": "number" 449 | }, 450 | "stack": { 451 | "type": "string" 452 | } 453 | } 454 | }, 455 | "example": { 456 | "message": "Un-Authorized", 457 | "status": 401, 458 | "stack": "Error: Un-Authorized" 459 | } 460 | } 461 | } 462 | }, 463 | "404": { 464 | "description": "Invalid input", 465 | "content": { 466 | "application/json": { 467 | "schema": { 468 | "type": "object", 469 | "properties": { 470 | "message": { 471 | "type": "string" 472 | }, 473 | "status": { 474 | "type": "number" 475 | }, 476 | "stack": { 477 | "type": "string" 478 | } 479 | } 480 | }, 481 | "example": { 482 | "message": "Make sure your request includes a question and answer.", 483 | "status": 404, 484 | "stack": "ReferenceError: Make sure your request includes a question and answer." 485 | } 486 | } 487 | } 488 | }, 489 | "500": { 490 | "description": "Error: Internal Server Error", 491 | "content": { 492 | "application/json": { 493 | "schema": { 494 | "type": "object", 495 | "properties": { 496 | "message": { 497 | "type": "string" 498 | }, 499 | "status": { 500 | "type": "number" 501 | }, 502 | "stack": { 503 | "type": "string" 504 | } 505 | }, 506 | "example": { 507 | "message": "Request to failed", 508 | "status": 500, 509 | "stack": "FetchError: request to failed" 510 | } 511 | } 512 | } 513 | } 514 | } 515 | } 516 | } 517 | }, 518 | "/faqs/{id}": { 519 | "get": { 520 | "tags": [ 521 | "FAQs" 522 | ], 523 | "summary": "Returns one FAQ object", 524 | "operationId": "getSingleFaqById", 525 | "description": "This end point returns only one FAQ(with answer) based on `id` from the collection of FAQs on/about CodingGarden Community and CJ.", 526 | "parameters": [ 527 | { 528 | "name": "id", 529 | "in": "path", 530 | "required": true, 531 | "description": "Unique identifier for each FAQs. Refer Schema for an example.", 532 | "schema": { 533 | "type": "string", 534 | "example": "5d526753bf8d2f24f4f12236" 535 | } 536 | } 537 | ], 538 | "responses": { 539 | "200": { 540 | "description": "One FAQ object", 541 | "content": { 542 | "application/json": { 543 | "schema": { 544 | "type": "array", 545 | "items": { 546 | "type": "object", 547 | "properties": { 548 | "type": { 549 | "type": "string", 550 | "example": "faq" 551 | }, 552 | "id": { 553 | "type": "string", 554 | "example": "5d526753bf8d2f24f4f12236" 555 | }, 556 | "attributes": { 557 | "required": [ 558 | "question", 559 | "answer" 560 | ], 561 | "type": "object", 562 | "properties": { 563 | "question": { 564 | "type": "string", 565 | "example": "What break timer do you use?" 566 | }, 567 | "answer": { 568 | "type": "string", 569 | "example": "It's called Time Out by Dejal. It is only available for Mac. For Windows, checkout Eye Leo. I have it setup for a 10 second micro break every 10 minutes and a 5 minute break every 60 minutes." 570 | }, 571 | "createdAt": { 572 | "type": "string", 573 | "example": "2019-08-13T07:31:31.879Z" 574 | }, 575 | "updatedAt": { 576 | "type": "string", 577 | "example": "2019-08-13T07:31:31.879Z" 578 | } 579 | } 580 | } 581 | } 582 | } 583 | } 584 | } 585 | } 586 | }, 587 | "404": { 588 | "description": "Requested FAQ item doesn't exist.", 589 | "content": { 590 | "application/json": { 591 | "schema": { 592 | "type": "object", 593 | "properties": { 594 | "message": { 595 | "type": "string" 596 | }, 597 | "status": { 598 | "type": "number" 599 | }, 600 | "stack": { 601 | "type": "string" 602 | } 603 | } 604 | }, 605 | "example": { 606 | "message": "The requested ID does not exist.", 607 | "status": 404, 608 | "stack": "ReferenceError: The requested ID does not exist." 609 | } 610 | } 611 | } 612 | }, 613 | "500": { 614 | "description": "Error: Internal Server Error", 615 | "content": { 616 | "application/json": { 617 | "schema": { 618 | "type": "object", 619 | "properties": { 620 | "message": { 621 | "type": "string" 622 | }, 623 | "status": { 624 | "type": "number" 625 | }, 626 | "stack": { 627 | "type": "string" 628 | } 629 | }, 630 | "example": { 631 | "message": "Request to failed", 632 | "status": 500, 633 | "stack": "FetchError: request to failed" 634 | } 635 | } 636 | } 637 | } 638 | } 639 | } 640 | }, 641 | "delete": { 642 | "tags": [ 643 | "FAQs" 644 | ], 645 | "summary": "Delete a FAQ", 646 | "description": "This end-point removes a FAQ entry based on provided `id`", 647 | "operationId": "deleteFAQ", 648 | "parameters": [ 649 | { 650 | "name": "X-Admin-Secret", 651 | "in": "header", 652 | "schema": { 653 | "type": "string" 654 | } 655 | }, 656 | { 657 | "name": "id", 658 | "in": "path", 659 | "description": "FAQ id to delete", 660 | "required": true, 661 | "schema": { 662 | "type": "string", 663 | "example": "5d528db4091476273cce7d9d" 664 | } 665 | } 666 | ], 667 | "responses": { 668 | "200": { 669 | "description": "FAQ entry deleted.", 670 | "content": { 671 | "application/json": { 672 | "schema": { 673 | "properties": { 674 | "status": { 675 | "type": "number", 676 | "example": 200 677 | }, 678 | "message": { 679 | "type": "string", 680 | "example": "FAQ removed successfully from DB." 681 | } 682 | } 683 | } 684 | } 685 | } 686 | }, 687 | "401": { 688 | "description": "You are not authorized to perform this task.", 689 | "content": { 690 | "application/json": { 691 | "schema": { 692 | "type": "object", 693 | "properties": { 694 | "message": { 695 | "type": "string" 696 | }, 697 | "status": { 698 | "type": "number" 699 | }, 700 | "stack": { 701 | "type": "string" 702 | } 703 | } 704 | }, 705 | "example": { 706 | "message": "Un-Authorized", 707 | "status": 401, 708 | "stack": "Error: Un-Authorized" 709 | } 710 | } 711 | } 712 | }, 713 | "404": { 714 | "description": "Could not find FAQ with that `id`", 715 | "content": { 716 | "application/json": { 717 | "schema": { 718 | "properties": { 719 | "message": { 720 | "type": "string", 721 | "example": "There is no FAQ to delete with that ID." 722 | }, 723 | "status": { 724 | "type": "number", 725 | "example": 404 726 | }, 727 | "stack": { 728 | "type": "string", 729 | "example": "ReferenceError: There is no FAQ to delete with that ID." 730 | } 731 | } 732 | } 733 | } 734 | } 735 | }, 736 | "500": { 737 | "description": "Error: Internal Server Error", 738 | "content": { 739 | "application/json": { 740 | "schema": { 741 | "type": "object", 742 | "properties": { 743 | "message": { 744 | "type": "string" 745 | }, 746 | "status": { 747 | "type": "number" 748 | }, 749 | "stack": { 750 | "type": "string" 751 | } 752 | }, 753 | "example": { 754 | "message": "Request to failed", 755 | "status": 500, 756 | "stack": "FetchError: request to failed" 757 | } 758 | } 759 | } 760 | } 761 | } 762 | } 763 | } 764 | }, 765 | "/history": { 766 | "get": { 767 | "tags": [ 768 | "History" 769 | ], 770 | "summary": "Returns list of milestones.", 771 | "operationId": "getEntireHistory", 772 | "description": "This end point returns list of milestones that CodingGarden Community has crossed over a long time ago in a galaxy far far away..", 773 | "responses": { 774 | "200": { 775 | "description": "An array of milestone objects", 776 | "content": { 777 | "application/json": { 778 | "schema": { 779 | "type": "array", 780 | "items": { 781 | "type": "object", 782 | "required": [ 783 | "type" 784 | ], 785 | "properties": { 786 | "type": { 787 | "type": "string", 788 | "example": "history" 789 | }, 790 | "id": { 791 | "type": "string", 792 | "example": "5d53a0cfbb779e2988d67d10" 793 | }, 794 | "attributes": { 795 | "required": [ 796 | "name", 797 | "date", 798 | "description" 799 | ], 800 | "type": "object", 801 | "properties": { 802 | "type": { 803 | "type": "string", 804 | "example": "video" 805 | }, 806 | "name": { 807 | "type": "string", 808 | "example": "First video!" 809 | }, 810 | "videoID": { 811 | "type": "string", 812 | "example": "WYa47JkZH_U&t=552s" 813 | }, 814 | "date": { 815 | "type": "string", 816 | "example": "2016-12-14T00:00:00.000Z" 817 | }, 818 | "description": { 819 | "type": "string", 820 | "example": "The description of the video..." 821 | }, 822 | "url": { 823 | "type": "string", 824 | "example": "https://www.youtube.com/watch?v=WYa47JkZH_U&t=552s" 825 | }, 826 | "thumbnail": { 827 | "type": "string", 828 | "example": "https://i.ytimg.com/vi/WYa47JkZH_U/hqdefault.jpg?sqp=-oaymwEZCPYBEIoBSFXyq4qpAwsIARUAAIhCGAFwAQ==&rs=AOn4CLAXBaYYlSuEcmhpAH712ajkjMcOxA" 829 | }, 830 | "createdAt": { 831 | "type": "string", 832 | "example": "2019-08-14T05:49:03.844Z" 833 | }, 834 | "updatedAt": { 835 | "type": "string", 836 | "example": "2019-08-14T05:49:03.844Z" 837 | } 838 | } 839 | } 840 | } 841 | } 842 | } 843 | } 844 | } 845 | }, 846 | "500": { 847 | "description": "Error: Internal Server Error", 848 | "content": { 849 | "application/json": { 850 | "schema": { 851 | "type": "object", 852 | "properties": { 853 | "message": { 854 | "type": "string" 855 | }, 856 | "status": { 857 | "type": "number" 858 | }, 859 | "stack": { 860 | "type": "string" 861 | } 862 | }, 863 | "example": { 864 | "message": "Request to failed", 865 | "status": 500, 866 | "stack": "FetchError: request to failed" 867 | } 868 | } 869 | } 870 | } 871 | } 872 | } 873 | } 874 | }, 875 | "/history/{id}": { 876 | "get": { 877 | "tags": [ 878 | "History" 879 | ], 880 | "summary": "Returns single milestone object", 881 | "operationId": "getSingleMilestoneById", 882 | "description": "This end point returns single Milestone event of CodingGarden Community based on `id` as they happened. True story.", 883 | "parameters": [ 884 | { 885 | "name": "id", 886 | "in": "path", 887 | "required": true, 888 | "description": "Milestone `id`.", 889 | "schema": { 890 | "type": "string", 891 | "example": "5d53a0cfbb779e2988d67d10" 892 | } 893 | } 894 | ], 895 | "responses": { 896 | "200": { 897 | "description": "A milestone object", 898 | "content": { 899 | "application/json": { 900 | "schema": { 901 | "type": "array", 902 | "items": { 903 | "type": "object", 904 | "required": [ 905 | "type" 906 | ], 907 | "properties": { 908 | "type": { 909 | "type": "string", 910 | "example": "history" 911 | }, 912 | "id": { 913 | "type": "string", 914 | "example": "5d53a0cfbb779e2988d67d10" 915 | }, 916 | "attributes": { 917 | "required": [ 918 | "name", 919 | "date", 920 | "description" 921 | ], 922 | "type": "object", 923 | "properties": { 924 | "type": { 925 | "type": "string", 926 | "example": "video" 927 | }, 928 | "name": { 929 | "type": "string", 930 | "example": "First video!" 931 | }, 932 | "videoID": { 933 | "type": "string", 934 | "example": "WYa47JkZH_U&t=552s" 935 | }, 936 | "date": { 937 | "type": "string", 938 | "example": "2016-12-14T00:00:00.000Z" 939 | }, 940 | "description": { 941 | "type": "string", 942 | "example": "The description of the video..." 943 | }, 944 | "url": { 945 | "type": "string", 946 | "example": "https://www.youtube.com/watch?v=WYa47JkZH_U&t=552s" 947 | }, 948 | "thumbnail": { 949 | "type": "string", 950 | "example": "https://i.ytimg.com/vi/WYa47JkZH_U/hqdefault.jpg?sqp=-oaymwEZCPYBEIoBSFXyq4qpAwsIARUAAIhCGAFwAQ==&rs=AOn4CLAXBaYYlSuEcmhpAH712ajkjMcOxA" 951 | }, 952 | "createdAt": { 953 | "type": "string", 954 | "example": "2019-08-14T05:49:03.844Z" 955 | }, 956 | "updatedAt": { 957 | "type": "string", 958 | "example": "2019-08-14T05:49:03.844Z" 959 | } 960 | } 961 | } 962 | } 963 | } 964 | } 965 | } 966 | } 967 | }, 968 | "404": { 969 | "description": "Error: Not Found", 970 | "content": { 971 | "application/json": { 972 | "schema": { 973 | "type": "object", 974 | "properties": { 975 | "message": { 976 | "type": "string" 977 | }, 978 | "status": { 979 | "type": "number" 980 | }, 981 | "stack": { 982 | "type": "string" 983 | } 984 | } 985 | }, 986 | "example": { 987 | "message": "Invalid History ID.", 988 | "status": 404, 989 | "stack": "RangeError: Invalid History ID." 990 | } 991 | } 992 | } 993 | }, 994 | "500": { 995 | "description": "Error: Internal Server Error", 996 | "content": { 997 | "application/json": { 998 | "schema": { 999 | "type": "object", 1000 | "properties": { 1001 | "message": { 1002 | "type": "string" 1003 | }, 1004 | "status": { 1005 | "type": "number" 1006 | }, 1007 | "stack": { 1008 | "type": "string" 1009 | } 1010 | }, 1011 | "example": { 1012 | "message": "Request to failed", 1013 | "status": 500, 1014 | "stack": "FetchError: request to failed" 1015 | } 1016 | } 1017 | } 1018 | } 1019 | } 1020 | } 1021 | } 1022 | }, 1023 | "/videos": { 1024 | "get": { 1025 | "tags": [ 1026 | "Videos" 1027 | ], 1028 | "summary": "Returns list of Videos.", 1029 | "operationId": "getAllVideos", 1030 | "description": "This endpoint returns a list of all the videos from The Coding Garden YouTube channel.", 1031 | "responses": { 1032 | "200": { 1033 | "description": "An array of video objects", 1034 | "content": { 1035 | "application/json": { 1036 | "schema": { 1037 | "type": "array", 1038 | "items": { 1039 | "type": "object", 1040 | "required": [ 1041 | "type" 1042 | ], 1043 | "properties": { 1044 | "type": { 1045 | "type": "string", 1046 | "example": "video" 1047 | }, 1048 | "id": { 1049 | "type": "string", 1050 | "example": "5d53a0cfbb779e2988d67d10" 1051 | }, 1052 | "attributes": { 1053 | "required": [ 1054 | "name", 1055 | "date", 1056 | "description" 1057 | ], 1058 | "type": "object", 1059 | "properties": { 1060 | "type": { 1061 | "type": "string", 1062 | "example": "video" 1063 | }, 1064 | "name": { 1065 | "type": "string", 1066 | "example": "First video!" 1067 | }, 1068 | "videoID": { 1069 | "type": "string", 1070 | "example": "WYa47JkZH_U&t=552s" 1071 | }, 1072 | "date": { 1073 | "type": "string", 1074 | "example": "2016-12-14T00:00:00.000Z" 1075 | }, 1076 | "description": { 1077 | "type": "string", 1078 | "example": "The description of the video..." 1079 | }, 1080 | "url": { 1081 | "type": "string", 1082 | "example": "https://www.youtube.com/watch?v=WYa47JkZH_U&t=552s" 1083 | }, 1084 | "thumbnail": { 1085 | "type": "string", 1086 | "example": "https://i.ytimg.com/vi/WYa47JkZH_U/hqdefault.jpg?sqp=-oaymwEZCPYBEIoBSFXyq4qpAwsIARUAAIhCGAFwAQ==&rs=AOn4CLAXBaYYlSuEcmhpAH712ajkjMcOxA" 1087 | }, 1088 | "createdAt": { 1089 | "type": "string", 1090 | "example": "2019-08-14T05:49:03.844Z" 1091 | }, 1092 | "updatedAt": { 1093 | "type": "string", 1094 | "example": "2019-08-14T05:49:03.844Z" 1095 | } 1096 | } 1097 | } 1098 | } 1099 | } 1100 | } 1101 | } 1102 | } 1103 | }, 1104 | "500": { 1105 | "description": "Error: Internal Server Error", 1106 | "content": { 1107 | "application/json": { 1108 | "schema": { 1109 | "type": "object", 1110 | "properties": { 1111 | "message": { 1112 | "type": "string" 1113 | }, 1114 | "status": { 1115 | "type": "number" 1116 | }, 1117 | "stack": { 1118 | "type": "string" 1119 | } 1120 | }, 1121 | "example": { 1122 | "message": "Request to failed", 1123 | "status": 500, 1124 | "stack": "FetchError: request to failed" 1125 | } 1126 | } 1127 | } 1128 | } 1129 | } 1130 | } 1131 | } 1132 | }, 1133 | "/videos/{id}": { 1134 | "get": { 1135 | "tags": [ 1136 | "Videos" 1137 | ], 1138 | "summary": "Returns single video object", 1139 | "operationId": "getSingleVideoById", 1140 | "description": "This endpoint returns single video from The Coding Garden YouTube channel based on `id`.", 1141 | "parameters": [ 1142 | { 1143 | "name": "id", 1144 | "in": "path", 1145 | "required": true, 1146 | "description": "Video `id`.", 1147 | "schema": { 1148 | "type": "string", 1149 | "example": "5d53a0cfbb779e2988d67d22" 1150 | } 1151 | } 1152 | ], 1153 | "responses": { 1154 | "200": { 1155 | "description": "A video object", 1156 | "content": { 1157 | "application/json": { 1158 | "schema": { 1159 | "type": "array", 1160 | "items": { 1161 | "type": "object", 1162 | "required": [ 1163 | "type" 1164 | ], 1165 | "properties": { 1166 | "type": { 1167 | "type": "string", 1168 | "example": "video" 1169 | }, 1170 | "id": { 1171 | "type": "string", 1172 | "example": "5d53a0cfbb779e2988d67d10" 1173 | }, 1174 | "attributes": { 1175 | "required": [ 1176 | "name", 1177 | "date", 1178 | "description" 1179 | ], 1180 | "type": "object", 1181 | "properties": { 1182 | "type": { 1183 | "type": "string", 1184 | "example": "video" 1185 | }, 1186 | "name": { 1187 | "type": "string", 1188 | "example": "First video!" 1189 | }, 1190 | "videoID": { 1191 | "type": "string", 1192 | "example": "WYa47JkZH_U&t=552s" 1193 | }, 1194 | "date": { 1195 | "type": "string", 1196 | "example": "2016-12-14T00:00:00.000Z" 1197 | }, 1198 | "description": { 1199 | "type": "string", 1200 | "example": "The description of the video..." 1201 | }, 1202 | "url": { 1203 | "type": "string", 1204 | "example": "https://www.youtube.com/watch?v=WYa47JkZH_U&t=552s" 1205 | }, 1206 | "thumbnail": { 1207 | "type": "string", 1208 | "example": "https://i.ytimg.com/vi/WYa47JkZH_U/hqdefault.jpg?sqp=-oaymwEZCPYBEIoBSFXyq4qpAwsIARUAAIhCGAFwAQ==&rs=AOn4CLAXBaYYlSuEcmhpAH712ajkjMcOxA" 1209 | }, 1210 | "createdAt": { 1211 | "type": "string", 1212 | "example": "2019-08-14T05:49:03.844Z" 1213 | }, 1214 | "updatedAt": { 1215 | "type": "string", 1216 | "example": "2019-08-14T05:49:03.844Z" 1217 | } 1218 | } 1219 | } 1220 | } 1221 | } 1222 | } 1223 | } 1224 | } 1225 | }, 1226 | "404": { 1227 | "description": "Error: Not Found", 1228 | "content": { 1229 | "application/json": { 1230 | "schema": { 1231 | "type": "object", 1232 | "properties": { 1233 | "message": { 1234 | "type": "string" 1235 | }, 1236 | "status": { 1237 | "type": "number" 1238 | }, 1239 | "stack": { 1240 | "type": "string" 1241 | } 1242 | } 1243 | }, 1244 | "example": { 1245 | "message": "Invalid Video ID.", 1246 | "status": 404, 1247 | "stack": "RangeError: Invalid Video ID." 1248 | } 1249 | } 1250 | } 1251 | }, 1252 | "500": { 1253 | "description": "Error: Internal Server Error", 1254 | "content": { 1255 | "application/json": { 1256 | "schema": { 1257 | "type": "object", 1258 | "properties": { 1259 | "message": { 1260 | "type": "string" 1261 | }, 1262 | "status": { 1263 | "type": "number" 1264 | }, 1265 | "stack": { 1266 | "type": "string" 1267 | } 1268 | }, 1269 | "example": { 1270 | "message": "Request to failed", 1271 | "status": 500, 1272 | "stack": "FetchError: request to failed" 1273 | } 1274 | } 1275 | } 1276 | } 1277 | } 1278 | } 1279 | } 1280 | }, 1281 | "/admin/seed": { 1282 | "post": { 1283 | "tags": [ 1284 | "Admin" 1285 | ], 1286 | "summary": "Populate DB", 1287 | "operationId": "seed", 1288 | "parameters": [ 1289 | { 1290 | "name": "X-Admin-Secret", 1291 | "in": "header", 1292 | "schema": { 1293 | "type": "string" 1294 | } 1295 | } 1296 | ], 1297 | "responses": { 1298 | "200": { 1299 | "description": "text/plain response: OK", 1300 | "content": { 1301 | "text/plain": { 1302 | "schema": { 1303 | "type": "String", 1304 | "example": "OK" 1305 | } 1306 | } 1307 | } 1308 | }, 1309 | "401": { 1310 | "description": "You are not authorized to perform this task.", 1311 | "content": { 1312 | "application/json": { 1313 | "schema": { 1314 | "type": "object", 1315 | "properties": { 1316 | "message": { 1317 | "type": "string" 1318 | }, 1319 | "status": { 1320 | "type": "number" 1321 | }, 1322 | "stack": { 1323 | "type": "string" 1324 | } 1325 | } 1326 | }, 1327 | "example": { 1328 | "message": "Un-Authorized", 1329 | "status": 401, 1330 | "stack": "Error: Un-Authorized" 1331 | } 1332 | } 1333 | } 1334 | }, 1335 | "500": { 1336 | "description": "Error: Internal Server Error", 1337 | "content": { 1338 | "application/json": { 1339 | "schema": { 1340 | "type": "object", 1341 | "properties": { 1342 | "message": { 1343 | "type": "string" 1344 | }, 1345 | "status": { 1346 | "type": "number" 1347 | }, 1348 | "stack": { 1349 | "type": "string" 1350 | } 1351 | }, 1352 | "example": { 1353 | "message": "Request to failed", 1354 | "status": 500, 1355 | "stack": "FetchError: request to failed" 1356 | } 1357 | } 1358 | } 1359 | } 1360 | } 1361 | } 1362 | } 1363 | } 1364 | } 1365 | } -------------------------------------------------------------------------------- /docs/architecture/design-decisions.md: -------------------------------------------------------------------------------- 1 | # Design Decisions 2 | 3 | ## Versioning 4 | 5 | To still have backwards compatibility with breaking changes the API will serve 6 | multiple major versions at the same time during a transition period. 7 | 8 | The versioning will be implemented using a custom HTTP header in the request. 9 | The API will check if the header `X-API-VERSION` is set. If it is it will 10 | process the request based on the desired version. If it is empty or not set it 11 | will default to the latest version. 12 | 13 | ## Compatibility with all clients 14 | 15 | ### Blocked HTTP methods 16 | 17 | When clients are behind a firewall or use a proxy server it could happen, that 18 | requests the client generates are modified. A proxy server could for example 19 | only allow `GET` and `POST` requests and discard any `PUT`, `DELETE`, or other 20 | HTTP methods. 21 | 22 | > The API should be able to operate on `GET` and `POST` alone. If the client is 23 | > not able to make a `PUT`, `DELETE`, etc. request it will use a `POST` request 24 | > with an additional HTTP header `X-HTTP-Method-Override` and a value of 25 | > whichever method originally failed. 26 | 27 | ### Blocked custom headers 28 | 29 | In other cases any HTTP request header that is not listed in the [HTTP/1.1 30 | specifications](https://tools.ietf.org/html/rfc2616) might be blocked by a 31 | firewall or proxy server. 32 | 33 | This will make versioning using a custom headers (e.g. `X-API-Version`) or the 34 | method described above to circumvent blocked HTTP methods impossible. To keep 35 | things simple the API will not implement any measure to avoid this issue. This 36 | decision is also based on the experience that not many clients will have blocked 37 | HTTP headers. 38 | 39 | > If a client is unable to receive and/or send custom HTTP headers the client 40 | > will not be able to use any special features like using a specific API 41 | > version. 42 | -------------------------------------------------------------------------------- /docs/architecture/index.md: -------------------------------------------------------------------------------- 1 | # Architecture documentation 2 | 3 | - [Design Decisions](/docs/architecture/design-decisions.md) 4 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Coding Garden Community App API 2 | 3 | This documentation will contain all information for the backend API of the 4 | Coding Garden Community App. For general information on the Community App please 5 | visit the [App's Wiki](https://github.com/CodingGardenCommunity/app-wiki) 6 | 7 | ## Architecture 8 | 9 | See [architecture documentation](/docs/architecture/index.md) for an overview of the APIs goals and design 10 | decisions. 11 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-backend", 3 | "version": 2, 4 | "scope": "coding-garden-community", 5 | "builds": [{ 6 | "src": "src/index.js", 7 | "use": "@now/node-server" 8 | }, { 9 | "src": "src/public/**", 10 | "use": "@now/static" 11 | }], 12 | "routes": [{ 13 | "src": "/docs/(swagger-ui-bundle.js|swagger-ui-standalone-preset.js)", 14 | "dest": "src/public/$1" 15 | }, { 16 | "src": "/(.*)", 17 | "dest": "src/index.js" 18 | }] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-backend", 3 | "version": "0.1.0", 4 | "description": "The backend API for the Coding Garden community app.", 5 | "main": "src/index.js", 6 | "jest": { 7 | "testEnvironment": "node", 8 | "preset": "jest-puppeteer", 9 | "collectCoverageFrom": [ 10 | "!**node_modules**", 11 | "src/api/**/*.js" 12 | ], 13 | "coverageThreshold": { 14 | "global": { 15 | "branches": 80, 16 | "functions": 90, 17 | "lines": 90, 18 | "statements": 90 19 | } 20 | } 21 | }, 22 | "scripts": { 23 | "start": "cross-env NODE_ENV=production node src/index.js", 24 | "dev": "cross-env NODE_ENV=development nodemon src/index.js", 25 | "test": "cross-env NODE_ENV=test jest --verbose --coverage --detectOpenHandles --forceExit", 26 | "lint": "eslint . --ext .js", 27 | "seed": "node src/tasks/seed/localDevSeed.js", 28 | "deploy": "./deploy.sh", 29 | "format": "prettier --write src/**/*.js", 30 | "buildAPIDoc": "swagger-ui-watcher ./Swag/MainSwag.yaml --bundle=./docs/APIs.json", 31 | "coverage:open": "node src/tasks/openCoverage.js" 32 | }, 33 | "repository": "https://github.com/CodingGardenCommunity/app-backend.git", 34 | "author": "Coding Garden Community App", 35 | "license": "MIT", 36 | "husky": { 37 | "hooks": { 38 | "pre-commit": "yarn run lint && yarn run format && yarn run test" 39 | } 40 | }, 41 | "dependencies": { 42 | "colors": "^1.4.0", 43 | "cors": "^2.8.5", 44 | "dotenv": "^8.1.0", 45 | "express": "^4.17.1", 46 | "joi": "^14.3.1", 47 | "mongoose": "^5.7.5", 48 | "node-fetch": "^2.6.0", 49 | "swagger-ui-express": "^4.1.1" 50 | }, 51 | "devDependencies": { 52 | "babel-eslint": "^10.0.3", 53 | "cross-env": "^6.0.0", 54 | "eslint": "^6.4.0", 55 | "eslint-config-airbnb": "^18.0.1", 56 | "eslint-config-airbnb-base": "^14.0.0", 57 | "eslint-config-prettier": "^6.3.0", 58 | "eslint-plugin-import": "^2.18.2", 59 | "eslint-plugin-jest": "^22.17.0", 60 | "eslint-plugin-prettier": "^3.1.1", 61 | "eslint-plugin-promise": "^4.2.1", 62 | "husky": "^3.0.5", 63 | "jest": "^24.9.0", 64 | "jest-puppeteer": "^4.3.0", 65 | "nodemon": "^1.19.2", 66 | "prettier": "^1.18.2", 67 | "puppeteer": "^1.20.0", 68 | "supertest": "^4.0.2" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/api/admin/admin.controller.js: -------------------------------------------------------------------------------- 1 | const { mainSeed } = require('../../tasks/seed/seed'); 2 | 3 | async function seed(req, res, next) { 4 | try { 5 | await mainSeed(); 6 | res.sendStatus(200); 7 | } catch (error) { 8 | next(error); 9 | } 10 | } 11 | 12 | module.exports = { 13 | seed, 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/admin/admin.routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { seed } = require('./admin.controller'); 3 | 4 | router.post('/seed', seed); 5 | 6 | module.exports = router; 7 | -------------------------------------------------------------------------------- /src/api/admin/admin.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const app = require('../../app'); 4 | 5 | describe('GET /admin', () => { 6 | it('Should respond with a 404 status code', done => { 7 | request(app) 8 | .get('/admin') 9 | .expect('Content-Type', /json/) 10 | .expect(404, done); 11 | }); 12 | }); 13 | 14 | describe('POST /admin/seed', () => { 15 | it('Should respond with a 200 status code', done => { 16 | request(app) 17 | .post('/admin/seed') 18 | .expect(200, done); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/api/contributors/contributors.controller.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | module.exports = async function getContributors(req, res, next) { 4 | const contribURL = 'https://raw.githubusercontent.com/CodingGardenCommunity/contributors/master/contributors.json'; 5 | try { 6 | const response = await fetch(contribURL); 7 | let data = await response.json(); 8 | 9 | if ('id' in req.params) { 10 | const individualData = data.filter(({ github: id }) => id === req.params.id); 11 | if (individualData.length > 0) data = individualData; 12 | else throw new RangeError('There is no contributor with the ID that you requested.'); 13 | } 14 | 15 | const finalResponse = data 16 | .sort((a, b) => { 17 | if (a.active && !b.active) return -1; 18 | if (!a.active && b.active) return 1; 19 | return new Date(a.joined) - new Date(b.joined); 20 | }) 21 | .map(({ name, github: username, image, countryCode, teamIds, active, joined }) => ({ 22 | type: 'contributor', 23 | id: username, 24 | attributes: { 25 | username, 26 | name, 27 | image, 28 | countryCode, 29 | active, 30 | joined, 31 | teamIds, 32 | }, 33 | })); 34 | res.json(finalResponse); 35 | } catch (error) { 36 | if (error instanceof RangeError) res.status(404); 37 | next(error); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/api/contributors/contributors.routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const getContributors = require('./contributors.controller'); 3 | 4 | router.get('/', getContributors); 5 | router.get('/:id', getContributors); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /src/api/contributors/contributors.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const app = require('../../app'); 4 | 5 | describe('GET /contributors', () => { 6 | it('Should respond with a 200 status code', async done => { 7 | const { status, type, body } = await request(app).get('/contributors'); 8 | expect(status).toEqual(200); 9 | expect(type).toEqual('application/json'); 10 | expect(body.length).toBeGreaterThan(0); 11 | done(); 12 | }); 13 | }); 14 | 15 | describe('GET /contributors/:id', () => { 16 | it('Should respond with a 200 status code for valid id', async done => { 17 | const { status, type, body } = await request(app).get('/contributors/w3cj'); 18 | expect(status).toEqual(200); 19 | expect(type).toEqual('application/json'); 20 | expect(body[0].id).toEqual('w3cj'); 21 | done(); 22 | }); 23 | 24 | it('Should respond with 404 status code for invalid id', async done => { 25 | const { 26 | status, 27 | type, 28 | body: { message }, 29 | } = await request(app).get('/contributors/someInvalidID'); 30 | expect(status).toEqual(404); 31 | expect(type).toEqual('application/json'); 32 | expect(message).toEqual('There is no contributor with the ID that you requested.'); 33 | done(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/api/docs/docs.controller.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const latestAPIVersion = 'v1'; 4 | 5 | function sendVersionMarkup(req, res) { 6 | res.sendFile(path.join(__dirname, '/versions.html')); 7 | } 8 | 9 | function redirectToLatestAPIVersion(req, res) { 10 | res.redirect(latestAPIVersion); 11 | } 12 | 13 | module.exports = { sendVersionMarkup, redirectToLatestAPIVersion }; 14 | -------------------------------------------------------------------------------- /src/api/docs/docs.routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { serve, setup } = require('swagger-ui-express'); 3 | const { sendVersionMarkup, redirectToLatestAPIVersion } = require('./docs.controller'); 4 | const openApiDocumentation = require('../../../docs/APIs.json'); 5 | 6 | const options = { 7 | // customCssUrl: '/custom.css' // If additional options are needed. 8 | }; 9 | 10 | router.use('/', serve); 11 | 12 | router.get('/', redirectToLatestAPIVersion); 13 | router.get('/versions', sendVersionMarkup); 14 | router.get('/v1', setup(openApiDocumentation, options)); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /src/api/docs/docs.test.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const { PORT } = require('../../config'); 3 | const app = require('../../app').listen(PORT); 4 | 5 | let browser; 6 | let page; 7 | const BASE_URL = `http://localhost:${PORT}/docs`; 8 | 9 | beforeAll(async () => { 10 | browser = await puppeteer.launch(); 11 | page = await browser.newPage(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await browser.close(); 16 | app.close(); 17 | }); 18 | 19 | describe('GET /docs/', () => { 20 | it('Should display "Available versions" text on page.', async () => { 21 | await page.goto(`${BASE_URL}/versions`); 22 | await expect(page).toMatch('Available versions'); 23 | }); 24 | }); 25 | 26 | describe('GET /docs/v1', () => { 27 | it('Should display "CodingGarden Community App APIs" text on page.', async () => { 28 | await page.goto(`${BASE_URL}/v1`); 29 | await expect(page).toMatch('CodingGarden Community App APIs'); 30 | }); 31 | }); 32 | 33 | const click = async (selector, p) => { 34 | const element = await p.waitForSelector(selector); 35 | await element.click(); 36 | }; 37 | 38 | describe('GET API response through API Doc', () => { 39 | it('Should be able to query /contributors API from Doc page.', async () => { 40 | await page.goto(BASE_URL); 41 | 42 | await click('#operations-Contributors-getAllContributors', page); 43 | await click('.opblock-body > .opblock-section > .opblock-section-header > .try-out > .btn', page); 44 | await click('.opblock-body > .execute-wrapper > .btn', page); 45 | 46 | await page.waitForSelector('.live-responses-table tbody .response-col_status'); 47 | const statusCodeText = await page.$eval('.live-responses-table tbody .response-col_status', el => el.textContent); 48 | const statusCode = parseInt(statusCodeText, 10); 49 | 50 | expect(statusCode).toBe(200); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/api/docs/versions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CodingGarden Community App API versions 8 | 9 | 10 |

Available versions:

11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/api/faqs/faqs.controller.js: -------------------------------------------------------------------------------- 1 | const FAQ = require('./faqs.model'); 2 | 3 | async function getFAQs(req, res, next) { 4 | try { 5 | let response; 6 | if ('id' in req.params) { 7 | if (!/^[a-fA-F0-9]{24}$/.test(req.params.id)) throw new ReferenceError('Invalid FAQ ID.'); 8 | try { 9 | response = [await FAQ.findById(req.params.id).exec()]; 10 | if (response[0] === null) throw new ReferenceError('The requested ID does not exist.'); 11 | } catch ({ message }) { 12 | throw new ReferenceError(message); 13 | } 14 | } else response = await FAQ.find({}).exec(); 15 | 16 | const finalResponse = response.map(({ id, question, answer, createdAt, updatedAt }) => ({ 17 | type: 'faq', 18 | id, 19 | attributes: { 20 | question, 21 | answer, 22 | createdAt, 23 | updatedAt, 24 | }, 25 | })); 26 | res.json(finalResponse); 27 | } catch (error) { 28 | if (error instanceof ReferenceError) res.status(404); 29 | next(error); 30 | } 31 | } 32 | 33 | async function addFAQ(req, res, next) { 34 | try { 35 | const { question, answer } = req.body; 36 | if (!question || !answer) throw new ReferenceError('Make sure your request includes a question and answer.'); 37 | const { _id } = await new FAQ({ question, answer }).save(); 38 | res.json({ 39 | status: 200, 40 | message: `FAQ with ID: ${_id} has been added successfully to the DB.`, 41 | }); 42 | } catch (error) { 43 | if (error instanceof ReferenceError) res.status(404); 44 | next(error); 45 | } 46 | } 47 | 48 | async function removeFAQ(req, res, next) { 49 | try { 50 | const { id: _id } = req.params; 51 | if (!/^[a-fA-F0-9]{24}$/.test(_id)) throw new ReferenceError('Invalid FAQ ID.'); 52 | const deletedFAQ = await FAQ.deleteOne({ _id }); 53 | if (deletedFAQ.deletedCount === 0) throw new ReferenceError('There is no FAQ to delete with that ID.'); 54 | res.json({ 55 | status: 200, 56 | message: 'FAQ removed successfully from DB.', 57 | }); 58 | } catch (error) { 59 | if (error instanceof ReferenceError) res.status(404); 60 | next(error); 61 | } 62 | } 63 | 64 | module.exports = { 65 | getFAQs, 66 | addFAQ, 67 | removeFAQ, 68 | }; 69 | -------------------------------------------------------------------------------- /src/api/faqs/faqs.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | const FAQSchema = new Schema( 6 | { 7 | question: { 8 | type: String, 9 | required: true, 10 | }, 11 | answer: { 12 | type: String, 13 | required: true, 14 | }, 15 | }, 16 | { 17 | timestamps: { 18 | createdAt: 'createdAt', 19 | updatedAt: 'updatedAt', 20 | }, 21 | } 22 | ); 23 | 24 | module.exports = mongoose.model('faqs', FAQSchema); 25 | -------------------------------------------------------------------------------- /src/api/faqs/faqs.routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { isAdmin } = require('../../middlewares'); 3 | const { getFAQs, addFAQ, removeFAQ } = require('./faqs.controller'); 4 | 5 | router.get('/', getFAQs); 6 | router.get('/:id', getFAQs); 7 | router.post('/', isAdmin, addFAQ); 8 | router.delete('/:id', isAdmin, removeFAQ); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /src/api/faqs/faqs.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const { connection } = require('mongoose'); 3 | 4 | const app = require('../../app'); 5 | const FAQ = require('./faqs.model'); 6 | const { prePopulate } = require('../../helpers/testHelpers'); 7 | 8 | const ids = { 9 | invalid: 'invalid_id', 10 | nonexistent: 'aaaaaaaaaaaaaaaaaaaaaaaa', 11 | }; 12 | 13 | const question = 'Populate Question'; 14 | const answer = 'Populate answer'; 15 | const doc = { question, answer }; 16 | 17 | beforeAll(async () => { 18 | ids.valid = await prePopulate(FAQ, doc); 19 | }); 20 | 21 | afterAll(async () => { 22 | await FAQ.deleteMany({}); 23 | connection.close(); 24 | }); 25 | 26 | describe('GET /faqs', () => { 27 | it('Should respond with a 200 status code', done => 28 | request(app) 29 | .get('/faqs') 30 | .expect('Content-Type', /json/) 31 | .expect(200, done)); 32 | it('Should respond with an array', async done => { 33 | const { body } = await request(app) 34 | .get('/faqs') 35 | .expect(200); 36 | expect(body).toEqual(expect.any(Array)); 37 | done(); 38 | }); 39 | it('Should respond with a non-empty array', async done => { 40 | const { body } = await request(app) 41 | .get('/faqs') 42 | .expect(200); 43 | expect(body).toEqual(expect.any(Array)); 44 | expect(body.length).toBeGreaterThan(0); 45 | done(); 46 | }); 47 | }); 48 | 49 | describe('GET /faqs/:id', () => { 50 | it('With an invalid id, should respond with an invalid id message', async done => { 51 | const { 52 | body: { message }, 53 | } = await request(app) 54 | .get(`/faqs/${ids.invalid}`) 55 | .expect(404); 56 | expect(message).toBe('Invalid FAQ ID.'); 57 | done(); 58 | }); 59 | it('With a non existent id, should respond with a non existent id message', async done => { 60 | const { 61 | body: { message }, 62 | } = await request(app) 63 | .get(`/faqs/${ids.nonexistent}`) 64 | .expect(404); 65 | expect(message).toBe('The requested ID does not exist.'); 66 | done(); 67 | }); 68 | it('With a valid id, should respond with a 200 status code', done => 69 | request(app) 70 | .get(`/faqs/${ids.valid}`) 71 | .expect(200, done)); 72 | }); 73 | 74 | describe('POST /faqs', () => { 75 | it('Without a body, should respond with a 404 status code and error message', async done => { 76 | const { 77 | body: { message }, 78 | } = await request(app) 79 | .post('/faqs') 80 | .expect(404); 81 | expect(message).toBe('Make sure your request includes a question and answer.'); 82 | done(); 83 | }); 84 | it('With a valid body, should respond with a 200 status code', async done => { 85 | const { 86 | body: { message }, 87 | } = await request(app) 88 | .post('/faqs') 89 | .send({ question: 'Test Question', answer: 'Test answer' }) 90 | .expect(200); 91 | expect(message).toEqual(expect.stringMatching(/^FAQ with ID: ([a-f0-9]{24}) has been added successfully to the DB\.$/)); 92 | done(); 93 | }); 94 | }); 95 | 96 | describe('DELETE /faqs/:id', () => { 97 | it('With an invalid id, should respond with an invalid id message', async done => { 98 | const { 99 | body: { message }, 100 | } = await request(app) 101 | .delete(`/faqs/${ids.invalid}`) 102 | .expect(404); 103 | expect(message).toBe('Invalid FAQ ID.'); 104 | done(); 105 | }); 106 | it('With a non existent id, should respond with a non existent id message', async done => { 107 | const { 108 | body: { message }, 109 | } = await request(app) 110 | .delete(`/faqs/${ids.nonexistent}`) 111 | .expect(404); 112 | expect(message).toBe('There is no FAQ to delete with that ID.'); 113 | done(); 114 | }); 115 | it('With a valid id, should respond with a success message', async done => { 116 | const { 117 | body: { message }, 118 | } = await request(app) 119 | .delete(`/faqs/${ids.valid}`) 120 | .expect(200); 121 | expect(message).toBe('FAQ removed successfully from DB.'); 122 | done(); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /src/api/history/history.controller.js: -------------------------------------------------------------------------------- 1 | const History = require('./history.model'); 2 | 3 | async function getHistory(req, res, next) { 4 | try { 5 | let response; 6 | if ('id' in req.params) { 7 | if (!/^[a-fA-F0-9]{24}$/.test(req.params.id)) throw new ReferenceError('Invalid History ID.'); 8 | try { 9 | response = [await History.findById(req.params.id).exec()]; 10 | if (response[0] === null) throw new ReferenceError('The requested ID does not exist.'); 11 | } catch ({ message }) { 12 | throw new ReferenceError(message); 13 | } 14 | } else response = await History.find({}).exec(); 15 | 16 | const finalResponse = response.map(({ id, type, name, videoID, title, date, description, url, thumbnail, createdAt, updatedAt }) => ({ 17 | type: 'history', 18 | id, 19 | attributes: { 20 | type, 21 | name, 22 | videoID, 23 | title, 24 | date, 25 | description, 26 | url, 27 | thumbnail, 28 | createdAt, 29 | updatedAt, 30 | }, 31 | })); 32 | return res.json(finalResponse); 33 | } catch (error) { 34 | if (error instanceof ReferenceError) res.status(404); 35 | return next(error); 36 | } 37 | } 38 | 39 | module.exports = { getHistory }; 40 | -------------------------------------------------------------------------------- /src/api/history/history.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | const HistorySchema = new Schema( 6 | { 7 | type: { 8 | type: String, 9 | required: true, 10 | enum: ['milestone', 'video'], 11 | }, 12 | name: { 13 | type: String, 14 | required: true, 15 | }, 16 | date: { 17 | type: Date, 18 | required: true, 19 | }, 20 | description: { 21 | type: String, 22 | required: true, 23 | }, 24 | url: { 25 | type: String, 26 | default: null, 27 | }, 28 | videoID: { 29 | type: String, 30 | default: null, 31 | }, 32 | thumbnail: { 33 | type: String, 34 | default: null, 35 | }, 36 | }, 37 | { 38 | timestamps: { 39 | createdAt: 'createdAt', 40 | updatedAt: 'updatedAt', 41 | }, 42 | } 43 | ); 44 | 45 | module.exports = mongoose.model('histories', HistorySchema); 46 | -------------------------------------------------------------------------------- /src/api/history/history.routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { getHistory } = require('./history.controller'); 3 | 4 | router.get('/', getHistory); 5 | router.get('/:id', getHistory); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /src/api/history/history.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const { connection } = require('mongoose'); 3 | 4 | const app = require('../../app'); 5 | const History = require('./history.model'); 6 | const { prePopulate } = require('../../helpers/testHelpers'); 7 | 8 | const ids = { 9 | invalid: 'invalid_id', 10 | nonexistent: 'aaaaaaaaaaaaaaaaaaaaaaaa', 11 | }; 12 | 13 | const type = 'video'; 14 | const name = 'Some name'; 15 | const date = new Date(); 16 | const description = 'Some description'; 17 | 18 | const doc = { type, name, date, description }; 19 | 20 | beforeAll(async () => { 21 | ids.valid = await prePopulate(History, doc); 22 | }); 23 | 24 | afterAll(async () => { 25 | await History.deleteMany({}); 26 | connection.close(); 27 | }); 28 | 29 | describe('GET /history', () => { 30 | it('Should respond with a 200 status code', done => 31 | request(app) 32 | .get('/history') 33 | .expect('Content-Type', /json/) 34 | .expect(200, done)); 35 | }); 36 | 37 | describe('GET /history/:id', () => { 38 | it('With an invalid id, should respond with an invalid id message', async done => { 39 | const { 40 | body: { message }, 41 | } = await request(app) 42 | .get(`/history/${ids.invalid}`) 43 | .expect(404); 44 | expect(message).toBe('Invalid History ID.'); 45 | done(); 46 | }); 47 | 48 | it('With a non existent id, should respond with a non existent id message', async done => { 49 | const { 50 | body: { message }, 51 | } = await request(app) 52 | .get(`/history/${ids.nonexistent}`) 53 | .expect(404); 54 | expect(message).toBe('The requested ID does not exist.'); 55 | done(); 56 | }); 57 | 58 | it('Should respond with a 200 status code for valid id', async done => { 59 | const { status, body } = await request(app).get(`/history/${ids.valid}`); 60 | expect(status).toEqual(200); 61 | expect(body[0].id).toBe(`${ids.valid}`); 62 | done(); 63 | }); 64 | 65 | it('Should respond with 404 status code for invalid id', async done => { 66 | const { 67 | status, 68 | body: { message }, 69 | } = await request(app).get('/history/someInvalidID'); 70 | expect(status).toEqual(404); 71 | expect(message).toEqual('Invalid History ID.'); 72 | done(); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/api/videos/videos.controller.js: -------------------------------------------------------------------------------- 1 | const Video = require('./videos.model'); 2 | 3 | async function getVideos(req, res, next) { 4 | try { 5 | let response; 6 | if ('id' in req.params) { 7 | if (!/^[a-fA-F0-9]{24}$/.test(req.params.id)) throw new ReferenceError('Invalid Video ID.'); 8 | try { 9 | response = [await Video.findById(req.params.id).exec()]; 10 | if (response[0] === null) throw new ReferenceError('The requested ID does not exist.'); 11 | } catch ({ message }) { 12 | throw new ReferenceError(message); 13 | } 14 | } else response = await Video.find({}).exec(); 15 | 16 | const finalResponse = response.map(({ id, type, name, videoID, title, date, description, url, thumbnail, createdAt, updatedAt }) => ({ 17 | type: 'video', 18 | id, 19 | attributes: { 20 | type, 21 | name, 22 | videoID, 23 | title, 24 | date, 25 | description, 26 | url, 27 | thumbnail, 28 | createdAt, 29 | updatedAt, 30 | }, 31 | })); 32 | return res.json(finalResponse); 33 | } catch (error) { 34 | if (error instanceof ReferenceError) res.status(404); 35 | return next(error); 36 | } 37 | } 38 | 39 | module.exports = { getVideos }; 40 | -------------------------------------------------------------------------------- /src/api/videos/videos.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | const VideoSchema = new Schema( 6 | { 7 | type: { 8 | type: String, 9 | enum: ['milestone', 'video'], 10 | default: 'video', 11 | }, 12 | name: { 13 | type: String, 14 | required: true, 15 | }, 16 | date: { 17 | type: Date, 18 | required: true, 19 | }, 20 | description: { 21 | type: String, 22 | required: false, 23 | }, 24 | url: { 25 | type: String, 26 | default: null, 27 | }, 28 | videoID: { 29 | type: String, 30 | default: null, 31 | }, 32 | thumbnail: { 33 | type: String, 34 | default: null, 35 | }, 36 | }, 37 | { 38 | timestamps: { 39 | createdAt: 'createdAt', 40 | updatedAt: 'updatedAt', 41 | }, 42 | } 43 | ); 44 | 45 | module.exports = mongoose.model('videos', VideoSchema); 46 | -------------------------------------------------------------------------------- /src/api/videos/videos.routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { getVideos } = require('./videos.controller'); 3 | 4 | router.get('/', getVideos); 5 | router.get('/:id', getVideos); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /src/api/videos/videos.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const { connection } = require('mongoose'); 3 | 4 | const app = require('../../app'); 5 | const Video = require('./videos.model'); 6 | const { prePopulate } = require('../../helpers/testHelpers'); 7 | 8 | const ids = { 9 | invalid: 'invalid_id', 10 | nonexistent: 'aaaaaaaaaaaaaaaaaaaaaaaa', 11 | }; 12 | 13 | const type = 'video'; 14 | const name = 'Some name'; 15 | const date = new Date(); 16 | const description = 'Some description'; 17 | 18 | const doc = { type, name, date, description }; 19 | 20 | beforeAll(async () => { 21 | ids.valid = await prePopulate(Video, doc); 22 | }); 23 | 24 | afterAll(async () => { 25 | await Video.deleteMany({}); 26 | connection.close(); 27 | }); 28 | 29 | describe('GET /videos', () => { 30 | it('Should respond with a 200 status code', done => 31 | request(app) 32 | .get('/videos') 33 | .expect('Content-Type', /json/) 34 | .expect(200, done)); 35 | }); 36 | 37 | describe('GET /videos/:id', () => { 38 | it('With an invalid id, should respond with an invalid id message', async done => { 39 | const { 40 | body: { message }, 41 | } = await request(app) 42 | .get(`/videos/${ids.invalid}`) 43 | .expect(404); 44 | expect(message).toBe('Invalid Video ID.'); 45 | done(); 46 | }); 47 | 48 | it('With a non existent id, should respond with a non existent id message', async done => { 49 | const { 50 | body: { message }, 51 | } = await request(app) 52 | .get(`/videos/${ids.nonexistent}`) 53 | .expect(404); 54 | expect(message).toBe('The requested ID does not exist.'); 55 | done(); 56 | }); 57 | 58 | it('Should respond with a 200 status code for valid id', async done => { 59 | const { status, body } = await request(app).get(`/videos/${ids.valid}`); 60 | expect(status).toEqual(200); 61 | expect(body[0].id).toBe(`${ids.valid}`); 62 | done(); 63 | }); 64 | 65 | it('Should respond with 404 status code for invalid id', async done => { 66 | const { 67 | status, 68 | body: { message }, 69 | } = await request(app).get('/videos/someInvalidID'); 70 | expect(status).toEqual(404); 71 | expect(message).toEqual('Invalid Video ID.'); 72 | done(); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const routes = require('./routes'); 3 | 4 | const { fetchVideosJob } = require('./helpers/fetchData'); 5 | 6 | // Database connection 7 | require('./helpers/databaseConnection'); 8 | 9 | // Fetch new video once when app starts. 10 | if (process.env.NODE_ENV !== 'test') fetchVideosJob(); 11 | 12 | const { errorHandler, notFound, cors } = require('./middlewares'); 13 | 14 | // Initialize server 15 | const app = express(); 16 | 17 | // App middleware 18 | if (process.env.NODE_ENV !== 'test') { 19 | app.use(cors); 20 | } 21 | 22 | app.use(express.json()); 23 | 24 | // App routes 25 | app.get('/', (req, res) => res.json({ message: 'Check out /contributors, /faqs, /history, /docs, and /videos' })); 26 | app.use(routes); 27 | 28 | app.use(notFound); 29 | app.use(errorHandler); 30 | 31 | module.exports = app; 32 | -------------------------------------------------------------------------------- /src/app.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const app = require('./app'); 4 | 5 | describe('GET /', () => { 6 | it('Should respond with a message', async done => { 7 | const msg = { message: 'Check out /contributors, /faqs, /history, /docs, and /videos' }; 8 | request(app) 9 | .get('/') 10 | .expect('Content-Type', /json/) 11 | .expect(200, msg, done); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | require('dotenv').config(); 4 | 5 | const options = { 6 | NODE_ENV: Joi.string() 7 | .default('development') 8 | .allow(['development', 'test', 'production']), 9 | PORT: Joi.string().default(3000), 10 | HOST: Joi.string().default('0.0.0.0'), 11 | ADMIN_SECRET: Joi.string().required(), 12 | }; 13 | 14 | if (process.env.NODE_ENV === 'test') { 15 | options.TEST_MONGO_URI = Joi.string().required(); 16 | } else { 17 | options.MONGO_URI = Joi.string().required(); 18 | } 19 | 20 | const schema = Joi.object(options).unknown(true); 21 | 22 | const { error, value: config } = Joi.validate(process.env, schema); 23 | 24 | if (error) { 25 | // eslint-disable-next-line no-console 26 | console.error('Missing property in config.', error.message); 27 | process.exit(1); 28 | } 29 | 30 | module.exports = config; 31 | -------------------------------------------------------------------------------- /src/helpers/databaseConnection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { NODE_ENV, TEST_MONGO_URI, MONGO_URI } = require('../config'); 4 | 5 | const URI = NODE_ENV === 'test' ? TEST_MONGO_URI : MONGO_URI; 6 | 7 | mongoose.connect(URI, { useNewUrlParser: true, useUnifiedTopology: true }); 8 | -------------------------------------------------------------------------------- /src/helpers/fetchData.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const fetch = require('node-fetch'); 3 | const { red, green } = require('colors/safe'); 4 | const Video = require('../api/videos/videos.model'); 5 | const { YOUTUBE_API_KEY, YOUTUBE_CHANNEL_ID } = require('../config'); 6 | 7 | async function fetchLatestYoutubeVideos({ maxResults, publishedAfter }) { 8 | const mrOpt = maxResults ? `maxResults=${maxResults}` : ''; 9 | const paOpt = publishedAfter ? `publishedAfter=${publishedAfter}` : ''; 10 | const optUrl = [mrOpt, paOpt].filter(Boolean).join('&') || ''; 11 | const apiUrl = `https://www.googleapis.com/youtube/v3/search?key=${YOUTUBE_API_KEY}&channelId=${YOUTUBE_CHANNEL_ID}&part=snippet&order=date&${optUrl}`; 12 | try { 13 | const resp = await fetch(apiUrl); 14 | const { items, error } = await resp.json(); 15 | 16 | if (error) { 17 | error.errors.forEach(({ reason }) => { 18 | console.error(`[fetch-error] ${reason}`); 19 | }); 20 | return []; 21 | } 22 | 23 | if (items) { 24 | return items.map(({ id: { videoId: videoID }, snippet: { title: name, publishedAt: date, description, thumbnails: { high: { url: thumbnail } } } }) => { 25 | return { 26 | name, 27 | date, 28 | description, 29 | url: `https://www.youtube.com/watch?v=${videoID}`, 30 | videoID, 31 | thumbnail, 32 | }; 33 | }); 34 | } 35 | return []; 36 | } catch (err) { 37 | console.error(`[error]: ${err}`); 38 | return []; 39 | } 40 | } 41 | 42 | async function fetchVideosJob() { 43 | try { 44 | const video = await Video.findOne({}).sort({ date: -1 }); 45 | // Check if db has at least one video 46 | if (video) { 47 | // Transforms date format to the Youtube-API standard. 48 | const lastDate = video.date.toISOString(); 49 | 50 | const fetchedVideos = await fetchLatestYoutubeVideos({ publishedAfter: lastDate }); 51 | 52 | if (fetchedVideos.length > 0) { 53 | fetchedVideos.forEach(async newVideo => { 54 | if (newVideo.date !== lastDate) { 55 | const { name } = await new Video(newVideo).save(); 56 | console.log(green(`[cron-job] Added new video from Youtube: ${name}, at ${new Date().toISOString()}`)); 57 | } 58 | }); 59 | } 60 | } 61 | } catch (err) { 62 | console.error(red(`[cron-job-error] ${err}`)); 63 | } 64 | } 65 | 66 | module.exports = { fetchLatestYoutubeVideos, fetchVideosJob }; 67 | -------------------------------------------------------------------------------- /src/helpers/testHelpers.js: -------------------------------------------------------------------------------- 1 | // prepoPulate takes in a mongoose model and a document as arguments 2 | // and inserts the document to the collection defined by the model. 3 | async function prePopulate(Model, document) { 4 | const { _id } = await new Model(document).save(); 5 | return _id; 6 | } 7 | 8 | module.exports = { 9 | prePopulate, 10 | }; 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors/safe'); 2 | 3 | const { PORT, HOST } = require('./config'); 4 | const app = require('./app'); 5 | 6 | // Run server 7 | // eslint-disable-next-line no-console 8 | app.listen(PORT, HOST, () => console.log(colors.cyan(`Server started @ http://${HOST}:${PORT}/`))); 9 | -------------------------------------------------------------------------------- /src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | const cors = require('cors'); 2 | 3 | const allowedOrigins = ['https://web.codinggarden.community', 'https://web-dev.codinggarden.community']; 4 | 5 | const corsOptions = { 6 | origin(origin, callback) { 7 | if (process.env.NODE_ENV === 'development' || !origin || allowedOrigins.includes(origin)) { 8 | callback(null, true); 9 | } else { 10 | callback(new Error('Not allowed by CORS')); 11 | } 12 | }, 13 | }; 14 | 15 | function isAdmin(req, res, next) { 16 | const secret = req.get('X-Admin-Secret'); 17 | if (secret === process.env.ADMIN_SECRET || process.env.NODE_ENV === 'test') { 18 | next(); 19 | } else { 20 | res.status(401); 21 | const error = new Error('Un-Authorized'); 22 | next(error); 23 | } 24 | } 25 | 26 | function notFound(req, res, next) { 27 | const error = new Error(`Not Found - ${req.originalUrl}`); 28 | res.status(404); 29 | next(error); 30 | } 31 | 32 | // eslint-disable-next-line no-unused-vars 33 | function errorHandler(error, req, res, next) { 34 | const { message, stack } = error; 35 | const status = res.statusCode === 200 ? 500 : res.statusCode; 36 | res.status(status).json({ 37 | message, 38 | status, 39 | stack: process.env.NODE_ENV === 'production' ? '🥞' : stack, 40 | }); 41 | } 42 | 43 | module.exports = { 44 | isAdmin, 45 | errorHandler, 46 | notFound, 47 | cors: cors(corsOptions), 48 | }; 49 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | 3 | const { isAdmin } = require('./middlewares'); 4 | const contributors = require('./api/contributors/contributors.routes'); 5 | const faqs = require('./api/faqs/faqs.routes'); 6 | const admin = require('./api/admin/admin.routes'); 7 | const history = require('./api/history/history.routes'); 8 | const videos = require('./api/videos/videos.routes'); 9 | const documentation = require('./api/docs/docs.routes'); 10 | 11 | const router = Router(); 12 | 13 | router.use('/contributors', contributors); 14 | router.use('/faqs', faqs); 15 | router.use('/admin', isAdmin, admin); 16 | router.use('/history', history); 17 | router.use('/videos', videos); 18 | router.use('/docs', documentation); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /src/tasks/openCoverage.js: -------------------------------------------------------------------------------- 1 | const { platform } = require('os'); 2 | const { 3 | promises: { stat }, 4 | } = require('fs'); 5 | const { exec } = require('child_process'); 6 | 7 | const file = 'coverage/lcov-report/index.html'; 8 | 9 | function openFile() { 10 | switch (platform()) { 11 | case 'win32': 12 | return exec(`start ${file}`); 13 | case 'darwin': 14 | return exec(`open ${file}`); 15 | default: 16 | return exec(`xdg-open ${file}`); 17 | } 18 | } 19 | 20 | (async () => { 21 | try { 22 | await stat(file); 23 | openFile(); 24 | } catch (err) { 25 | try { 26 | exec('npm run test', openFile); 27 | } catch (error) { 28 | process.stdout.write(err); 29 | process.exit(1); 30 | } 31 | } 32 | })(); 33 | -------------------------------------------------------------------------------- /src/tasks/seed/localDevSeed.js: -------------------------------------------------------------------------------- 1 | const { mainSeed } = require('./seed'); 2 | 3 | (async () => { 4 | try { 5 | await mainSeed(); 6 | } catch (error) { 7 | // eslint-disable-next-line no-console 8 | console.log(error); 9 | } finally { 10 | process.exit(0); 11 | } 12 | })(); 13 | -------------------------------------------------------------------------------- /src/tasks/seed/seed.js: -------------------------------------------------------------------------------- 1 | require('../../helpers/databaseConnection'); 2 | 3 | const seedFAQs = require('./seedFaq'); 4 | const seedHistories = require('./seedHistory'); 5 | const seedVideos = require('./seedVideos'); 6 | 7 | async function mainSeed() { 8 | try { 9 | await seedFAQs(); 10 | await seedHistories(); 11 | await seedVideos(); 12 | } catch (error) { 13 | // eslint-disable-next-line no-console 14 | console.log(error); 15 | } 16 | } 17 | 18 | module.exports = { 19 | mainSeed, 20 | }; 21 | -------------------------------------------------------------------------------- /src/tasks/seed/seedFaq.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors/safe'); 2 | const FAQ = require('../../api/faqs/faqs.model'); 3 | 4 | const faqData = [ 5 | { 6 | question: 'What break timer do you use?', 7 | answer: 8 | "It's called Time Out by Dejal. It is only available for Mac. For Windows, checkout Eye Leo. I have it setup for a 10 second micro break every 10 minutes and a 5 minute break every 60 minutes.", 9 | }, 10 | { 11 | question: 'Will the livestream be available as a video?', 12 | answer: 13 | 'Yes! Every livestream is immediately available to watch after the stream is over. The URL is will be the same. Longer streams take time to process, but will show up on the Coding Garden videos page after a few hours.', 14 | }, 15 | { 16 | question: 'What code editor do you use?', 17 | answer: 18 | 'In my earlier videos I used Atom. Now I use VS Code. I have lots of plugins and settings that make VS Code behave the way it does. Checkout the vscode-settings repo on github to see all of the plugins and settings I use.', 19 | }, 20 | { 21 | question: 'What theme do you use in VS Code?', 22 | answer: 'For VS Code I use Seti-Monokai. In my older videos where I am using Atom, I use Brahalla Syntax.', 23 | }, 24 | { 25 | question: 'What keyboard do you use?', 26 | answer: 'An inexpensive mechanical keyboard from Amazon. Check it out here: https://amzn.to/2EwYmSd', 27 | }, 28 | { 29 | question: 'How long have you been coding?', 30 | answer: 31 | 'Over 15 years! I started coding HTML / CSS websites as a kid. Learned Java, C, C++ in college. Wrote C# / .NET desktop applications for a while. Started learning modern web technologies in my spare time, and taught JavaScript full stack web development for 3+ years.', 32 | }, 33 | { 34 | question: "What's the best way to contact you?", 35 | answer: 'Join the discord. https://coding.garden/discord', 36 | }, 37 | { 38 | question: 'How do you add emojis in VS Code?', 39 | answer: 'This is a feature of Mac OS X. Press CTRL+CMD+Space to bring up the emoji menu! On Windows 10 you can use CTRL+Period', 40 | }, 41 | ]; 42 | 43 | async function seedFAQs() { 44 | await FAQ.deleteMany({}); 45 | await FAQ.insertMany(faqData); 46 | // eslint-disable-next-line no-console 47 | console.log(colors.yellow('DB Seeded with FAQ Data')); 48 | } 49 | module.exports = seedFAQs; 50 | -------------------------------------------------------------------------------- /src/tasks/seed/seedHistory.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors/safe'); 2 | const History = require('../../api/history/history.model'); 3 | 4 | const historyData = [ 5 | { 6 | type: 'milestone', 7 | name: 'Coding Garden Created!', 8 | date: '2015-10-13', 9 | description: 'When the legend started...', 10 | }, 11 | { 12 | type: 'video', 13 | name: 'First video!', 14 | videoID: 'WYa47JkZH_U&t=552s', 15 | title: 'Build a Full Stack JavaScript CRUD App with Node/Express/Handlebars/Bootstrap/Postgres/Knex', 16 | date: '2016-12-14', 17 | description: 'The description of the video...', 18 | url: 'https://www.youtube.com/watch?v=WYa47JkZH_U&t=552s', 19 | thumbnail: 'https://i.ytimg.com/vi/WYa47JkZH_U/hqdefault.jpg?sqp=-oaymwEZCPYBEIoBSFXyq4qpAwsIARUAAIhCGAFwAQ==&rs=AOn4CLAXBaYYlSuEcmhpAH712ajkjMcOxA', 20 | }, 21 | { 22 | type: 'milestone', 23 | name: 'Reached 500 subscribers!', 24 | date: '2018-03-17', 25 | description: 'CJ reached 500 subscribers!', 26 | }, 27 | { 28 | type: 'milestone', 29 | name: 'Reached 50000 views!', 30 | date: '2018-05-05', 31 | description: 'CJ reached 50000 views!', 32 | }, 33 | { 34 | type: 'milestone', 35 | name: 'Reached 1000 subscribers!', 36 | date: '2018-05-19', 37 | description: 'CJ reached 1000 subscribers!', 38 | }, 39 | { 40 | type: 'milestone', 41 | name: 'Reached 2000 subscribers!', 42 | date: '2018-07-04', 43 | description: 'CJ reached 2000 subscribers!', 44 | }, 45 | { 46 | type: 'milestone', 47 | name: 'Reached 100000 views!', 48 | date: '2018-07-15', 49 | description: 'CJ reached 100000 views!', 50 | }, 51 | { 52 | type: 'milestone', 53 | name: 'Reached 3000 subscribers!', 54 | date: '2018-08-19', 55 | description: 'CJ reached 3000 subscribers!', 56 | }, 57 | { 58 | type: 'milestone', 59 | name: 'Reached 4000 subscribers!', 60 | date: '2018-09-12', 61 | description: 'CJ reached 4000 subscribers!', 62 | }, 63 | { 64 | type: 'milestone', 65 | name: 'Reached 5000 subscribers!', 66 | date: '2018-09-26', 67 | description: 'CJ reached 5000 subscribers!', 68 | }, 69 | { 70 | type: 'milestone', 71 | name: 'Reached 6000 subscribers!', 72 | date: '2018-10-04', 73 | description: 'CJ reached 6000 subscribers!', 74 | }, 75 | { 76 | type: 'milestone', 77 | name: 'Reached 7000 subscribers!', 78 | date: '2018-10-20', 79 | description: 'CJ reached 7000 subscribers!', 80 | }, 81 | { 82 | type: 'milestone', 83 | name: 'Reached 200000 views!', 84 | date: '2018-10-21', 85 | description: 'CJ reached 200000 views!', 86 | }, 87 | { 88 | type: 'milestone', 89 | name: 'Reached 8000 subscribers!', 90 | date: '2018-11-20', 91 | description: 'CJ reached 8000 subscribers!', 92 | }, 93 | { 94 | type: 'milestone', 95 | name: 'Reached 9000 subscribers!', 96 | date: '2019-01-05', 97 | description: 'CJ reached 9000 subscribers!', 98 | }, 99 | { 100 | type: 'milestone', 101 | name: 'Reached 300000 views!', 102 | date: '2019-01-21', 103 | description: 'CJ reached 300000 views!', 104 | }, 105 | { 106 | type: 'milestone', 107 | name: 'Reached 10000 subscribers!', 108 | date: '2019-02-19', 109 | description: 'CJ reached 10000 subscribers!', 110 | }, 111 | { 112 | type: 'milestone', 113 | name: 'Reached 400000 views!', 114 | date: '2019-03-30', 115 | description: 'CJ reached 400000 views!', 116 | }, 117 | { 118 | type: 'milestone', 119 | name: 'Reached 11000 subscribers!', 120 | date: '2019-04-02', 121 | description: 'CJ reached 11000 subscribers!', 122 | }, 123 | ]; 124 | 125 | async function seedHistories() { 126 | await History.deleteMany({}); 127 | await History.insertMany(historyData); 128 | // eslint-disable-next-line no-console 129 | console.log(colors.yellow('DB Seeded with History Data')); 130 | } 131 | module.exports = seedHistories; 132 | --------------------------------------------------------------------------------