├── .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 | [](https://opensource.org/licenses/MIT)     
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 |
--------------------------------------------------------------------------------