part3d
Validation and ESLint
Define validation rules in schema in models/note.js
const noteSchema = new mongoose.Schema({ content: { type: String, minLength: 5, required: true }, important: Boolean})
catch error in create note:
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:
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:
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
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)
// 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 endpointapp.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:
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
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.
Lint
pnpm install eslint --save-dev
npx eslint --init
answer questions:syntaxcommonjsnonejavascriptnodeYespnpm
get eslint.config.mjs
reformat it:
import globals from "globals";
export default [ { files: ["**/*.js"], languageOptions: { sourceType: "commonjs", globals: { ...globals.node, }, ecmaVersion: "latest", } },]
add js.configs.recommend,
// ...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:
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:
{ // ... "scripts": { "start": "node index.js", "dev": "nodemon index.js", // ...
"lint": "eslint ." }, // ...}
ignore dist:
// ...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:
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 eslintpnpm i -D @stylistic/eslint-plugin-jsnpx eslint --init// add js.configs.recommend
add lint script
the eslint.config.mjs and index.mjs file as below:
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/**"], },]
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 endpointapp.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}`);});