DONE
This commit is contained in:
118
.gitignore
vendored
Normal file
118
.gitignore
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
/y.js
|
||||
/package-lock.json
|
||||
34
helpers/http.js
Normal file
34
helpers/http.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
*
|
||||
* @param type
|
||||
* @param message
|
||||
* @returns {{code: string, type: null, message: null, status: string}}
|
||||
*/
|
||||
export const setResp400 = (type = null, message = null) => ({
|
||||
code: '400 Bad Request',
|
||||
status: 'Rejected',
|
||||
type: type,
|
||||
message: message,
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
* @param message
|
||||
* @returns {{code: string, message: null, status: string}}
|
||||
*/
|
||||
export const setResp403 = (message = null) => ({
|
||||
code: '403 Forbidden',
|
||||
status: 'Forbidden',
|
||||
message: message,
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {{code: string, type: string, message: string, status: string}}
|
||||
*/
|
||||
export const setResp500 = () => ({
|
||||
code: '500 Internal Server Error',
|
||||
status: 'Error',
|
||||
type: 'Critical error',
|
||||
message: 'Try reload the page in a while',
|
||||
})
|
||||
7
helpers/utils.js
Normal file
7
helpers/utils.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
*
|
||||
* @param reduced
|
||||
* @param input
|
||||
* @returns {{}}
|
||||
*/
|
||||
export const extractProps = (reduced, input) => Object.keys(reduced).reduce((a, b) => (a[b] = input[b], a), {})
|
||||
25
index.js
Normal file
25
index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import express from 'express'
|
||||
import router from './routes/router.js'
|
||||
|
||||
const PORT = 1976
|
||||
const app = express()
|
||||
|
||||
app.disable('etag')
|
||||
app.use((req, res, next) => {
|
||||
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate')
|
||||
res.header('Expires', '-1')
|
||||
res.header('Pragma', 'no-cache')
|
||||
next()
|
||||
})
|
||||
app.use(express.json())
|
||||
app.use('/', router)
|
||||
|
||||
const startServer = async () => {
|
||||
try {
|
||||
app.listen(PORT, () => console.log('Server is running on port: ' + PORT))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
await startServer()
|
||||
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "restfulapi",
|
||||
"version": "0.0.1",
|
||||
"description": "NodeJS application having a few ReSTFul endpoints",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.amok.space:8822/yevhen/restful-api-task.git"
|
||||
},
|
||||
"keywords": [
|
||||
"ReST API",
|
||||
"HTTP",
|
||||
"CRUD",
|
||||
"Todolist",
|
||||
"NodeJS"
|
||||
],
|
||||
"author": "Yevhen theAmok",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.13"
|
||||
}
|
||||
}
|
||||
290
repositories/data.json
Normal file
290
repositories/data.json
Normal file
@@ -0,0 +1,290 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Western carpet python",
|
||||
"category": "quote",
|
||||
"archive": false,
|
||||
"content": "I'll parse the online AI firewall, that should bandwidth the USB alarm!",
|
||||
"created_at": "2021-08-29T01:56:20.756Z",
|
||||
"updated_at": "2021-10-03T17:42:27.242Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Bornean pitviper",
|
||||
"category": "random_thought",
|
||||
"archive": false,
|
||||
"content": "If we program the microchip, we can get to the RSS bandwidth through the 1080p SMS matrix!",
|
||||
"created_at": "2021-07-15T09:37:47.284Z",
|
||||
"updated_at": "2021-08-06T01:33:55.383Z"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Cat snake",
|
||||
"category": "idea",
|
||||
"archive": false,
|
||||
"content": "I'll parse the haptic SMTP pixel, that should panel the AGP microchip!",
|
||||
"created_at": "2021-09-04T10:25:06.090Z",
|
||||
"updated_at": "2021-08-06T17:54:52.519Z"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Bluntnose viper",
|
||||
"category": "idea",
|
||||
"archive": true,
|
||||
"content": "The RSS port is down, synthesize the auxiliary interface so we can generate the GB array!",
|
||||
"created_at": "2021-08-31T02:48:43.764Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Scarlet kingsnake",
|
||||
"category": "idea",
|
||||
"archive": true,
|
||||
"content": "overriding the array won't do anything, we need to navigate the haptic SSL feed!",
|
||||
"created_at": "2021-09-17T10:21:26.174Z",
|
||||
"updated_at": "2021-07-07T18:14:54.247Z"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Green mamba",
|
||||
"category": "task",
|
||||
"archive": true,
|
||||
"content": "The HDD driver is down, generate the back-end panel so we can calculate the THX program!",
|
||||
"created_at": "2021-08-24T14:52:07.315Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "White-lipped tree viper",
|
||||
"category": "idea",
|
||||
"archive": true,
|
||||
"content": "Use the solid state SQL panel, then you can back up the redundant transmitter!",
|
||||
"created_at": "2021-07-22T16:08:28.099Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "Mangrove snake",
|
||||
"category": "idea",
|
||||
"archive": false,
|
||||
"content": "Try to synthesize the AGP feed, maybe it will copy the open-source hard drive!",
|
||||
"created_at": "2021-08-11T06:04:47.752Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title": "Sea snake",
|
||||
"category": "random_thought",
|
||||
"archive": true,
|
||||
"content": "Try to parse the THX protocol, maybe it will compress the bluetooth hard drive!",
|
||||
"created_at": "2021-08-20T22:52:54.944Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"title": "Cyclades blunt-nosed viper",
|
||||
"category": "random_thought",
|
||||
"archive": false,
|
||||
"content": "The SQL hard drive is down, navigate the redundant program so we can bypass the JSON matrix!",
|
||||
"created_at": "2021-09-15T10:43:02.014Z",
|
||||
"updated_at": "2021-08-07T07:06:51.050Z"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"title": "Western carpet python",
|
||||
"category": "quote",
|
||||
"archive": false,
|
||||
"content": "I'll parse the online AI firewall, that should bandwidth the USB alarm!",
|
||||
"created_at": "2021-09-10T15:30:21.732Z",
|
||||
"updated_at": "2021-10-14T22:37:50.128Z"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"title": "Indian python",
|
||||
"category": "task",
|
||||
"archive": false,
|
||||
"content": "Try to compress the SAS firewall, maybe it will override the solid state capacitor!",
|
||||
"created_at": "2021-07-11T07:26:31.840Z",
|
||||
"updated_at": "2021-09-04T16:11:18.316Z"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"title": "Dwarf pipe snake",
|
||||
"category": "random_thought",
|
||||
"archive": false,
|
||||
"content": "I'll compress the 1080p HDD bandwidth, that should hard drive the AI system!",
|
||||
"created_at": "2021-09-16T21:55:09.955Z",
|
||||
"updated_at": "2021-08-16T18:39:41.482Z"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"title": "Australian scrub python",
|
||||
"category": "task",
|
||||
"archive": true,
|
||||
"content": "Try to compress the TCP sensor, maybe it will input the bluetooth feed!",
|
||||
"created_at": "2021-09-20T04:20:47.013Z",
|
||||
"updated_at": "2021-07-02T21:43:14.712Z"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"title": "Baja California lyresnake",
|
||||
"category": "quote",
|
||||
"archive": true,
|
||||
"content": "I'll navigate the solid state SMTP monitor, that should application the SCSI driver!",
|
||||
"created_at": "2021-08-24T23:03:13.707Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"title": "Yellow anaconda",
|
||||
"category": "quote",
|
||||
"archive": false,
|
||||
"content": "The ADP port is down, copy the online protocol so we can reboot the SMTP microchip!",
|
||||
"created_at": "2021-07-18T08:59:58.608Z",
|
||||
"updated_at": "2021-09-02T01:06:46.953Z"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"title": "Brown water python",
|
||||
"category": "quote",
|
||||
"archive": true,
|
||||
"content": "Try to connect the JSON interface, maybe it will input the haptic application!",
|
||||
"created_at": "2021-09-22T02:27:11.017Z",
|
||||
"updated_at": "2021-07-21T17:23:13.134Z"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"title": "Red-bellied black snake",
|
||||
"category": "random_thought",
|
||||
"archive": true,
|
||||
"content": "I'll override the optical SAS program, that should feed the XML panel!",
|
||||
"created_at": "2021-07-06T02:49:11.318Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"title": "Green snake",
|
||||
"category": "quote",
|
||||
"archive": false,
|
||||
"content": "You can't generate the alarm without programming the online RSS pixel!",
|
||||
"created_at": "2021-07-19T06:42:56.112Z",
|
||||
"updated_at": "2021-09-25T11:48:31.274Z"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"title": "King rat snake",
|
||||
"category": "idea",
|
||||
"archive": false,
|
||||
"content": "The FTP pixel is down, generate the virtual capacitor so we can copy the JBOD application!",
|
||||
"created_at": "2021-09-26T09:32:25.827Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"title": "Northern white-lipped python",
|
||||
"category": "quote",
|
||||
"archive": false,
|
||||
"content": "Try to transmit the ADP matrix, maybe it will parse the bluetooth port!",
|
||||
"created_at": "2021-08-04T18:57:45.351Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"title": "King brown",
|
||||
"category": "task",
|
||||
"archive": true,
|
||||
"content": "I'll hack the haptic USB monitor, that should bus the JBOD transmitter!",
|
||||
"created_at": "2021-08-27T21:45:28.945Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"title": "Grass snake",
|
||||
"category": "random_thought",
|
||||
"archive": false,
|
||||
"content": "The GB array is down, generate the solid state port so we can copy the GB sensor!",
|
||||
"created_at": "2021-09-07T02:55:30.896Z",
|
||||
"updated_at": "2021-09-26T21:01:21.378Z"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"title": "Red-tailed bamboo pitviper",
|
||||
"category": "quote",
|
||||
"archive": false,
|
||||
"content": "I'll program the online SSL panel, that should protocol the RSS capacitor!",
|
||||
"created_at": "2021-07-23T10:13:48.120Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"title": "Indian tree viper",
|
||||
"category": "random_thought",
|
||||
"archive": false,
|
||||
"content": "You can't copy the pixel without bypassing the 1080p SMTP card!",
|
||||
"created_at": "2021-08-07T19:47:49.200Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"title": "Corn snake",
|
||||
"category": "random_thought",
|
||||
"archive": true,
|
||||
"content": "If we copy the capacitor, we can get to the USB sensor through the online RAM card!",
|
||||
"created_at": "2021-07-24T09:30:24.222Z",
|
||||
"updated_at": "2021-09-16T19:03:48.529Z"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"title": "Blanding's tree snake",
|
||||
"category": "random_thought",
|
||||
"archive": false,
|
||||
"content": "copying the transmitter won't do anything, we need to program the digital TCP driver!",
|
||||
"created_at": "2021-08-09T11:17:56.670Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"title": "Rattlesnake",
|
||||
"category": "quote",
|
||||
"archive": true,
|
||||
"content": "The RAM array is down, calculate the 1080p sensor so we can hack the JBOD bus!",
|
||||
"created_at": "2021-09-01T22:55:33.156Z",
|
||||
"updated_at": "2021-07-12T11:28:04.735Z"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"title": "Khasi Hills keelback",
|
||||
"category": "idea",
|
||||
"archive": true,
|
||||
"content": "If we parse the driver, we can get to the HDD port through the solid state CSS port!",
|
||||
"created_at": "2021-07-28T15:50:20.241Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"title": "Red spitting cobra",
|
||||
"category": "task",
|
||||
"archive": true,
|
||||
"content": "The AI circuit is down, synthesize the auxiliary port so we can generate the JBOD panel!",
|
||||
"created_at": "2021-07-17T00:35:58.760Z",
|
||||
"updated_at": "2021-10-06T11:31:10.150Z"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"title": "Southern Indonesian spitting cobra",
|
||||
"category": "random_thought",
|
||||
"archive": false,
|
||||
"content": "If we hack the bus, we can get to the TCP capacitor through the 1080p CSS transmitter!",
|
||||
"created_at": "2021-08-17T12:04:24.277Z",
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"title": "Cat snake",
|
||||
"category": "task",
|
||||
"archive": false,
|
||||
"content": "bypassing the system won't do anything, we need to quantify the open-source RAM matrix!",
|
||||
"created_at": "2021-10-14T13:03:11.921Z",
|
||||
"updated_at": null
|
||||
}
|
||||
]
|
||||
24
repositories/schema.js
Normal file
24
repositories/schema.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { object, boolean, date, number, string } from 'yup'
|
||||
|
||||
export const webFields = { title: null, category: null, archive: null, content: null }
|
||||
export const idOnlySchema = number().integer().positive().min(1).max(1024).required()
|
||||
|
||||
const initialSchema = {
|
||||
updated_at: string().nullable().default(null),
|
||||
created_at: date().default(() => new Date()),
|
||||
content: string().ensure().max(1024),
|
||||
archive: boolean().nullable().default(false),
|
||||
category: string().matches(/^(random_thought|idea|task|quote)$/).required(),
|
||||
title: string().trim().min(1).max(127).required(),
|
||||
id: string().nullable().default(null),
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine exportable schemas using previously created chunks along with adding a new ones dynamically
|
||||
*/
|
||||
|
||||
export const newNoteSchema = object(initialSchema)
|
||||
|
||||
initialSchema.id = idOnlySchema
|
||||
initialSchema.updated_at = date().default(() => new Date())
|
||||
export const updateNoteSchema = object(initialSchema)
|
||||
17
routes/methods/delete.js
Normal file
17
routes/methods/delete.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { setResp500 } from '../../helpers/http.js'
|
||||
import NotesService from '../../services/NotesService.js'
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {NotesService}
|
||||
*/
|
||||
const db = new NotesService()
|
||||
|
||||
export const deleteNote = async (req, res) => {
|
||||
try{
|
||||
const data = await db.deleteNote(req.validatedData)
|
||||
res.status(data.code || 200).json(data)
|
||||
}catch (e) {
|
||||
res.status(500).json(setResp500())
|
||||
}
|
||||
}
|
||||
65
routes/methods/get.js
Normal file
65
routes/methods/get.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { setResp403, setResp500 } from '../../helpers/http.js'
|
||||
import NotesService from '../../services/NotesService.js'
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {NotesService}
|
||||
*/
|
||||
const db = new NotesService()
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const setForbidden = async (req, res) => {
|
||||
const ip = req.headers['x-real-ip'] || req.connection.remoteAddress
|
||||
res.status(403).json(setResp403(`You are not allowed to access the resource. Your IP is ${ip}`))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const getAllNotes = async (req, res) => {
|
||||
try{
|
||||
const data = await db.getNotes
|
||||
res.status(data.code || 200).json(data)
|
||||
}catch (e) {
|
||||
res.status(500).json(setResp500())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const getSingleNote = async (req, res) => {
|
||||
|
||||
try{
|
||||
const data = await db.getSingle(req.validatedData)
|
||||
res.status(data.code || 200).json(data)
|
||||
}catch (e) {
|
||||
res.status(500).json(setResp500())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const getStats = (req, res) => {
|
||||
try{
|
||||
const data = db.getStats
|
||||
res.status(data.code || 200).json( data )
|
||||
}catch (e) {
|
||||
res.status(500).json(setResp500())
|
||||
}
|
||||
}
|
||||
18
routes/methods/patch.js
Normal file
18
routes/methods/patch.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { setResp500 } from '../../helpers/http.js'
|
||||
import NotesService from '../../services/NotesService.js'
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {NotesService}
|
||||
*/
|
||||
const db = new NotesService()
|
||||
|
||||
export const editNote = async (req, res) => {
|
||||
|
||||
try{
|
||||
const data = await db.updateNote(req.validatedData)
|
||||
res.status(data.code || 200).json(data)
|
||||
}catch (e) {
|
||||
res.status(500).json(setResp500())
|
||||
}
|
||||
}
|
||||
18
routes/methods/post.js
Normal file
18
routes/methods/post.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { setResp500 } from '../../helpers/http.js'
|
||||
import NotesService from '../../services/NotesService.js'
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {NotesService}
|
||||
*/
|
||||
const db = new NotesService()
|
||||
|
||||
export const addNote = async (req, res) => {
|
||||
|
||||
try{
|
||||
const data = await db.insertNote(req.validatedData)
|
||||
res.status(data.code || 200).json(data)
|
||||
}catch (e) {
|
||||
res.status(500).json(setResp500())
|
||||
}
|
||||
}
|
||||
20
routes/router.js
Normal file
20
routes/router.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Router } from 'express'
|
||||
import { addNote } from './methods/post.js'
|
||||
import { getAllNotes, getSingleNote, getStats, setForbidden } from './methods/get.js'
|
||||
import { editNote } from './methods/patch.js'
|
||||
import { deleteNote } from './methods/delete.js'
|
||||
import { id as validate } from '../services/ValidationService.js'
|
||||
import { idOnlySchema, newNoteSchema, updateNoteSchema } from '../repositories/schema.js'
|
||||
|
||||
const router = new Router()
|
||||
|
||||
router.get('/notes(\/|)$/', getAllNotes)
|
||||
router.get('/notes\/stats(\/|)$/', getStats)
|
||||
router.get('/notes/:id([0-9]{1,4})$/', validate( idOnlySchema ), getSingleNote) // e.g. also notes with `0x1a` => id = 26 will be affected
|
||||
router.get('*', setForbidden)
|
||||
|
||||
router.post('/notes(\/|)$/', validate( newNoteSchema ), addNote)
|
||||
router.patch('/notes/:id([0-9]{1,4})$/', validate( updateNoteSchema ), editNote)
|
||||
router.delete('/notes/:id([0-9]{1,4})$/', validate( idOnlySchema ), deleteNote)
|
||||
|
||||
export default router
|
||||
182
services/NotesService.js
Normal file
182
services/NotesService.js
Normal file
@@ -0,0 +1,182 @@
|
||||
//const data = require('../repositories/data.json')
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
class NotesService {
|
||||
dbPath
|
||||
db
|
||||
limit
|
||||
|
||||
constructor () {
|
||||
|
||||
this.dbPath = path.resolve('./repositories/data.json')
|
||||
|
||||
try {
|
||||
const data = fs.readFileSync(this.dbPath, 'utf8')
|
||||
this.db = JSON.parse(data || '[]')
|
||||
} catch (e) {
|
||||
this.db = []
|
||||
}
|
||||
|
||||
this.limit = 7
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* USE AT FINAL STAGE BEFORE RETURN FROM THE METHOD TO AVOID HELTER SKELTER IN THE RESULT
|
||||
*
|
||||
* @param result
|
||||
* @param query
|
||||
* @returns {*|{result: null, data: *[], query: null, message: string, type: string, status: string}}
|
||||
* @private
|
||||
*/
|
||||
_composeResponse (result, query) {
|
||||
|
||||
const response = {
|
||||
code: 200,
|
||||
status: 'Accepted',
|
||||
message: `The database contains ${this.db.length} notes`,
|
||||
result: `Matched ${result.length} note(s)`,
|
||||
query: query,
|
||||
type: 'Completed',
|
||||
data: [],
|
||||
}
|
||||
|
||||
if (result.length === 1) {
|
||||
response.data = result[0]
|
||||
} else if (result.length > 1) {
|
||||
response.data = result
|
||||
} else {
|
||||
response.code = 404
|
||||
response.type = 'Warning'
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id
|
||||
* @returns {*|{result: null, data: *[], query: null, message: string, type: string, status: string}}
|
||||
*/
|
||||
getSingle (id) {
|
||||
//, returned LIMIT ${this.limit} ORDER BY id DESC
|
||||
const note = this.db.filter(el => el.id === +id)
|
||||
return this._composeResponse(note, `id = ${id} LIMIT 1`)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {*|{result: null, data: *[], query: null, message: string, type: string, status: string}}
|
||||
*/
|
||||
get getNotes () {
|
||||
const notes = this.db.sort((a, b) => new Date(a.created_at) > new Date(b.created_at) ? -1 : 1)
|
||||
return this._composeResponse(notes.slice(0, this.limit), `ORDER BY created_at DESC LIMIT ` + this.limit)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id
|
||||
* @returns {Promise<*|{result: null, data: *[], query: null, message: string, type: string, status: string}>}
|
||||
*/
|
||||
async deleteNote (id) {
|
||||
const deletedNote = this.db.filter(note => note.id === +id)
|
||||
|
||||
if (deletedNote.length === 1) {
|
||||
this.db = this.db.filter(note => note.id !== +id)
|
||||
await this.saveDatabase()
|
||||
}
|
||||
|
||||
return this._composeResponse(deletedNote, `DELETED FROM db WHERE id = ${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data
|
||||
* @returns {Promise<*|{result: null, data: *[], query: null, message: string, type: string, status: string}>}
|
||||
*/
|
||||
async insertNote( data ){
|
||||
|
||||
const notes = this.db.sort((a, b) => new Date(a.id) > new Date(b.id) ? -1 : 1)
|
||||
data.id = ( notes.length >= 1 ) ? notes[0].id + 1 : 1
|
||||
this.db.push(data)
|
||||
await this.saveDatabase()
|
||||
|
||||
return this._composeResponse([data], `INSERT INTO database SET id = ${data.id} (AI)`)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data
|
||||
* @returns {Promise<*|{result: null, data: *[], query: null, message: string, type: string, status: string}>}
|
||||
*/
|
||||
async updateNote( data ){
|
||||
|
||||
const noteExists = this.db.find(note => note.id === +data.id)
|
||||
|
||||
if( noteExists ){
|
||||
noteExists.title = data.title
|
||||
noteExists.content = data.content
|
||||
noteExists.category = data.category
|
||||
noteExists.archive = data.archive
|
||||
noteExists.updated_at = data.updated_at
|
||||
|
||||
await this.saveDatabase()
|
||||
return this._composeResponse([noteExists], `UPDATED WHERE id = ${data.id}`)
|
||||
}
|
||||
|
||||
return this._composeResponse([], `UPDATED WHERE id = ${data.id}`)
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async saveDatabase () {
|
||||
try {
|
||||
await fs.promises.writeFile(this.dbPath, JSON.stringify(this.db, null, 2), { encoding: 'utf-8' })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {{result: string, data: {}, query: string, message: string, status: string}}
|
||||
*/
|
||||
get getStats () {
|
||||
|
||||
const stats = {}
|
||||
const cats = [...new Set(this.db.map(item => item.category))]
|
||||
|
||||
if (typeof cats[0] !== 'undefined') {
|
||||
const isArchive = this.db.map(item => {
|
||||
const container = {}
|
||||
container[item.category] = item.archive
|
||||
return container
|
||||
})
|
||||
|
||||
cats.forEach((cat, i) => {
|
||||
stats[cat] = {
|
||||
total: this.db.filter(el => el.category === cat).length,
|
||||
archived: isArchive.filter(el => el[cat] === true).length,
|
||||
active: isArchive.filter(el => el[cat] === false).length,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
status: 'Accepted',
|
||||
message: `The database contains ${this.db.length} notes`,
|
||||
result: `Aggregated data statistics`,
|
||||
query: `GROUP BY categories, (active|archived)`,
|
||||
type: 'Completed',
|
||||
data: stats,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NotesService
|
||||
32
services/ValidationService.js
Normal file
32
services/ValidationService.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { setResp400 } from '../helpers/http.js'
|
||||
import { extractProps } from '../helpers/utils.js'
|
||||
import {webFields} from '../repositories/schema.js'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param schema
|
||||
* @returns {(function(*, *, *): Promise<*|undefined>)|*}
|
||||
*/
|
||||
export const id = (schema) => async (req, res, next) => {
|
||||
|
||||
let objToValidate
|
||||
|
||||
if (req.method === 'POST' || req.method === 'PATCH') {
|
||||
|
||||
if (req.method === 'PATCH'){
|
||||
webFields.id = null
|
||||
}
|
||||
|
||||
objToValidate = extractProps(webFields, req.body)
|
||||
}else{
|
||||
const { id } = req.params
|
||||
objToValidate = id
|
||||
}
|
||||
|
||||
try {
|
||||
req.validatedData = await schema.validate(objToValidate)
|
||||
next()
|
||||
} catch (error) {
|
||||
return res.status(400).json(setResp400(error.name || null, error.message || null ))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user