Built at: 2024-10-16T09:55:25.150Z Skip to content

part3d

Validation and ESLint

Define validation rules in schema in models/note.js

models/note.js
const noteSchema = new mongoose.Schema({
content: {
type: String,
minLength: 5,
required: true
},
important: Boolean
})

catch error in create note:

index.js
app.post('/api/notes', (request, response, next) => {
const body = request.body
const note = new Note({
content: body.content,
important: body.important || false,
})
note.save()
.then(savedNote => {
response.json(savedNote)
})
.catch(error => next(error))
})

Expand the error handler to deal with these validation errors:

index.js
const errorHandler = (error, request, response, next) => {
console.error(error.message)
if (error.name === 'CastError') {
return response.status(400).send({ error: 'malformatted id' })
} else if (error.name === 'ValidationError') {
return response.status(400).json({ error: error.message })
}
next(error)
}

validata in update note:

index.js
app.put('/api/notes/:id', (request, response, next) => {
const { content, important } = request.body
Note.findByIdAndUpdate(
request.params.id,
{ content, important },
{ new: true, runValidators: true, context: 'query' }
)
.then(updatedNote => {
response.json(updatedNote)
})
.catch(error => next(error))
})

deploy to production

fly.io set screts:

fly secrets set MONGODB_URI='mongodb+srv://fullstack:thepasswordishere@cluster0.o1opl.mongodb.net/noteApp?retryWrites=true&w=majority'

Allow all IP addresses in MongoDB Atlas. to whitelist the fly.io app’s IP address in MongoDB Atlas.

Exercises 3.19.-3.21.

3.19*: Phonebook database, step 7

Expand the validation so that the name stored in the database has to be at least three characters long.

You can display the default error message returned by Mongoose, even though they are not as readable as they could be

ref:
enable validators on update operations.
https://mongoosejs.com/docs/validation.html

3.20*: Phonebook database, step 8

A phone number must:

have length of 8 or more be formed of two parts that are separated by -, the first part has two or three numbers and the second part also consists of numbers

eg. 09-1234556 and 040-22334455 are valid phone numbers eg. 1234556, 1-22334455 and 10-22-334455 are invalid

ref:
Custom https://mongoosejs.com/docs/validation.html#custom-validators

3.21 Deploying the database backend to production

My solutions

My solutions:
(Please complete your own solutions before click here.)

3.19 validator: name length at least 3

models/person.cjs
const mongoose = require('mongoose')
mongoose.set('strictQuery', false)
// const password = process.argv[2]
// const url = `mongodb://fullstackopencn:${password}@fullstackopencn-shard-00-00.aozyn.mongodb.net:27017,fullstackopencn-shard-00-01.aozyn.mongodb.net:27017,fullstackopencn-shard-00-02.aozyn.mongodb.net:27017/phonebookApp?ssl=true&replicaSet=atlas-10eei6-shard-0&authSource=admin&retryWrites=true&w=majority&appName=fullstackopencn`
const url = process.env.MONGODB_URI
console.log('connecting to', url)
// mongoose.connect(url)
mongoose.connect(url)
.then(result => {
console.log('connected to MongoDB')
})
.catch(error => {
console.log('error connecting to MongoDB:', error.message)
})
const phonebookSchema = new mongoose.Schema({
// name: String,
name: {
type: String,
minLength: 3,
required: true
},
number: String,
})
// const Person = mongoose.model('Person', phonebookSchema)
phonebookSchema.set('toJSON', {
transform: (document, returnedObject) => {
returnedObject.id = returnedObject._id.toString()
delete returnedObject._id
delete returnedObject.__v
}
})
module.exports = mongoose.model('Person', phonebookSchema)
index.js
// revised post /api/persons
import { createRequire } from 'module';
const require = createRequire(import.meta.url)
const axios = require("axios");
const morgan = require("morgan");
const _ = require("lodash");
require('dotenv').config()
const express = require("express");
const cors = require("cors");
const app = express();
const Person = require('./models/person.cjs')
morgan.token('postBody', function getPostBody(req) {
// console.log(req.method)
// console.log(JSON.stringify(req.body) === JSON.stringify({}))
// if (JSON.stringify(req.body) !== JSON.stringify({})) {
if (req.method.toLowerCase() === "post".toLowerCase()) {
return JSON.stringify(req.body)
}
return null
})
app.use(cors());
app.use(express.static('dist'))
app.use(express.json());
// app.use(morgan("tiny"))
app.use(morgan(':method :url :status :res[content-length] - :response-time ms :postBody'))
let persons = [
{
id: "1",
name: "Arto Hellas",
number: "040-123456",
},
{
id: "2",
name: "Ada Lovelace",
number: "39-44-5323523",
},
{
id: "3",
name: "Dan Abramov",
number: "12-43-234345",
},
{
id: "4",
name: "Mary Poppendieck",
number: "39-23-6423122",
},
];
// app.get("/api/persons", (request, response) => {
// response.json(persons);
// });
app.get('/api/persons', (request, response) => {
Person.find({}).then(persons => {
response.json(persons)
})
})
app.get("/info", (request, response, next) => {
let respPersons;
// axios.get("http://localhost:3001/api/persons")
// .then(resp => {
// // console.log(resp)
// respPersons = resp.data
// // console.log(respPersons)
// response.send(`
// Phonebook has info for ${respPersons.length} ${respPersons.length === 1 ? "person" : "people"} <br />
// ${new Date().toString()}
// `)
// })
// .catch(error => next(error))
Person.find({})
.then(respPersons => {
// response.json(respPersons)
// console.log(respPersons)
response.send(`
Phonebook has info for ${respPersons.length} ${respPersons.length === 1 ? "person" : "people"} <br />
${new Date().toString()}
`)
})
.catch(error => next(error))
})
// app.get("/api/persons/:id", (request, response) => {
// const id = request.params.id;
// const person = persons.find((person) => person.id === id)
// if (person) {
// response.json(person)
// } else {
// response.status(404).end()
// }
// })
app.get('/api/persons/:id', (request, response, next) => {
console.log("getting /api/persons/:id, id: ", request.params.id)
// try {
// Person.findById(request.params.id).then(person => {
// response.json(person)
// })
// } catch (error) {
// console.log(error)
// }
Person.findById(request.params.id)
.then(person => {
if (person) {
response.json(person)
} else {
response.status(404).end()
}
})
.catch(error => next(error))
})
app.delete("/api/persons/:id", (request, response, next) => {
// const id = request.params.id;
// persons = persons.filter((person) => person.id !== id)
// response.status(204).end()
Person.findByIdAndDelete(request.params.id)
.then(result => {
response.status(204).end()
})
.catch(error => next(error))
})
app.put("/api/persons/:id", (request, response, next) => {
// const body = request.body
const { name, number } = request.body
// const person = {
// name: body.name,
// number: body.number,
// }
// Person.findByIdAndUpdate(request.params.id, person, { new: true })
Person.findByIdAndUpdate(
request.params.id,
{ name, number },
{ new: true, runValidators: true, context: 'query' }
)
.then(updatedPerson => {
response.json(updatedPerson)
})
.catch(error => next(error))
})
// const generateId = () => {
// const maxId = persons.length > 0 ? Math.max(...persons.map((p) => Number(p.id))) : 0
// return String(maxId + 1)
// }
// app.post("/api/persons", (request, response) => {
// const body = request.body;
// if (!body.name || !body.number) {
// return response.status(400).json({
// error: "person name or number missing",
// })
// }
// const personsNameArray = persons.map((p) => p.name)
// if (_.includes(personsNameArray, body.name)) {
// return response.status(400).json({
// error: "name must be unique"
// })
// }
// const person = {
// name: body.name,
// number: body.number,
// id: generateId(),
// }
// persons = persons.concat(person)
// response.json(person)
// })
app.post('/api/persons', async (request, response, next) => {
const body = request.body
if (body.name === undefined || body.number === undefined) {
return response.status(400).json({ error: 'name or number missing' })
}
const existPerson = await Person.findOne({ 'name': body.name }, 'name number id')
if (existPerson) {
console.log(`Person with name ${body.name} exist, updating the number...`)
// Update the existing person with the new information
const updatedPerson = await Person.findByIdAndUpdate(
existPerson.id,
body,
{ new: true, runValidators: true, overwrit: true }
);
return response.status(200).json(updatedPerson)
}
const person = new Person({
name: body.name,
number: body.number,
})
person.save()
.then(savedPerson => {
response.json(savedPerson)
})
.catch(error => next(error))
})
const unknownEndpoint = (request, response) => {
response.status(404).send({ error: 'unknown endpoint' })
}
// handler of requests with unknown endpoint
app.use(unknownEndpoint)
const errorHandler = (error, request, response, next) => {
console.log(error.message)
if (error.name === "CastError") {
return response.status(400).send({ error: 'malformed id' })
} else if (error.name === "ValidationError") {
return response.status(400).json({ error: error.message })
}
next(error)
}
app.use(errorHandler)
const PORT = process.env.PORT;
// const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

frontend:

App.jsx
import { useState, useEffect } from 'react'
import personService from './services/persons'
import Filter from './components/Filter'
import PersonForm from './components/PersonForm'
import Notification from './components/Notification'
import NotificationError from './components/NotificationError'
import Persons from './components/Persons'
// var _ = require('lodash')
import _ from 'lodash'
const App = () => {
const [persons, setPersons] = useState([])
const [newName, setNewName] = useState('')
const [newNumber, setNewNumber] = useState('')
const [filterName, setFilterName] = useState('')
const [infoMessage, setInfoMessage] = useState(null)
const [errorMessage, setErrorMessage] = useState(null)
useEffect(() => {
personService
.getAll()
.then(initialPersons => {
setPersons(initialPersons)
})
}, [])
// console.log('render', persons.length, 'persons')
const handleNameChange = (event) => {
event.preventDefault()
setNewName(event.target.value)
}
const handleNumberChange = (event) => {
event.preventDefault()
setNewNumber(event.target.value)
}
const addPerson = (event) => {
event.preventDefault()
const personObject = {
name: newName,
number: newNumber,
// id: String(persons.length + 1)
}
const existPersonArray = persons.filter(person => _.includes(person, newName))
// console.log(existPersonArray)
if (existPersonArray.length > 0) {
// alert(`${newName} is already added to phonebook`)
const existPerson = existPersonArray[0]
// console.log(existPerson)
if (
window.confirm(`${newName} is already added to phonebook, replace the old number with a new one?`)
) {
const id = existPerson.id
console.log("existPerson id", id)
personService
.update(id, personObject)
.then(returnedPerson => {
setPersons(persons.map(person => person.id !== id ? person : returnedPerson))
setNewName('')
setNewNumber('')
setInfoMessage(`Updated ${returnedPerson.name}`)
setTimeout(() => {
setInfoMessage(null)
}, 5000)
})
.catch(error => {
console.log(error)
setErrorMessage(`Information of ${existPerson.name} has already been removed from server`)
setTimeout(() => {
setErrorMessage(null)
}, 5000)
setPersons(persons.filter(person => person.id !== id))
})
}
} else {
personService
.create(personObject)
.then(returnedPerson => {
setPersons(persons.concat(returnedPerson))
setNewName('')
setNewNumber('')
setInfoMessage(`Added ${returnedPerson.name}`)
setTimeout(() => {
setInfoMessage(null)
}, 5000)
})
.catch(error => {
// This is the way to access the error message
console.log(error.response.data.error)
setErrorMessage(error.response.data.error)
setTimeout(() => {
setErrorMessage(null)
}, 5000)
})
}
}
const handleFilterName = (event) => {
event.preventDefault()
setFilterName(event.target.value)
}
const personsToShow = persons.filter(
person => _.includes(person.name.toLowerCase(), filterName.toLowerCase())
)
const handlePersonDelete = (person) => {
// console.log(`deleting person ${id}`)
if (window.confirm(`delete ${person.name} ?`)) {
personService
.deleteId(person.id)
.then(responsePerson => {
setPersons(persons.filter(n => n.id !== responsePerson.id))
})
}
}
return (
<div>
<h2>Phonebook</h2>
<Notification message={infoMessage} />
<NotificationError message={errorMessage} />
<Filter filterName={filterName} onChange={handleFilterName} />
<h2>Add a new</h2>
<PersonForm addPerson={addPerson} newName={newName} newNumber={newNumber} handleNameChange={handleNameChange} handleNumberChange={handleNumberChange} />
<h2>Numbers</h2>
<Persons personsToShow={personsToShow} handleClick={handlePersonDelete} />
</div>
)
}
export default App

3.20* number should be:

length at least 8 or more
first 2 to 3 number, then a dash, then numbers

models/person.cjs
const phonebookSchema = new mongoose.Schema({
// name: String,
name: {
type: String,
minLength: 3,
required: true
},
// number: String,
number: {
type: String,
validate: {
validator: function (v) {
// const regex = /^(\d{2,3}-\d+)$/;
const regex = /^(?=.{8,})(\d{2,3}-\d+)$/;
return regex.test(v)
},
message: props => `${props.value} is not a valid phone number!`
},
required: [true, 'User phone number required']
}
})

3.21 full stack and deploy

Build frontend, copy “dist” folder to backend root, deploy.

Make sure local http://localhost:3001 works and the deployed fly.io/render domain works.

https://backend-fullstackopen-phonebook.fly.dev/

Lint

pnpm install eslint --save-dev
npx eslint --init
answer questions:
syntax
commonjs
none
javascript
node
Yes
pnpm

get eslint.config.mjs

reformat it:

eslint.config.mjs
import globals from "globals";
export default [
{
files: ["**/*.js"],
languageOptions: {
sourceType: "commonjs",
globals: {
...globals.node,
},
ecmaVersion: "latest",
}
},
]

add js.configs.recommend,

eslint.config.mjs
// ...
import js from '@eslint/js'
// ...
export default [
js.configs.recommended,
{
// ...
}
]
pnpm install --save-dev @stylistic/eslint-plugin-js
import eslint-plugin-js
// ...
import stylisticJs from '@stylistic/eslint-plugin-js'
export default [
{
// ...
plugins: {
'@stylistic/js': stylisticJs
},
rules: {
'@stylistic/js/indent': [
'error',
2
],
'@stylistic/js/linebreak-style': [
'error',
'unix'
],
'@stylistic/js/quotes': [
'error',
'single'
],
'@stylistic/js/semi': [
'error',
'never'
],
},
},
]

so it is now:

eslint.config.mjs
import globals from "globals";
import js from '@eslint/js'
import stylisticJs from "@stylistic/eslint-plugin-js"
export default [
js.configs.recommended,
{
files: ["**/*.js"],
languageOptions: {
sourceType: "commonjs",
globals: {
...globals.node,
},
ecmaVersion: "latest",
},
plugins: {
'@stylistic/js': stylisticJs
},
rules: {
'@stylistic/js/indent': [
'error',
2
],
'@stylistic/js/linebreak-style': [
'error',
'unix'
],
'@stylistic/js/quotes': [
'error',
'single'
],
'@stylistic/js/semi': [
'error',
'never'
],
},
},
]
npx eslint index.js

to eliminate error:

Expected linebreaks to be 'LF' but found 'CRLF'

revise in rule:

// '@stylistic/js/linebreak-style': [
// 'error',
// 'unix'
// ],
'@stylistic/js/linebreak-style': [
'error',
'windows'
],
// 'linebreak-style': ["off", "windows"],
// 'linebreak-style': ["error", "windows"],
// 'linebreak-style': 0,
// 'linebreak-style': 'off',

add lint script:

package.json
{
// ...
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
// ...
"lint": "eslint ."
},
// ...
}

ignore dist:

eslint.config.mjs
// ...
export default [
// ...
{
ignores: ["dist/**"],
},
//...
]

eqeqeq
trailing spaces
curly-spacing
arrow-spacing

export default [
// ...
rules: {
// ...
'eqeqeq': 'error',
'no-trailing-spaces': 'error',
'object-curly-spacing': [
'error', 'always'
],
'arrow-spacing': [
'error', { 'before': true, 'after': true },
],
},
]

no-console off

Overall:

eslint.config.mjs
import globals from "globals";
import stylisticJs from '@stylistic/eslint-plugin-js'
import js from '@eslint/js'
export default [
js.configs.recommended,
{
files: ["**/*.js"],
languageOptions: {
sourceType: "commonjs",
globals: {
...globals.node,
},
ecmaVersion: "latest",
},
plugins: {
'@stylistic/js': stylisticJs
},
rules: {
'@stylistic/js/indent': [
'error',
2
],
'@stylistic/js/linebreak-style': [
'error',
// 'unix'
'windows'
],
'@stylistic/js/quotes': [
'error',
'single'
],
'@stylistic/js/semi': [
'error',
'never'
],
'eqeqeq': 'error',
'no-trailing-spaces': 'error',
'object-curly-spacing': [
'error', 'always'
],
'arrow-spacing': [
'error', { 'before': true, 'after': true },
],
'no-console': 'off',
},
},
{
ignores: ["dist/**", "build/**"],
},
]

install ESlint plugin in vscode.

That’s all for this chapter.

Exercise 3.22.

3.22: Lint configuration Add ESlint to your application and fix all the warnings.

My solutions

My solutions:
(Please complete your own solutions before click here.)

install eslint and prettier eslint plugins in vscode.

pnpm i -D eslint
pnpm i -D @stylistic/eslint-plugin-js
npx eslint --init
// add js.configs.recommend

add lint script

the eslint.config.mjs and index.mjs file as below:

eslint.config.mjs
import globals from "globals";
import stylisticJs from '@stylistic/eslint-plugin-js'
import js from '@eslint/js'
export default [
js.configs.recommended,
{
files: ["**/*.js"],
languageOptions: {
sourceType: "commonjs",
// sourceType: "module",
globals: {
...globals.node,
},
ecmaVersion: "latest",
},
plugins: {
'@stylistic/js': stylisticJs
},
rules: {
'@stylistic/js/indent': [
'error',
2
],
'@stylistic/js/linebreak-style': [
'error',
// 'unix'
'windows'
],
'@stylistic/js/quotes': [
'error',
'single'
],
'@stylistic/js/semi': [
'error',
'never'
],
'eqeqeq': 'error',
'no-trailing-spaces': 'error',
'object-curly-spacing': [
'error', 'always'
],
'arrow-spacing': [
'error', { 'before': true, 'after': true },
],
'no-console': 'off',
},
},
{
ignores: ["dist/**", "build/**"],
},
]
index.mjs
import { createRequire } from 'module';
const require = createRequire(import.meta.url)
// const axios = require("axios");
const morgan = require("morgan");
/* eslint-disable no-unused-vars */
const _ = require("lodash");
import globals from "globals";
const console = globals.console;
const process = globals.process;
require('dotenv').config()
const express = require("express");
const cors = require("cors");
const app = express();
const Person = require('./models/person.cjs')
morgan.token('postBody', function getPostBody(req) {
// console.log(req.method)
// console.log(JSON.stringify(req.body) === JSON.stringify({}))
// if (JSON.stringify(req.body) !== JSON.stringify({})) {
if (req.method.toLowerCase() === "post".toLowerCase()) {
return JSON.stringify(req.body)
}
return null
})
app.use(cors());
app.use(express.static('dist'))
app.use(express.json());
// app.use(morgan("tiny"))
app.use(morgan(':method :url :status :res[content-length] - :response-time ms :postBody'))
// let persons = [
// {
// id: "1",
// name: "Arto Hellas",
// number: "040-123456",
// },
// {
// id: "2",
// name: "Ada Lovelace",
// number: "39-44-5323523",
// },
// {
// id: "3",
// name: "Dan Abramov",
// number: "12-43-234345",
// },
// {
// id: "4",
// name: "Mary Poppendieck",
// number: "39-23-6423122",
// },
// ];
// app.get("/api/persons", (request, response) => {
// response.json(persons);
// });
app.get('/api/persons', (request, response) => {
Person.find({}).then(persons => {
response.json(persons)
})
})
app.get("/info", (request, response, next) => {
// let respPersons;
// axios.get("http://localhost:3001/api/persons")
// .then(resp => {
// // console.log(resp)
// respPersons = resp.data
// // console.log(respPersons)
// response.send(`
// Phonebook has info for ${respPersons.length} ${respPersons.length === 1 ? "person" : "people"} <br />
// ${new Date().toString()}
// `)
// })
// .catch(error => next(error))
Person.find({})
.then(respPersons => {
// response.json(respPersons)
// console.log(respPersons)
response.send(`
Phonebook has info for ${respPersons.length} ${respPersons.length === 1 ? "person" : "people"} <br />
${new Date().toString()}
`)
})
.catch(error => next(error))
})
// app.get("/api/persons/:id", (request, response) => {
// const id = request.params.id;
// const person = persons.find((person) => person.id === id)
// if (person) {
// response.json(person)
// } else {
// response.status(404).end()
// }
// })
app.get('/api/persons/:id', (request, response, next) => {
console.log("getting /api/persons/:id, id: ", request.params.id)
// try {
// Person.findById(request.params.id).then(person => {
// response.json(person)
// })
// } catch (error) {
// console.log(error)
// }
Person.findById(request.params.id)
.then(person => {
if (person) {
response.json(person)
} else {
response.status(404).end()
}
})
.catch(error => next(error))
})
app.delete("/api/persons/:id", (request, response, next) => {
// const id = request.params.id;
// persons = persons.filter((person) => person.id !== id)
// response.status(204).end()
Person.findByIdAndDelete(request.params.id)
.then(result => {
console.log(result)
response.status(204).end()
})
.catch(error => next(error))
})
app.put("/api/persons/:id", (request, response, next) => {
// const body = request.body
const { name, number } = request.body
// const person = {
// name: body.name,
// number: body.number,
// }
// Person.findByIdAndUpdate(request.params.id, person, { new: true })
Person.findByIdAndUpdate(
request.params.id,
{ name, number },
{ new: true, runValidators: true, context: 'query' }
)
.then(updatedPerson => {
response.json(updatedPerson)
})
.catch(error => next(error))
})
// const generateId = () => {
// const maxId = persons.length > 0 ? Math.max(...persons.map((p) => Number(p.id))) : 0
// return String(maxId + 1)
// }
// app.post("/api/persons", (request, response) => {
// const body = request.body;
// if (!body.name || !body.number) {
// return response.status(400).json({
// error: "person name or number missing",
// })
// }
// const personsNameArray = persons.map((p) => p.name)
// if (_.includes(personsNameArray, body.name)) {
// return response.status(400).json({
// error: "name must be unique"
// })
// }
// const person = {
// name: body.name,
// number: body.number,
// id: generateId(),
// }
// persons = persons.concat(person)
// response.json(person)
// })
app.post('/api/persons', async (request, response, next) => {
const body = request.body
if (body.name === undefined || body.number === undefined) {
return response.status(400).json({ error: 'name or number missing' })
}
const existPerson = await Person.findOne({ 'name': body.name }, 'name number id')
if (existPerson) {
console.log(`Person with name ${body.name} exist, updating the number...`)
// Update the existing person with the new information
const updatedPerson = await Person.findByIdAndUpdate(
existPerson.id,
body,
{ new: true, runValidators: true, overwrit: true }
);
return response.status(200).json(updatedPerson)
}
const person = new Person({
name: body.name,
number: body.number,
})
person.save()
.then(savedPerson => {
response.json(savedPerson)
})
.catch(error => next(error))
})
const unknownEndpoint = (request, response) => {
response.status(404).send({ error: 'unknown endpoint' })
}
// handler of requests with unknown endpoint
app.use(unknownEndpoint)
const errorHandler = (error, request, response, next) => {
console.log(error.message)
if (error.name === "CastError") {
return response.status(400).send({ error: 'malformed id' })
} else if (error.name === "ValidationError") {
return response.status(400).json({ error: error.message })
}
next(error)
}
app.use(errorHandler)
const PORT = process.env.PORT;
// const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});