This commit is contained in:
2021-10-15 02:00:40 +03:00
parent 74316481f1
commit ade92e0493
15 changed files with 884 additions and 0 deletions

118
.gitignore vendored Normal file
View 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

View File

@@ -0,0 +1,3 @@
# Simple ReST API
See code for details...

34
helpers/http.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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 ))
}
}