This commit is contained in:
Ulises Gascón
2024-02-07 12:29:54 +01:00
committed by Ulises Gascon
parent 91ba1f5054
commit 9dd233c371
143 changed files with 68070 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
{ "presets": ["@babel/preset-env"] }
+1
View File
@@ -0,0 +1 @@
20.11.0
+19
View File
@@ -0,0 +1,19 @@
import mongoose from 'mongoose'
mongoose.set('toJSON', {
virtuals: true,
transform: (doc, converted) => {
delete converted._id
delete converted.__v
}
})
const whisperSchema = new mongoose.Schema({
message: String
})
const Whisper = mongoose.model('Whisper', whisperSchema)
export {
Whisper
}
+11
View File
@@ -0,0 +1,11 @@
version: '3.8'
services:
database:
container_name: whispering-database
image: mongo:7.0
ports:
- '27017:27017'
volumes:
- db-storage:/data/db
volumes:
db-storage:
+14
View File
@@ -0,0 +1,14 @@
import { app } from './server.js'
import mongoose from 'mongoose'
const port = process.env.PORT
try {
await mongoose.connect(process.env.MONGODB_URI)
console.log('Connected to MongoDB')
app.listen(port, () => {
console.log(`Running in http://localhost:${port}`)
})
} catch (error) {
console.error(error)
}
+6
View File
@@ -0,0 +1,6 @@
export default {
modulePathIgnorePatterns: ['<rootDir>/node_test/'],
coveragePathIgnorePatterns: [
'<rootDir>/tests/'
]
}
+10180
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
{
"name": "nodejs-for-beginners",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node --require dotenv/config index.js",
"test": "jest --setupFiles dotenv/config",
"test:coverage": "jest --coverage --setupFiles dotenv/config",
"lint": "standard",
"lint:fix": "standard --fix",
"infra:start": "docker-compose up -d --build",
"infra:stop": "docker-compose down --remove-orphans"
},
"author": "",
"license": "ISC",
"standard": {
"env": [
"jest"
],
"ignore": [
"public/*.js"
]
},
"dependencies": {
"body-parser": "^1.20.2",
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"express": "^4.18.3",
"mongoose": "7.4"
},
"devDependencies": {
"@babel/preset-env": "^7.24.1",
"jest": "^29.7.0",
"standard": "^17.1.0",
"supertest": "^6.3.3"
}
}
+66
View File
@@ -0,0 +1,66 @@
// == Selectors ==
const whispers = document.getElementById('whispers')
const whisperCreateButton = document.getElementById('whisper-create')
// == Event Listeners ==
whispers.addEventListener('click', event => {
if(event.target.tagName === 'BUTTON') {
const button = event.target
const article = event.target.closest('article')
const action = button.dataset.action
const id = article.dataset.id
const message = article.querySelector('p').innerText
if(action === 'edit') requestUserEdit(id, message)
if(action === 'delete') requestUserDelete(id)
}
})
whisperCreateButton.addEventListener('click', event => {
const message = prompt("What's your whisper?")
if(message) {
createWhisper(message)
.then(refreshAllUI)
}
})
// === Functions ==
// -- API --
const fetchAllWhispers = () => fetch('http://localhost:3000/api/v1/whisper').then((response) => response.json())
const deleteWhisper = (id) => fetch(`http://localhost:3000/api/v1/whisper/${id}`, { method: 'DELETE' })
const updateWhisper = (id, message) => fetch(`http://localhost:3000/api/v1/whisper/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) })
const createWhisper = (message) => fetch('http://localhost:3000/api/v1/whisper', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) })
// -- UI --
const refreshWhispers = data => whispers.innerHTML = data
.reverse()
.map(whisper => {
return `
<article data-id="${whisper.id}">
<div class="actions">
<button data-action="edit">✏️</button>
<button data-action="delete">❌</button>
</div>
<p>${whisper.message}</p>
</article>`
}).join('')
const refreshAllUI = () => fetchAllWhispers().then(refreshWhispers)
const requestUserEdit = (id, message) => {
const newMessage = prompt("Edit the Whisper", message);
if(newMessage && newMessage !== message) {
updateWhisper(id, newMessage)
.then(refreshAllUI)
}
console.log("Request User Edit", id, message)
}
const requestUserDelete = (id) => {
const confirmation = confirm("Are you sure you want to delete this whisper?");
if(confirmation) {
deleteWhisper(id)
.then(refreshAllUI)
}
}
// == Initialization ==
refreshAllUI()
+25
View File
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Whispering | Home</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav class="menu">
<ul>
<li><a href="/about">About</a></li>
<li><a href="#">Whispering</a></li>
</ul>
</nav>
<main>
<button type="button" class="block" id="whisper-create">Spread a whisper 🤭</button>
<div id="whispers"></div>
</main>
<script src="app.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+110
View File
@@ -0,0 +1,110 @@
html,
body {
font-family: sans-serif;
margin: 0;
padding: 0;
background-color: #fff;
}
.menu ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
height: 50px;
background-color: #343e8b;
}
.menu li {
float: right;
margin-right: 4%;
font-weight: bold;
line-height: 50px;
}
.menu li:last-child {
float: left;
margin-left: 4%;
}
.menu a {
color: white;
text-decoration: none;
}
main {
max-width: 600px;
margin: 0 auto;
padding: 20px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
img {
max-width: 100%;
height: auto;
}
figcaption {
font-size: 0.8em;
}
h1 {
font-size: 2em;
line-height: 1.5em;
}
h2 {
font-size: 1.5em;
line-height: 1.5em;
}
p {
font-size: 1.2em;
line-height: 1.5em;
}
#whispers {
border-top: 1px solid #ccc;
margin-top: 20px;
}
article {
background: #f8f6f6;
border: 2px solid #343e8b;
margin: 1.5em 10px;
padding: 0 10px;
border-radius: 5px;
display: flex;
flex-direction: column;
}
article p {
display: inline;
}
article .actions {
margin-top: 10px;
display: flex;
justify-content: flex-end;
}
article .actions > button {
cursor: pointer;
margin: 3px;
border: none;
background-color: #f8f6f6;
}
.block {
display: block;
width: 60%;
border: none;
background-color: #343e8b;
padding: 0.5em 10px;
font-size: 16px;
cursor: pointer;
text-align: center;
color: white;
border-radius: 5px;
}
+70
View File
@@ -0,0 +1,70 @@
import express from 'express'
import bodyParser from 'body-parser'
import { getAll, getById, create, updateById, deleteById } from './store.js'
const app = express()
app.use(express.static('public'))
app.use(bodyParser.json())
app.set('view engine', 'ejs')
app.get('/about', async (req, res) => {
const whispers = await getAll()
res.render('about', { whispers })
})
app.get('/api/v1/whisper', async (req, res) => {
const whispers = await getAll()
res.json(whispers)
})
app.get('/api/v1/whisper/:id', async (req, res) => {
const id = req.params.id
const whisper = await getById(id)
if (!whisper) {
res.sendStatus(404)
} else {
res.json(whisper)
}
})
app.post('/api/v1/whisper', async (req, res) => {
const { message } = req.body
if (!message) {
res.sendStatus(400)
} else {
const whisper = await create(message)
res.status(201).json(whisper)
}
})
app.put('/api/v1/whisper/:id', async (req, res) => {
const { message } = req.body
const id = req.params.id
if (!message) {
res.sendStatus(400)
} else {
const whisper = await getById(id)
if (!whisper) {
res.sendStatus(404)
} else {
await updateById(id, message)
res.sendStatus(200)
}
}
})
app.delete('/api/v1/whisper/:id', async (req, res) => {
const id = req.params.id
const whisper = await getById(id)
if (!whisper) {
res.sendStatus(404)
return
}
await deleteById(id)
res.sendStatus(200)
})
export { app }
+15
View File
@@ -0,0 +1,15 @@
import {
Whisper
} from './database.js'
const getAll = () => Whisper.find()
const getById = id => Whisper.findById({ _id: id })
const create = async (message) => {
const whisper = new Whisper({ message })
await whisper.save()
return whisper
}
const updateById = async (id, message) => Whisper.findOneAndUpdate({ _id: id }, { message }, { new: false })
const deleteById = async (id) => Whisper.deleteOne({ _id: id })
export { getAll, getById, create, updateById, deleteById }
+122
View File
@@ -0,0 +1,122 @@
import supertest from 'supertest'
import { app } from '../server'
import { getById } from '../store.js'
import { restoreDb, populateDb, getFixtures, ensureDbConnection, normalize, closeDbConnection } from './utils.js'
let whispers
let inventedId
let existingId
describe('Server', () => {
beforeAll(ensureDbConnection)
beforeEach(async () => {
await restoreDb()
await populateDb(whispers)
const fixtures = await getFixtures()
whispers = fixtures.whispers
inventedId = fixtures.inventedId
existingId = fixtures.existingId
})
afterAll(closeDbConnection)
describe('GET /about', () => {
it('Should return a 200 with the total whispers in the platform', async () => {
const response = await supertest(app).get('/about')
expect(response.status).toBe(200)
expect(response.text).toContain(`Currently there are ${whispers.length} whispers available`)
})
})
describe('GET /api/v1/whisper', () => {
it("Should return an empty array when there's no data", async () => {
await restoreDb() // empty the db
const response = await supertest(app).get('/api/v1/whisper')
expect(response.status).toBe(200)
expect(response.body).toEqual([])
})
it('Should return all the whispers', async () => {
const response = await supertest(app).get('/api/v1/whisper')
expect(response.status).toBe(200)
expect(response.body).toEqual(whispers)
})
})
describe('GET /api/v1/whisper/:id', () => {
it("Should return a 404 when the whisper doesn't exist", async () => {
const response = await supertest(app).get(`/api/v1/whisper/${inventedId}`)
expect(response.status).toBe(404)
})
it('Should return a whisper details', async () => {
const response = await supertest(app).get(`/api/v1/whisper/${existingId}`)
expect(response.status).toBe(200)
expect(response.body).toEqual(whispers.find(w => w.id === existingId))
})
})
describe('POST /api/v1/whisper', () => {
it('Should return a 400 when the body is empty', async () => {
const response = await supertest(app)
.post('/api/v1/whisper')
.send({})
expect(response.status).toBe(400)
})
it('Should return a 400 when the body is invalid', async () => {
const response = await supertest(app)
.post('/api/v1/whisper')
.send({ invented: 'This is a new whisper' })
expect(response.status).toBe(400)
})
it('Should return a 201 when the whisper is created', async () => {
const newWhisper = { message: 'This is a new whisper' }
const response = await supertest(app)
.post('/api/v1/whisper')
.send({ message: newWhisper.message })
expect(response.status).toBe(201)
expect(response.body.message).toEqual(newWhisper.message)
// Database changes
const storedWhisper = await getById(response.body.id)
expect(normalize(storedWhisper).message).toStrictEqual(newWhisper.message)
})
})
describe('PUT /api/v1/whisper/:id', () => {
it('Should return a 400 when the body is empty', async () => {
const response = await supertest(app)
.put(`/api/v1/whisper/${existingId}`)
.send({})
expect(response.status).toBe(400)
})
it('Should return a 400 when the body is invalid', async () => {
const response = await supertest(app)
.put(`/api/v1/whisper/${existingId}`)
.send({ invented: 'This a new field' })
expect(response.status).toBe(400)
})
it("Should return a 404 when the whisper doesn't exist", async () => {
const response = await supertest(app)
.put(`/api/v1/whisper/${inventedId}`)
.send({ message: 'Whisper updated' })
expect(response.status).toBe(404)
})
it('Should return a 200 when the whisper is updated', async () => {
const response = await supertest(app)
.put(`/api/v1/whisper/${existingId}`)
.send({ message: 'Whisper updated' })
expect(response.status).toBe(200)
// Database changes
const storedWhisper = await getById(existingId)
expect(normalize(storedWhisper)).toStrictEqual({ id: existingId, message: 'Whisper updated' })
})
})
describe('DELETE /api/v1/whisper/:id', () => {
it("Should return a 404 when the whisper doesn't exist", async () => {
const response = await supertest(app).delete(`/api/v1/whisper/${inventedId}`)
expect(response.status).toBe(404)
})
it('Should return a 200 when the whisper is deleted', async () => {
const response = await supertest(app).delete(`/api/v1/whisper/${existingId}`)
expect(response.status).toBe(200)
// Database changes
const storedWhisper = await getById(existingId)
expect(storedWhisper).toBe(null)
})
})
})
+32
View File
@@ -0,0 +1,32 @@
import mongoose from 'mongoose'
import {
Whisper
} from '../database.js'
const ensureDbConnection = async () => {
try {
if (mongoose.connection.readyState !== 1) {
await mongoose.connect(process.env.MONGODB_URI)
}
} catch (error) {
console.error('Error connecting to the database:', error)
throw error
}
}
const closeDbConnection = async () => {
if (mongoose.connection.readyState === 1) {
await mongoose.disconnect()
}
}
const restoreDb = () => Whisper.deleteMany({})
const populateDb = () => Whisper.insertMany([{ message: 'test' }, { message: 'hello world' }])
const getFixtures = async () => {
const data = await Whisper.find()
const whispers = JSON.parse(JSON.stringify(data))
const inventedId = '64e0e5c75a4a3c715b7c1074'
const existingId = data[0].id
return { inventedId, existingId, whispers }
}
const normalize = (data) => JSON.parse(JSON.stringify(data))
export { restoreDb, populateDb, getFixtures, ensureDbConnection, normalize, closeDbConnection }
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Whispering | Home</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav class="menu">
<ul>
<li><a href="/">Whispering</a></li>
</ul>
</nav>
<main>
<h1>Welcome to Whispering!</h1>
<figure>
<img src="people.jpg" alt="three people sitting at the table laughing together" />
<figcaption>Photo by <a href="https://unsplash.com/photos/g1Kr4Ozfoac">Brooke Cagle</a> from <a
href="https://unsplash.com/">Unsplash</a></figcaption>
</figure>
<h2>What is Whispering?</h2>
<p>Whispering is a microblogging platform that allows you to share your thoughts with the world and learn
Node.js on the way.</p>
<h2>Community live ⚡️</h2>
<p>Currently there are <%= whispers.length %> whispers available</p>
</main>
</body>
</html>