Skip to content

Commit ceed222

Browse files
authored
feat(core): Add deletePlan controller, tests, and CI pipeline
- Introduced conditional delete and other error handler middlewares, along with a simple unit test and a GitHub actions CI pipeline. - Added the MIT license. - Added CI badge and update `README.md`
1 parent 80c2080 commit ceed222

File tree

22 files changed

+1556
-82
lines changed

22 files changed

+1556
-82
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
HOSTNAME=xxxxxxxxx
2+
PORT=xxxx
3+
REDIS_PORT=xxxx

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ module.exports = {
2121
'default-param-last': 'off',
2222
camelcase: 'off',
2323
},
24-
};
24+
}

.github/workflows/test-suite.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2+
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3+
4+
name: Unit Test Suite
5+
6+
# Controls when the action will run.
7+
on:
8+
workflow_dispatch:
9+
# Triggers the workflow on push or pull request events but only for the master branch
10+
pull_request:
11+
branches: [master]
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
environment: ${{ vars.ENVIRONMENT }}
17+
strategy:
18+
matrix:
19+
node-version: [18.x]
20+
21+
name: Run unit tests
22+
steps:
23+
- uses: actions/checkout@v4
24+
- name: "Create .env file"
25+
run: |
26+
touch .env
27+
echo HOSTNAME=${{ vars.HOSTNAME }} >> .env
28+
echo PORT=${{ vars.PORT }} >> .env
29+
echo DATABASE=${{ vars.REDIS_PORT }} >> .env
30+
- name: Use Node.js ${{ matrix.node-version }}
31+
uses: actions/setup-node@v3
32+
with:
33+
node-version: ${{ matrix.node-version }}
34+
- run: yarn
35+
- run: yarn test

.prettierrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ module.exports = {
66
printWidth: 80,
77
tabWidth: 2,
88
endOfLine: 'auto',
9-
};
9+
}

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2023 Siddharth Rawat
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
# CSYE 7255: Adv Big Data Indexing Techniques
1+
# INFO-7255: Adv Big Data Indexing Techniques
22

3-
>NOTE: This is a WIP. Please visit different branches for each version of the demos for the submissions
3+
[![Unit Test Suite](https://github.com/sydrawat01/INFO7255/actions/workflows/test-suite.yml/badge.svg)](https://github.com/sydrawat01/INFO7255/actions/workflows/test-suite.yml)
44

55
## Setup
66

77
1. Install the dependencies
88

9-
```shell
10-
yarn
11-
#or if you are using npm: npm i
12-
```
9+
```shell
10+
yarn
11+
# or if you are using npm: npm i
12+
```
13+
1314
2. Start the server locally
1415

15-
```shell
16-
yarn start:dev
17-
# npm run start:dev
18-
```
16+
```shell
17+
yarn start:dev
18+
# npm run start:dev
19+
```
1920

2021
## Author
2122

package.json

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
{
22
"name": "api",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "RESTful API",
55
"main": "src/server.js",
66
"author": "Siddharth Rawat <rawat.sid@northeastern.edu>",
77
"license": "MIT",
88
"scripts": {
99
"env": "nodemon -r dotenv/config",
1010
"start:dev": "nodemon --exec babel-node ./src/server.js",
11-
"clean": "rm -rf ./dist"
11+
"clean": "rm -rf ./dist",
12+
"build": "yarn clean && babel ./src/server.js --out-dir ./dist",
13+
"test": "mocha --require @babel/register './tests/*.test.js' --recursive --exit",
14+
"test:dev": "mocha --require @babel/register './tests/*.test.js' --watch"
1215
},
1316
"dependencies": {
14-
"ajv": "^8.12.0",
1517
"app-root-path": "^3.1.0",
1618
"dotenv": "^16.3.1",
1719
"express": "^4.18.2",
@@ -27,6 +29,14 @@
2729
"@babel/plugin-proposal-class-properties": "^7.18.6",
2830
"@babel/preset-env": "^7.22.15",
2931
"@babel/register": "^7.22.15",
30-
"nodemon": "^3.0.1"
32+
"eslint": "^8.50.0",
33+
"eslint-config-airbnb-base": "^15.0.0",
34+
"eslint-config-prettier": "^9.0.0",
35+
"eslint-plugin-import": "^2.28.1",
36+
"eslint-plugin-prettier": "^5.0.0",
37+
"mocha": "^10.2.0",
38+
"nodemon": "^3.0.1",
39+
"prettier": "^3.0.3",
40+
"supertest": "^6.3.3"
3141
}
3242
}

src/api/app.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import express from 'express'
22
import { errorHandler } from './middlewares/errorHandler'
3-
// import { healthRoute } from './routes/health.route'
4-
// import { planRoute } from './routes/plan.route'
53
import { healthRoute, planRoute } from './routes/index.routes'
64

75
const app = express()

src/api/controllers/health.controller.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logger from '../../configs/logger.config'
2-
// import appConfig from '../configs/app.config'
32

43
const health = (req, res) => {
54
const { protocol, method, hostname, originalUrl } = req

src/api/controllers/plan.controller.js

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { findPlan, addPlan, delPlan } from '../services/redis.client'
1+
import { findPlan, addPlan, deleteByPlanId } from '../services/redis.client'
22
import { validate, md5hash } from '../utils/schemaValidation'
33
import {
44
BadRequestError,
5+
InternalServerError,
56
ResourceNotFoundError,
7+
PreConditionFailedError,
68
conflictHandler,
79
createHandler,
10+
noContentHandler,
811
notModifiedHandler,
912
successHandler,
1013
} from '../utils/error.util'
@@ -18,32 +21,31 @@ const getPlan = async (req, res, next) => {
1821
`Requesting ${method} ${protocol}://${hostname}${originalUrl}`,
1922
metaData
2023
)
21-
const planId = params.planId
2224
try {
25+
const { planId } = params
2326
if (planId === null || planId === '' || planId === '{}') {
2427
throw new BadRequestError(`Invalid planId`)
2528
}
2629
const value = await findPlan(planId)
2730
if (value.objectId === planId) {
31+
// conditional read based on `if-none-match` header
2832
if (
2933
req.headers['if-none-match'] &&
30-
value.ETag == req.headers['if-none-match']
34+
value.ETag === req.headers['if-none-match']
3135
) {
3236
res.setHeader('ETag', value.ETag)
3337
const data = {
3438
message: 'Plan has not changed',
3539
plan: JSON.parse(value.plan),
3640
}
3741
notModifiedHandler(res, data)
38-
return
3942
} else {
4043
res.setHeader('ETag', value.ETag)
4144
const data = {
4245
message: 'Plan has changed',
4346
plan: JSON.parse(value.plan),
4447
}
4548
successHandler(res, data)
46-
return
4749
}
4850
} else {
4951
throw new ResourceNotFoundError(`Plan not found`)
@@ -64,20 +66,20 @@ const savePlan = async (req, res, next) => {
6466
try {
6567
if (validate(req.body)) {
6668
const value = await findPlan(req.body.objectId)
69+
logger.warn(`VALUE: ${value}`)
6770
if (value) {
6871
res.setHeader('ETag', value.ETag)
6972
const data = { message: 'Item already exists' }
7073
conflictHandler(res, data)
71-
return
7274
} else {
73-
const ETag = (await addPlan(req.body)).ETag
75+
const newPlan = await addPlan(req.body)
76+
const { ETag } = newPlan
7477
res.setHeader('ETag', ETag)
7578
const data = {
7679
message: 'Item added',
7780
ETag,
7881
}
7982
createHandler(res, data)
80-
return
8183
}
8284
} else {
8385
throw new BadRequestError(`Item is not valid`)
@@ -87,6 +89,44 @@ const savePlan = async (req, res, next) => {
8789
}
8890
}
8991

90-
const deletePlan = async (req, res, next) => {}
92+
const deletePlan = async (req, res, next) => {
93+
const { protocol, method, hostname, originalUrl, params } = req
94+
const headers = { ...req.headers }
95+
const metaData = { protocol, method, hostname, originalUrl, headers, params }
96+
logger.info(
97+
`Requesting ${method} ${protocol}://${hostname}${originalUrl}`,
98+
metaData
99+
)
100+
try {
101+
const { planId } = params
102+
if (planId === null || planId === '' || planId === '{}') {
103+
throw new BadRequestError(`Invalid planId`)
104+
}
105+
const value = await findPlan(planId)
106+
if (value.objectId === planId) {
107+
// conditional delete based on `if-match` header
108+
if (req.headers['if-match'] && value.ETag === req.headers['if-match']) {
109+
const data = {
110+
plan: JSON.parse(value.plan),
111+
}
112+
logger.info(`Item found`, JSON.parse(value.plan))
113+
if (deleteByPlanId(planId)) {
114+
logger.info(`Item deleted`, JSON.parse(value.plan))
115+
noContentHandler(res, data)
116+
} else {
117+
throw new InternalServerError(`Item not deleted`)
118+
}
119+
} else {
120+
throw new PreConditionFailedError(
121+
`ETag provided in header is not valid`
122+
)
123+
}
124+
} else {
125+
throw new ResourceNotFoundError(`Plan not found`)
126+
}
127+
} catch (err) {
128+
next(err)
129+
}
130+
}
91131

92132
export { getPlan, savePlan, deletePlan }

0 commit comments

Comments
 (0)