Biên soạn: Trần Thiên Trọng – tranthientrong.it@gmail.com SETUP NODEJS https://wpbeaches.com/installing-node-js-on-macos-big-sur-and-earlier-macos-versions/ Setup NodeJS project ❯ npm init package name: (crud_mysql) crud_mysql version: (1.0.0) 1.0.0 description: This is project to demonstrate how to CRUD in NodeJS using MySQL git repository: author: Tran, Trong thien license: (ISC) ISC About to write to /Users/trong/Documents/Documents-Cloud/DEV/NodeJS/crud_mysql/package.json: { "name": "crud", "version": "1.0.0", "description": "This is project to demonstrate how to CRUD in NodeJS using MySQL", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "Tran, Trong thien", "license": "ISC" } Is this OK? (yes) yes Use `npm install <pkg>` afterwards to install a package and save it as a dependency in the package.json file. NodeJS project on new machine Bất cứ khi nào mở Node Project trên 1 máy mới, ta đều phải dùng npm install Nó sẽ nhìn vào file package.json để install các dependencies NODE MODULE SYSTEM Module giống như Library, đây là cách ta import một Module import * as fs from 'fs'; Lưu ý thêm cái này vào package.js để sử dụng ES6, mới import được { } "type":"module", export Để sử dụng các field từ file khác, ta phải import và export import * as service from './service.js'; export const name = 'Trong' console.log(service.name) export function myFunction(name) { return name; console.log(service.myFunction('Trong')) } ❯ node app.js Trong Trong FILE SYSTEM Current Directory import path from 'path'; const dirname = path.resolve(); console.log(dirname)); -> /Users/trong/Documents/Documents-Cloud/DEV/NodeJS/practice/web-server console.log(path.join(dirname, '../public')); -> /Users/trong/Documents/DocumentsCloud/DEV/NodeJS/practice/public Input from command line 1 điều buồn cười là nodeJS không có cách nào đơn giản để nhận input từ user cả. Mà phải phụ thuộc vào cái module. readline là module có sẵn trong NodeJS import * as readline from 'readline'; Who are you? trong Hey there trong! const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('Who are you?', name => { console.log(`Hey there ${name}!`); rl.close(); }); Write File import * as fs from 'fs'; fs.writeFileSync('notes.txt', 'This is note'); fs.appendFileSync('notes.txt', ', my name is Trong'); ASYNCHRONOUS Đơn giản là nó sẽ chạy ở Thread mới không đồng bộ với flow chính, thread này ở C++, đó là lý do vì sao người ta vẫn gọi là single-thread. *Note: Tại sao bên dưới setTimeout là 0 giây nhưng vẫn print sau Stopping -> Bởi vì các function trong asynchronous được gọi là callback, và callback chỉ được gọi khi flow chính hoàn tất. console.log(chalk.green('Starting')); setTimeout(() => { console.log('1s passed') }, 1000); setTimeout(() => { console.log('0s passed') }, 0); console.log(chalk.green('Stopping')); Callback Function Là 1 function BÌNH THƯỜNG ( gọi là B ), nhưng được truyền dưới dạng Agurment và Parameter của Function khác ( gọi là A ), mỗi lần ta gọi B, B sẽ chạy ngày lập tức nhưng chưa chắc chạy A, A sẽ được Gọi lại để chạy vào một thời điểm nào đó mà ta xác định trong B Promises Hiểu đơn giản thì Promises là một loại function mà kết quả trả về của nó là Asynchronous. Nghĩa là nó không trả về ngay khi bạn gọi mà nó “HỨA” trong tương lai sẽ trả về, vì thể nên tất cả function Promises đều có hàm .then( ) Bên trong Promise cho ta 2 parameter, đây là 2 callback để xác định khi nào then( ), khi nào cacth( ) à Nếu bạn gọi callback tên là resolve, thì kết quả của nó sẽ được run ở then( ) à Nếu bạn gọi callback tên là reject, thì kết quả của nó sẽ được run ở catch( ) const functionWillReturnInFuture = new Promise((resolve, reject) => { setTimeout(() => { resolve("I'm from 1s later"); }, 1000); }); functionWillReturnInFuture.then((result) => { console.log(`Result: ${result}`); }).catch((error)=>{ console.log(error); }); à Result: I'm from 1s later const functionWillReturnInFuture = new Promise((resolve, reject) => { setTimeout(() => { reject("I'm from 1s later"); }, 1000); }); functionWillReturnInFuture.then((result) => { console.log(`Result: ${result}`); }).catch((error)=>{ console.log(`Error: ${error}`); }); à Error: I'm from 1s later Kết hợp resolve và reject: const functionWillReturnInFuture = (someValue) =>{ return new Promise((resolve, reject) => { setTimeout(() => { if (someValue > 0){ resolve("I'm Positive"); } else { reject("ERROR Negative"); } }, 1000); }); } Promises Chaining Nếu ta gọi Promise trong 1 Promise khác thì thế lào? -> dùng then( ).then( ) thôi const functionWillReturnInFuture = new Promise((resolve, reject) => { setTimeout(() => { resolve("I'm from 1s later"); }, 1000); }); functionWillReturnInFuture.then((result) => { console.log(`Result 1: ${result}`); return functionWillReturnInFuture; } ).then((result) => { console.log(`Result 2: ${result}`); } ).catch((error) => { console.log(`Error: ${error}`); }); à Result 1: I'm from 1s later Result 2: I'm from 1s later Async / Await async function read() { await client.connect(); const database = client.db(databaseName); } await read(); Convert from then( ).catch( ) à try-catch findOne({_id: new ObjectId(_idParam)}) .then((result) => { if (result == null) { response.status(404).send(`TASK NOT FOUND`); } else { response.status(200).send(`FOUND ${result}`); } }) .catch((error) => { response.status(500).send(`${error}`); }); try { const foundTasks = await Task.find({}); if (foundTasks == null) { response.status(404).send(`TASKS NOT FOUND`); } else { response.status(200).send(`FOUND ${foundTasks}`); } } catch (error) { response.status(500).send(`${error}`); } HTTP REQUEST Có nhiều Module để có thể gửi request trong NodeJS, tuy nhiên theo document thì ta nên dùng https://www.npmjs.com/package/axios import axios, * as others from 'axios'; axios .get('https://google.com') .then(response => { console.log(`Status Code: ${response.status}`) console.log(`Data: ${response.data}`) }) .catch(error => { console.error(error) }); Status Code: 200 Data: ...bla bla bla................. GET import axios, * as others from 'axios'; axios.get(WEATHER_URL+`/current?access_key=${API_KEY} &query=Vietnam`) .then(response => { // handle success console.log(`StatusCode: ${response.status}`); console.log(response.data); }) .catch(error => { console.log(error); }) .then(() => { // always executed }); StatusCode: 200 { location: { name: 'Hanoi', country: 'Vietnam', region: '', lat: '21.033', lon: '105.850', timezone_id: 'Asia/Ho_Chi_Minh', localtime: '2022-02-15 19:21', localtime_epoch: 1644952860, utc_offset: '7.0' }, current: { observation_time: '12:21 PM', temperature: 16, weather_code: 122, weather_descriptions: [ 'Overcast' ], wind_speed: 7, wind_degree: 60, wind_dir: 'ENE', pressure: 1015, precip: 0, humidity: 88, cloudcover: 100, feelslike: 16, uv_index: 1, visibility: 9, is_day: 'no' } } axios.get(`${WEATHER_URL}/current?access_key=${API_KEY}&query=Vietnam`) .then(response => { // handle success StatusCode: 200 console.log(`StatusCode: ${response.status}`); Current console.log(`Current temperature: ${response.data.current.temperature}`); temperature: 16 console.log(typeof response.data); object }) WEBSERVER – EXPRESSJS -> Hiểu đơn giản thì ExpressJS giúp ích rất nhiều trong việc tạo WebServer ( REST API ) ExpressJS is a light-weight web application framework. You can then use a database like MongoDB with Mongoose (for modeling) to provide a backend for your Node.js application. Express.js basically helps you manage everything, from routes, to handling requests and views. import * as express from 'express'; const app = express(); Start Listen Server import express, * as others from 'express'; import chalk from 'chalk'; const app = express(); app.get('', (request, response) => { response.send('This is response'); }) app.get('/about', (request, response) => { response.send('About'); }) app.listen(3000,()=>{ console.log(chalk.green(`listening on port 3000`)); }) Đây là cách nó hoạt động: Send GET Request to localhost:3000 Send Response with body ‘This is response’ Client http://localhost:3000 this is response http://localhost:3000/about About Return Type JSON Server app.get('/trong', (request, response) => { response.send([ { name: "Thien Trong", age: 22 } ]); }) http://localhost:3000/trong [ { "name": "Thien Trong", "age": 22 } ] Directory Ví dụ ta muốn return public directory import express, * as others from 'express'; import path from 'path'; import chalk from 'chalk'; const app = express(); const dirname = path.resolve(); const webpageDir = path.join(dirname, '/public') console.log(webpageDir); app.use(express.static(webpageDir)); app.listen(3000, () => { console.log(chalk.green(`listening on port 3000`)); }) Vì index.html có trong directory nên nó sẽ tự loaded Query String Nhắc lại, Query String là đoạn phía sau dấu ? trong URL Ta thường nhớ đến payload trong POST request để gửi đến Server mà quên mất Query String chính là thông tin trong form mà ta có thể gửi đến Server luôn, vì thể Query String rất quan trọng nhé. Ta có thể gọi response.query để lấy giá trị này app.get('/product', (request, response) => { console.log(request.query); console.log(request.query.name); console.log(request.query.age); }) http://localhost:3000/product?name=’Trong’&age=22 Ta có thể set điều kiện như vầy: app.get('/product', (request, response) => { if(!request.query.name){ response.send("No name provided"); } else { response.send(request.query); } }) Create Endpoint Ở ví dụ dưới đây, ta sẽ tạo một endpoint với URL là: http:localhost.com:3000/weather Ta sẽ lấy dữ liệu từ một API khác nữa là weatherstack.com ( mục đích có data để làm mẫu thôi ) const API_KEY = 'ed5c69199afcb0bdddcf0bb4efb3b676'; const WEATHER_URL = 'http://api.weatherstack.com'; export const getWeather = (country, callbackFunc)=>{ axios .get(`${WEATHER_URL}/current?access_key=${API_KEY}&query=${country}`) .then(response => { callbackFunc(response.data.current) }) .catch(error => { }); } import express, * as others from 'express'; import {getWeather} from './weather-service.js'; app.get('/weather', (request, response) => { if(!request.query.country){ response.send("No country provided"); } else { getWeather(request.query.country, (weatherData)=>{ response.send(weatherData); }); } }) Send Request from front end Nếu ở NodeJS có các method của axios thì ở Client ( cụ thể là Javasrcript ) sẽ có method gọi là fetch gửi request đến URL <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="script.js"></script> </head> <body> <h1> Weather </h1> </body> </html> console.log('Client side javascript file is loaded!') const API_KEY = 'ed5c69199afcb0bdddcf0bb4efb3b676'; const WEATHER_URL = 'http://api.weatherstack.com'; fetch(`${WEATHER_URL}/current?access_key=${API_KEY}&query=Vietnam`).then((response)=>{ response.json().then((data)=>{ console.log(data); }); }); Lưu ý bởi vì tất cả đoạn code trên đều xảy ra ở Client, bạn sẽ không nhìn thấy gì ở terminal Thay vào đó bạn sẽ nhìn thấy console.log ở browser Send Response back to Front End Front End Gửi 1 Request đến http:localhost:3000/weather?country=Vietnam fetch(`/weather?country=Vietnam`,{ headers : { 'Content-Type': 'application/json', 'Accept': 'application/json' } }).then((response)=>{ response.json().then((data)=>{ console.log(data); }); }); Backend lắng nghe trên endpoint http:localhost:3000/weather và return JSON app.get('/weather', (request, response) => { if(!request.query.country){ response.send("No country provided"); } else { getWeather(request.query.country, (weatherData)=>{ response.send(weatherData); }); } }) app.listen(3000, () => { console.log(chalk.green(`listening on port 3000`)); }) MONGODB Setup Download: https://www.mongodb.com/try/download/community Cái chúng ta tải về và sử dụng được gọi là MongoDB Server( Để tránh nhầm lẫn với Atlas ) Sau khi tải về và giải nén, để thuận tiện: 1. Ta bỏ thư mục mongodb vào User/trong 2. Tạo thư mục mới tên mongodb-data 3. Gõ lệnh này để kết nối thư mục mongodb-data mới tạo ❯ /Users/trong/mongodb/bin/mongod --dbpath=/Users/trong/mongodb-data Nếu bạn nhìn vào terminal, sẽ thấy 2 dòng này: {"t":{"$date":"2022-02-17T08:51:44.249+07:00"},"s":"I", "c":"NETWORK", "id":23015, "ctx":"listener","msg":"Listening on","attr":{"address":"127.0.0.1"}} {"t":{"$date":"2022-02-17T08:51:44.249+07:00"},"s":"I", "c":"NETWORK", "id":23016, "ctx":"listener","msg":"Waiting for connections","attr":{"port":27017,"ssl":"off"}} à Nghĩa là mongoDB đang lắng nghe ở 127.0.0.1:27017 Connect to IDE 1. Download Robo 3T 2. Setting Connection 3. Để kiểm tra, ta thử gõ db.version() Giống như JBDC Driver, bộ kit sử dụng để lập trình MongoDB được gọi là MongoDB Driver. Document https://docs.mongodb.com/drivers/ Offical Driver https://www.npmjs.com/package/mongodb Connect to MongoDB Có 2 cách kết nối tới MongoDB (nhớ khởi động Mongo Server trước thông qua lệnh ) ❯ /Users/trong/mongodb/bin/mongod --dbpath=/Users/trong/mongodb-data Cách 1 import { MongoClient } from 'mongodb' const connectionURL = 'mongodb://127.0.0.1:27017'; const databaseName = 'task-app'; const client = new MongoClient(connectionURL); client.connect().then((client) => { console.log(chalk.bgGreen.black(` SUCCESS CONNECTED TO MONGODB !!! `)); const database = client.db(databaseName); }).catch((error)=>{ console.log(chalk.red(`Cannot connect to MongoDB: ${error}`)); }); import { MongoClient } from 'mongodb' const connectionURL = 'mongodb://127.0.0.1:27017'; const databaseName = 'task-app'; MongoClient.connect(connectionURL, {useNewUrlParser: true}, (error, client) => { Cách 2 if(error){ console.log(chalk.red(`Cannot connect to MongoDB: ${error}`)); } else { console.log(chalk.bgGreen.black(' SUCCESS CONNECTED TO MONGODB ')); const database = client.db(databaseName); } }); MongoDB basic ObjectID Giống như document id, nó được tạo ra tự động bởi MongoDB cho mỗi document bạn tạo ra. Tuy nhiên nên lưu ý là khi bạn xem document đó trên một IDE bất kỳ, chúng sẽ luôn hiện điều này nghĩa là giá trị thật của _id là kết quả trả về của ObjectId trả về, nó sẽ không hiện kiểu String để ta nhìn thấy hết, và giá trị 620e45705f9eff8e218c92b7 cũng không phải giá trị thực sự của _id. Chúng ta có thể tạo ObjectID theo ý mình: client.connect().then((client) => { console.log(chalk.bgGreen.black(` SUCCESS CONNECTED TO MONGODB !!! `)); const database = client.db(databaseName); database.collection('tasks').insertMany([{ _id: 1, description: 'Clean the house', complete: true, }, { _id: 2, name: 'Fix light', complete: false, }, { _id: 3, name: 'Debug', complete: false, }] }); CRUD C insertOne Ở ví dụ bên dưới, ta sẽ insert một document mới vào collection users import {MongoClient} from 'mongodb' const connectionURL = 'mongodb://127.0.0.1:27017'; const databaseName = 'task-app'; const client = new MongoClient(connectionURL); client.connect().then((client) => { console.log(chalk.bgGreen.black(` SUCCESS CONNECTED TO MONGODB !!! `)); const database = client.db(databaseName); database.collection('users').insertOne({ name: 'Trong', age: 22, } ); }).catch((error) => { console.log(chalk.red(`Cannot connect to MongoDB: ${error}`)); }); db.getCollection('users').find({}); insertMany client.connect().then((client) => { console.log(chalk.bgGreen.black(` SUCCESS CONNECTED TO MONGODB !!! `)); const database = client.db(databaseName); database.collection('users').insertMany([ { name: 'Trong', age: 22, }, { name: 'Cute', age: 22, } ], (error, result) => { if (error) { console.log(chalk.red(`Cannot Insert`)); } else { console.log(chalk.bgGreen.black(' SUCCESS INSERT ')); console.log(JSON.stringify(result)); } }); }).catch((error) => { console.log(chalk.red(`Cannot connect to MongoDB: ${error}`)); }); R findOne database.collection('users').findOne({ name: 'Trong' }, (error, result) => { if (error) { console.log(chalk.red(`Unable To Find`)); } else { if(result == null){ console.log(chalk.bgGreen.black(' FOUND NOTHING !!! ')); } else { console.log(chalk.bgGreen.black(` FOUND ${JSON.stringify(result)}`)); } } }); database.collection('users').findOne({ name: 'Trong', age: 30 }, (error, result) => { if (error) { console.log(chalk.red(`Unable To Find`)); } else { if(result == null){ console.log(chalk.bgGreen.black(' FOUND NOTHING !!! ')); } else { console.log(chalk.bgGreen.black(` FOUND ${JSON.stringify(result)}`)); } } }); Lưu ý, để tìm theo _id, bạn cần tìm ObjectId( _id ) database.collection('users').findOne({ _id: new ObjectId("620e45705f9eff8e218c92b7"), }, (error, result) => { if (error) { console.log(chalk.red(`Unable To Find`)); } else { if(result == null){ console.log(chalk.bgGreen.black(' FOUND NOTHING !!! ')); } else { console.log(chalk.bgGreen.black(` FOUND ${JSON.stringify(result)}`)); } } }); find Như mọi method để query khác, khi kết quả trả về có nhiều kết quả, nó sẽ trả về Cursor Bạn có thể dùng toArray( ) để danh sách kết quả const resultList = database.collection('users').find({ name: 'Trong' }); resultList.toArray((error, results)=>{ if (error) { console.log(chalk.red(`Unable To Find`)); } else { if (results == null) { console.log(chalk.bgGreen.black(' FOUND NOTHING !!! ')); } else { console.log(chalk.bgGreen.black(` FOUND ${JSON.stringify(results)}`)); console.log(chalk.bgGreen.black(` FOUND ${JSON.stringify(results[0])}`)); } } }); FOUND [ {"_id":"620e45705f9eff8e218c92b7","name":"Trong","age":22}, {"_id":"620e498b1379e27bbe251b81","name":"Trong","age":22}, {"_id":"620e498b1379e27bbe251b82","name":"Trong","age":23}, {"_id":"620e498b1379e27bbe251b83","name":"Trong","age":24} ] FOUND {"_id":"620e45705f9eff8e218c92b7","name":"Trong","age":22} U Ở phần Update trong MongoDB, nó dùng 1 ký tự rất kỳ cmn lạ là $set, xem thêm các cách update khác tại đây: https://docs.mongodb.com/manual/reference/operator/update/ updateOne const updateResult = database.collection('tasks').updateOne( { _id: 1 }, { $set: { complete: false } }); updateResult.then((result)=>{ console.log(chalk.green('UPDATE SUCCESS documents =>'), result); }).catch((error)=>{ console.log(chalk.red('UPDATE FAIL =>'), error); }); à $inc à database.collection('users').updateOne( {_id: new ObjectId('620e498b1379e27bbe251b83')}, { $inc: {age: 1} } ); updateMany Update Many sẽ khác với Insert Many, nếu với Insert Many ta tạo từng Document, thì Update Many nghĩa là update một lần cho tất cả Documents thảo mãn filter à database.collection('tasks').updateMany( {complete: false}, { $set: {complete: true} } ); D deleteOne Nếu chúng ta có > 2 Document cùng thoả mãn filter, nó sẽ delete cái đầu tiên database.collection('tasks').deleteOne( {name: 'Fix light'}).then((result) => { console.log(chalk.green('DELETE SUCCESS documents =>'), result); }).catch((error) => { console.log(chalk.red('DELETE FAIL =>'), error); }); deleteMany à database.collection('users').deleteMany( {age: 25}).then((result) => { console.log(chalk.green('DELETE SUCCESS documents =>'), result); }).catch((error) => { console.log(chalk.red('DELETE FAIL =>'), error); }); MONGOOSE https://www.npmjs.com/package/mongoose Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box. If your app uses only one database, use mongoose.connect await mongoose.connect(`${connectionURL}/${databaseName}`); If you need to create additional connections, use mongoose.createConnection const conn = mongoose.createConnection('your connection string'); ODM Create Model Nếu ORM là Object Relation Mapping thì ODM là Object Document Mapping await mongoose.connect(mongodb:127.0.0.1:27017/task-app) const MyModel = mongoose.model('ModelName', schema); Một điều hay là ModelName bạn có thể đặt số ít hoặc số nhiều, chữ hoa hay không hoa, Mongoose cũng sẽ tự hiểu. Ví dụ: mongoose.model('Task', schema); mongoose.model('Tasks', schema); mongoose.model('tasks', schema); mongoose.model('task', schema); mongoose.model('tAsK', schema); import mongoose from 'mongoose'; import validator, * as otherValidator from 'validator'; import chalk from 'chalk'; const connectionURL = 'mongodb://127.0.0.1:27017'; const databaseName = 'task-app'; await mongoose.connect(`${connectionURL}/${databaseName}`, {useNewUrlParser: true}); const UserSchema = new mongoose.Schema({ name: { type: String, required: true, trim: true, }, email: { type: String, required: true, trim: true, validate(value) { if (!validator.isEmail(value)) { throw new Error('Not a right email address'); } } }, password: { type: String, required: true, trim: true, minLength: 6, validate(value) { if (value.toLowerCase().includes('password')) { throw new Error('Cannot set password to "password"'); } } }, age: { type: Number, default: 0, validate(value) { if (value < 0) { throw new Error('Người âm phủ ???'); } } }, }); export const User = mongoose.model('users', UserSchema); save( ) Sau khi bạn tạo ra được Object từ Document, thì bạn có thể save( ) instance của Object đó, lúc này document mới cũng được thêm vào. const UserSchema = new mongoose.Schema({ name: String, age: Number, }); const User = mongoose.model('User', UserSchema); const userInstance = new User({name: 'Trong', age: 22}); userInstance .save() .then(() => {console.log(chalk.green(`Document Added: ${userInstance}`)); }) .catch((err) => {console.log(chalk.red(err)); }); Lưu ý 1: Nếu instance được tạo ra bị sai Type, nó sẽ báo lỗi const userInstance = new User({name: 'Trong', age: 'old'}); userInstance .save() .then(() => {console.log(chalk.green(`Document Added: ${userInstance}`)); }) .catch((err) => {console.log(chalk.red(err)); }); à ValidationError: age: Cast to Number failed for value "old" (type string) at path "age" Lưu ý 2: Một điều hay của NoSQL là nó không có 1 fixed-structure Ví dụ: Ta tạo thêm 1 field tên là somethingElse và add vào Collection vẫn không sao cả, không nhất thiết tất cả documents trong 1 collection phải có chung 1 strucuture như Relational const TaskSchema = new mongoose.Schema({ name: String, complete: Boolean, somethingElse: String, }); const Task = mongoose.model('task', TaskSchema); const myTask = new Task({name: 'Learn NodeJS', complete: false, somethingElse: 'hehe'}); SchemaType Options https://mongoosejs.com/docs/schematypes.html Với mỗi SchemaType bạn lại có thêm nhiều options để áp dụng lên type đó Ví dụ: const UserSchema = new mongoose.Schema({ name: { type: String, required: true, trim: true, }, email: { type: String, required: true, trim: true, validate(value) { if (!validator.isEmail(value)) { throw new Error('Not a right email address'); } } }, age: { type: Number, default: 0, validate(value) { if (value < 0) { throw new Error('Người âm phủ ???'); } } }, }); String • lowercase: boolean, whether to always call .toLowerCase() on the value • uppercase: boolean, whether to always call .toUpperCase() on the value • trim: boolean, whether to always call .trim() on the value • match: RegExp, creates a validator that checks if the value matches the given regular expression • enum: Array, creates a validator that checks if the value is in the given array. • minLength: Number, creates a validator that checks if the value length is not less than the given number • maxLength: Number, creates a validator that checks if the value length is not greater than the given number • populate: Object, sets default populate options Number • min: Number, creates a validator that checks if the value is greater than or equal to the given minimum. • max: Number, creates a validator that checks if the value is less than or equal to the given maximum. • enum: Array, creates a validator that checks if the value is strictly equal to one of the values in the given array. • populate: Object, sets default populate options Date • min: Date • max: Date ObjectId • populate: Object, sets default populate options All Schema Types • required: boolean or function, if true adds a required validator for this property • default: Any or function, sets a default value for the path. If the value is a function, the return value of the function is used as the default. • select: boolean, specifies default projections for queries • validate: function, adds a validator function for this property • get: function, defines a custom getter for this property using Object.defineProperty(). • set: function, defines a custom setter for this property using Object.defineProperty(). • alias: string, mongoose >= 4.10.0 only. Defines a virtual with the given name that gets/sets this path. • immutable: boolean, defines path as immutable. Mongoose prevents you from changing immutable paths unless the parent document has isNew: true. • transform: function, Mongoose calls this function when you call Document#toJSON() function, including when you JSON.stringify() a document. const numberSchema = new Schema({ integerOnly: { type: Number, get: v => Math.round(v), set: v => Math.round(v), alias: 'i' } }); const Number = mongoose.model('Number', numberSchema); const doc = new Number(); doc.integerOnly = 2.001; doc.integerOnly; à 2 doc.i; à 2 doc.i = 3.001; doc.integerOnly; à 3 doc.i; à 3 Indexes You can also define MongoDB indexes using schema type options. • index: boolean, whether to define an index on this property. • unique: boolean, whether to define a unique index on this property. • sparse: boolean, whether to define a sparse index on this property. const schema2 = new Schema({ test: { type: String, index: true, unique: true // Unique index. If you specify `unique: true` // specifying `index: true` is optional if you do `unique: true` } }); Data Validation required Ví dụ: Có thể thiếu complete nhưng không thể thiếu name vì đã required const TaskSchema = new mongoose.Schema({ name: { type: String, required: true, }, complete: Boolean, }); const myTask = new Task({ complete: false}); à ValidationError: name: Path `name` is required. validate( ) const UserSchema = new mongoose.Schema({ name: String, age: { type: Number, validate(value){ if(value < 0){ throw new Error('Người âm phủ ???'); } } }, }); const userInstance = new User({name: 'Trong', age: -1}); à ValidationError: age: Người âm phủ ??? Ví dụ: Ta muốn set field password với các điều kiện sau đây: • Không được để trống • >= 6 ký tự • Không được bao gồm ‘password’ const UserSchema = new mongoose.Schema({ password: { type: String, required: true, trim: true, minLength: 6, validate(value){ if (value.toLowerCase().includes('password')) { throw new Error('Cannot set password to "password"'); } } }, }); Kêt hợp với validator.js const UserSchema = new mongoose.Schema({ name: String, email: { type: String, required: true, validate(value){ if (!validator.isEmail(value)){ throw new Error('Not a right email address'); } } }, }); const User = mongoose.model('User', UserSchema); const userInstance = new User({name: 'Trong', email: 'trong@'}); à ValidationError: email: Not a right email address Middleware Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions. Define Middleware Before Compiling Models Pre Calling pre() trước khi compiling a model. The below script will print out "Hello from pre save": next trong ví dụ dưới đây nhằm báo hiệu rằng bạn đã xong, tiếp theo là save, nếu không gọi next( ) thì chương trình sẽ bị treo. UserSchema.pre('save', async (next)=>{ console.log('hello from pre save'); next(); }) Connect 2 collections Đây là 2 field riêng lẻ, liệu ta sẽ tạo kết nối với chúng ntn? ref Tạo references đến Model khác const UserSchema = new mongoose.Schema({ ... }); mongoose.model('users', UserSchema); const TaskSchema = new mongoose.Schema({ owner: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'users' } }); Ta có thể dùng populate( ) để lấy references object const task = await Task.findById('62124a1e637f0ea64b432583'); await task.populate('owner'); console.log(task.owner); à { _id: new ObjectId("621238f48095808e9ddb6450"), name: 'Trong Cute 123', email: 'trongcute@gmail.com', password: '$2b$08$Dq/BB5COJEl2UPi3j1jU7O4JZXBGpE4wwOLBvstXoeCwsgZeJ1th.', age: 22, } Một cách nữa để liên kết với model khác nhưng không cần phải tạo field của Model đó trong Schema virtual virtual có nghĩa là ta đang muốn tạo 1 field có ý nghĩa logic nhưng không muốn nó xuất hiện trong mongoDB, virtual này do Mongoose nghĩ ra, không phải của MongoDB const TaskSchema = new mongoose.Schema({ owner: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'users' } }); const UserSchema = new mongoose.Schema({ }); UserSchema.virtual('tasksOfUser', { ref: 'tasks', localField: '_id', foreignField: 'owner' }); Ở ví dụ trên ta đang muốn nói rằng ta muốn tạo 1 attribute virutal tên là tasksOfUser, field này kết nối localField của UserSchema ( là User._id ) đến foreignField của tasks ( là Task.owner ) Cách này có tác dụng lên cả One-Many, nó có thể kết nối 1 User – Many Task const user = await User.findById('621238f48095808e9ddb6450'); await user.populate('tasksOfUser'); console.log(user.tasksOfUser); à [ { _id: new ObjectId("62124a1e637f0ea64b432583"), name: 'Fix Bug', complete: false, owner: new ObjectId("621238f48095808e9ddb6450"), __v: 0 }, { _id: new ObjectId("6212ab51b73f24f0a1e7df2a"), name: 'Finish backend', complete: false, owner: new ObjectId("621238f48095808e9ddb6450"), __v: 0 } ] Cascade Delete UserSchema.pre('remove', async function (next) { const user = this; await Task.deleteMany({owner: user._id}); next(); }); router.delete('/users/:id', authMiddleware, async (request, response) => { const _idParam = request.params.id; try { const deletedUser = await User.findOne({_id: new ObjectId(_idParam)}); if (deletedUser == null) { response.status(404).send("User not found"); } else { await deletedUser.remove(); response.status(200).send(`DELETED: ${deletedUser}`); } } catch (error) { response.status(400).send(`${error}`); } }); REST API Về cơ bản, - Client sẽ gửi các Request đến Server, - Dựa vào nội dung bên trong Request, Server sẽ gửi các Response tương ứng Đối với từng lệnh CRUD sẽ tương ứng với các Url và Request Method khác nhau: Vào trang này để xem ý nghĩa của các status code: https://httpstatuses.com/ C app.use( express.json( ) ) sẽ giúp request.body trở thành một Object, nếu không sẽ ra undefined. import {MongoClient, ObjectId} from 'mongodb'; import {User} from './models/user.js'; import express from 'express'; const app = express(); const port = process.env.PORT || 3000; /* _______________________________________________ CREATE ____________________________________ */ app.use(express.json()); app.post('/users',(request, response)=>{ const user = new User(request.body); user .save() .then(result => { response.status(201).send(`CREATE ${task} into MongoDB`); }) .catch(error =>{ console.log(chalk.red(error)); }); }); app.listen(port, () => { console.log(chalk.green(`Listening on port ${port}`)); }) à Theo mặc định, response chúng ta send sẽ là Bạn sẽ thấy Status 201 này trong response nhờ vào dòng: response.status(201).send(`CREATE ${task} into MongoDB`); R Read All User.find( { } ) do mongoose cung cấp có chức năng y chang như database.collection('users').find({}) import {MongoClient, ObjectId} from 'mongodb'; import {User} from './models/user.js'; import express from 'express'; import {Task} from "./models/task.js"; const app = express(); const port = process.env.PORT || 3000; app.use(express.json()); app.get('/users',(request, response)=>{ User.find({}).then((result)=>{ response.status(200).send(`FOUND ${result}`); }).catch((error) => { response.status(500).send(`${error}`); }); }); FOUND { _id: new ObjectId("620e45705f9eff8e218c92b7"), name: 'Trong', age: 22 }, { _id: new ObjectId("620e498b1379e27bbe251b81"), name: 'Trong', age: 22 }, { _id: new ObjectId("620e498b1379e27bbe251b82"), name: 'Trong', age: 23 }, { _id: new ObjectId("620f0f04adec7d06bf24d8bd"), name: 'Trong', age: 22, __v: 0 }, { _id: new ObjectId("620f50115ad943a58163ca60"), name: 'Trong', email: 'trong@gmail.com', age: 22, __v: 0 }, { _id: new ObjectId("620fa37cda96293e589c7c64"), name: 'Trong', email: 'trong@gmail.com', password: 'trongvip', age: 22, __v: 0 } Read One request.params app.get('/users/:id',(request, response)=>{ console.log(request.params); }); { “id” : 3 } Khi ta đặt :id trong url nghĩa là id là 1 param của url này Tương tự: request.params { /users/:id/:a/:b' id: '1', à a: '2', b: '3' } app.get('/users/:id', (request, response) => { const _idParam = request.params.id; User.findOne({_id : new ObjectId(_idParam)}).then((result) => { if (result == null) { response.status(404).send(`USER NOT FOUND`); } else { response.status(200).send(`FOUND ${result}`); } }).catch((error) => { response.status(500).send(`${error}`); }); }); FOUND { _id: new ObjectId("620fa37cda96293e589c7c64"), name: 'Trong', email: 'trong@gmail.com', password: 'trongvip', age: 22, __v: 0 } U Update One findOneAndUpdate( filter, new object, options ) new Object sẽ có dạng: { attribute: new value, attibute 2: new value } Nếu attribute nào không có trong Document cũ thì nó sẽ KHÔNG được auto thêm vào app.patch('/users/:id', async (request, response) => { const _idParam = request.params.id; try { const updatedUser = await User.findOneAndUpdate( {_id: new ObjectId(_idParam)}, request.body, {new: true, runVariables: true} ); if (updatedUser == null) { response.status(404).send("User not found"); } else { response.send(`UPDATED: ${updatedUser}`); } } catch (error) { response.status(400).send(`${error}`); } }); à Exactly Fields Bạn có thể bắt chỉ được update số field bạn mong muốn app.patch('/users/:id', async (request, response) => { const fieldsUpdated = Object.key(request.body); const fieldAllowUpdate = ['name', 'email', 'password', 'age']; const isValidField = fieldsUpdated.every(field => fieldAllowUpdate.includes(field)); if(!isValidField){ response.status(500).send("You must update exactly fields"); return; } const _idParam = request.params.id; try { const updatedUser = await User.findOneAndUpdate( {_id: new ObjectId(_idParam)}, request.body, {new: true, runVariables: true} ); if (updatedUser == null) { response.status(404).send("User not found"); } else { response.send(`UPDATED: ${updatedUser}`); } } catch (error) { response.status(400).send(`${error}`); } }); D app.delete('/users/:id', async (request,response) => { const _idParam = request.params.id; try { const deletedUser = await User.findOneAndDelete({_id: new ObjectId(_idParam)}); if (deletedUser == null) { response.status(404).send("User not found"); } else { response.status(200).send(`DELETED: ${deletedUser}`); } } catch (error) { response.status(400).send(`${error}`); } }); Express Router Nó không khác gì hết, nếu: - app = express( ) - thì router = express.Router( ) Express Router là 1 built-in class để xây dụng các endpoints (URIs). The express router class helps in the creation of route handlers. const app = express(); app.use(express.json()); app.post('/tasks', async (request, response) => { }); const app = express(); const router = new express.Router(); app.use(express.json()); app.use(router); router.post('/tasks', async (request, response) => { }); REST with Middleware Ở đây ta dùng Middelware của Express, không phải của Mongooes without Middleware: New request à Run route handler with Middleware: New request à Do something à Run route handler Bằng cách dùng Middleware, ta có thể ALLOW / DENIDED các request ta muốn, hoặc kiểm tra trong request có JWT hay không, hoặc chặn tất cả request khi trang web đang bảo trì... const app = express(); app.use((request, response, next)=>{ console.log(`REQUEST PATH: ${request.path}`); console.log(`REQUEST METHOD: ${request.method}`); next(); }); REQUEST PATH: /users/login { "email":"trongcute@gmail.com", "password":"trongcute" REQUEST METHOD: POST } Hoặc chặn tất cả request khi trang web đang bảo trì... app.use((request, response, next)=>{ response.status(503).send('Website is under maintenance'); }); Put middleware between Request Thông thường ta sẽ đặt middleware để kiểm tra Authentication trước khi một request được thực hiện export const authMiddleware = async (request, response, next) => { console.log('Auth middleware called'); next(); } import {authMiddleware} from "../middleware/auth-middleware.js"; Run First Run Second router.get('/users', authMiddleware, async (request, response) => { try { const foundUsers = await User.find({}); response.status(200).send(`FOUND ${foundUsers}`); } catch (error) { response.status(500).send(`${error}`); } }); AUTHETICATION Encrypting Password https://www.npmjs.com/package/bcryptjs const password = 'Trongvip123!'; const hashedPassword = await bcrypt.hash(password, 8); console.log(password); à Trongvip123! console.log(hashedPassword); à $2b$08$j05WdKlNgJ16OjTadJnC2u1M/fYKQccQ3EopuNdtDC8MbUv1pz7la const isMatch = await bcrypt.compare('Trongvip123!', hashedPassword); console.log(isMatch); à true Lưu ý: Phải viết syntax theo kiểu function, chứ không phải arrow function, thì this mới có giá trị là User const UserSchema = new mongoose.Schema({ password: { type: String, required: true, trim: true, minLength: 6, validate(value) { if (value.toLowerCase().includes('password')) { throw new Error('Cannot set password to "password"'); } } } }); UserSchema.pre('save', async function (next){ const user = this; /* * isModified means CREATE and UPDATE too * */ if (user.isModified('password')){ user.password = await bcrypt.hash(user.password, 8); } next(); }) router.post('/users', async (request, response) => { const user = new User(request.body); try { await user.save(); console.log(chalk.green(`CREATE ${user} into MongoDB`)); response.status(201).send(`CREATE ${user} into MongoDB`); } catch (error) { console.log(chalk.red(error)); } }); à Login import import import import mongoose from 'mongoose'; bcrypt from 'bcrypt'; validator, * as otherValidator from 'validator'; chalk from 'chalk'; const connectionURL = 'mongodb://127.0.0.1:27017'; const databaseName = 'task-app'; await mongoose.connect(`${connectionURL}/${databaseName}`, {useNewUrlParser: true}); const UserSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true, trim: true, validate(value) { if (!validator.isEmail(value)) { throw new Error('Not a right email address'); } } }, password: { type: String, required: true, trim: true, minLength: 6, validate(value) { if (value.toLowerCase().includes('password')) { throw new Error('Cannot set password to "password"'); } } }, }); UserSchema.pre('save', async function (next){ const user = this; /* * isModified means CREATE and UPDATE too * */ if (user.isModified('password')){ user.password = await bcrypt.hash(user.password, 8); } next(); }) UserSchema.statics.findByCredentials = async (email, password) => { const user = await User.findOne({email: email}); if(user == null){ throw new Error('Email or password not found'); } const isMatch = await bcrypt.compare(password,user.password); if(isMatch==false){ throw new Error('Email or password not found'); } return user; } export const User = mongoose.model('users', UserSchema); export const router = new express.Router(); /* ========================================== LOGIN ===========================================*/ router.post('/users/login', async (request, response) => { try { const foundUser = await User.findByCredentials(request.body.email, request.body.password); response.send(foundUser); } catch (error) { response.status(400).send(`Something went wrong ${error}`); } }) à SUCCESSED { "email":"trongcute@gmail.com", "password":"trongcute" } Create JWT for REST API JWT = JSON WEBSERVER TOKEN https://www.npmjs.com/package/jsonwebtoken Basic Như bạn thấy bên dưới, JWT mang thông tin được gọi là dataStoreInToken, nó được mã hoá bằng randomCharacters, từ đó khi client cung cấp JWT, ta có thể kiểm tra dataStoreInToken là gì để đăng nhập, xử lý,... • jwt.sign( ) để tạo JWT • jwt.verify( ) để lấy data từ JWT import jwt from "jsonwebtoken"; const dataStoreInToken = { _id: '1234' }; const randomCharacters = 'abcxyz'; const generatedToken = jwt.sign(dataStoreInToken, randomCharacters, {expiresIn: '7 days'}); console.log(generatedToken); à eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJfaWQiOiIxMjM0IiwiaWF0IjoxNjQ1MzM3MTMyfQ .PWqm_2RVgnEu4CWkJ-LChC4WzmCWG86BOWNnwxil70o const resolvedData = jwt.verify(generatedToken, randomCharacters); console.log(resolvedData); Use JWT for Login à { _id: '1234', iat: 1645337473, exp: 1645942628 } const UserSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true, trim: true, validate(value) { if (!validator.isEmail(value)) { throw new Error('Not a right email address'); } } }, password: { type: String, required: true, trim: true, minLength: 6, validate(value) { if (value.toLowerCase().includes('password')) { throw new Error('Cannot set password to "password"'); } } }, tokens: [{ token: { type: String } }] }); UserSchema.methods.generateAuthToken = async function () { const user = this; const dataStoreInToken = { _id: user._id }; const randomCharacters = 'web-enterprise'; const generatedAuthToken = jwt.sign(dataStoreInToken, randomCharacters, {expiresIn: '7 days'}); user.tokens = user.tokens.concat({token: generatedAuthToken}); await user.save(); return generatedAuthToken; } Ở ví dụ bên dưới, ta dùng request.user = user để thêm user Object vào Request export const authMiddleware = async (request, response, next) => { console.log('Auth middleware called'); try { const authTokenInHeader = request.header('Authorization').replace('Bearer ',''); const dataInAuthToken = jwt.verify(authTokenInHeader, 'web-enterprise'); const user = await User.findOne( { _id: dataInAuthToken._id, 'tokens.token': authTokenInHeader }); if(user==null){ throw new Error(); } request.user = user; next(); } catch (error) { response.status(401).send({error: "Authentication error"}); } } Lúc này request bạn gửi tới sẽ có thêm attribute là request.user mà Middleware đã thêm vào router.get('/users/me', authMiddleware,async (request, response) => { try { response.status(200).send(`YOU ${request.user}`); } catch (error) { response.status(500).send(`${error}`); } }); router.post('/users', async (request, response) => { const user = new User(request.body); try { await user.save(); const authToken = await user.generateAuthToken(); response.status(201).send({"user": user, "authToken": authToken}); } catch (error) { console.log(chalk.red(error)); } }); SORTING, PAGINATION, FILTERING DATA Add timestamp Trong Mongoose có hổ trợ thêm options khi define Schema, 1 trong những option đó là Timestamp const UserSchema = new mongoose.Schema({ name: { ... }, email: { ... }, password: { ... }, age: { ... }, }, { timestamps:true }); Như bạn thấy, có 2 field được thêm vào là createdAt và updatedAt { "name": "Trong", "email":"trong@gmail.com", "password":"trongcute", "age": 22 } { "user": { "name": "Trong", "email": "trong@gmail.com", "password": "$2b$08$LuuWd3MhD4n17NJ.0ULe6uS1IapLAjERygGAo5rQ0SoQI8qvwv81m", "age": 22, "_id": "6212c7ac2d8ca91170a8f43c", "sessionTokens": [], "createdAt": "2022-02-20T22:58:52.852Z", "updatedAt": "2022-02-20T22:58:52.852Z", "__v": 0 } } Filtering Data Ta muốn Client filtering Data mà họ muốn lấy thông qua URL, vì thế ta dùng request.query để phân tích data router.get('/tasks', async (request, response) => { const filter = {}; if (request.query.complete) { filter.complete = (request.query.complete === 'true'); } if (request.query.name) { filter.name = request.query.name; } try { const foundTasks = await Task.find(filter); if (foundTasks == null) { response.status(404).send(`TASKS NOT FOUND`); } else { response.status(200).send(`FOUND ${foundTasks}`); } } catch (error) { response.status(500).send(`${error}`); } }); { _id: new ObjectId("6212e34f0382897c385cee57"), name: 'first', complete: false, owner: new ObjectId("6212c7ac2d8ca91170a8f43c"), createdAt: 2022-02-21T00: 56: 47.702Z, updatedAt: 2022-02-21T00: 56: 47.702Z, __v: 0 } Pagination URL pattern để pagination là: /tasks?limit=<số item trong 1 trang> & skip=<số trang đang đứng hiện tại, bắt đầu từ 0> router.get('/tasks', async (request, response) => { const filter = {} let skip; let limit; if (request.query.complete) { filter.complete = (request.query.complete === 'true'); } if (request.query.name) { filter.name = request.query.name; } if (request.query.skip) { skip = parseInt(request.query.skip); } if (request.query.limit) { limit = parseInt(request.query.limit); } try { const foundTasks = await Task.find(filter) .skip(skip) .limit(limit); if (foundTasks == null) { response.status(404).send(`TASKS NOT FOUND`); } else { response.status(200).send(`FOUND ${foundTasks}`); } } catch (error) { response.status(500).send(`${error}`); } }); { _id: new ObjectId("6212c98973b55cac6ff3daff"), name: 'Finish BE', complete: false, owner: new ObjectId("6212c7ac2d8ca91170a8f43c"), createdAt: 2022-02-20T23: 06: 49.509Z, updatedAt: 2022-02-20T23: 06: 49.509Z, __v: 0 }, { _id: new ObjectId("6212e34f0382897c385cee57"), name: 'first', complete: false, owner: new ObjectId("6212c7ac2d8ca91170a8f43c"), createdAt: 2022-02-21T00: 56: 47.702Z, updatedAt: 2022-02-21T00: 56: 47.702Z, __v: 0 } Sorting Trong hàm sort: • -1 là DESC • 1 là ASC router.get('/tasks', async (request, response) => { try { const foundTasks = await Task.find(filter) .sort({createdAt: -1}); if (foundTasks == null) { response.status(404).send(`TASKS NOT FOUND`); } else { response.status(200).send(`FOUND ${foundTasks}`); } } catch (error) { response.status(500).send(`${error}`); } }); Từ đó, Client khi muốn sort phải gửi URL có cả phần muốn Sort và sort theo DESC hay ASC, nên ta có: /tasks?sortBy=complete_desc à { complete: -1 } /tasks?sortBy=complete_asc à { complete: 1 } router.get('/tasks', async (request, response) => { const sort = {} if (request.query.sortBy) { const parts = request.query.sortBy.split('_'); if(parts[1] === 'desc' ){ sort[parts[0]] = -1; } else { sort[parts[0]] = 1; } } try { const foundTasks = await Task.find(filter) .sort(sort); if (foundTasks == null) { response.status(404).send(`TASKS NOT FOUND`); } else { response.status(200).send(`FOUND ${foundTasks}`); } } catch (error) { response.status(500).send(`${error}`); } }); FILE UPLOAD VALIDATOR.JS https://www.npmjs.com/package/validator import * as validator from 'validator'; isEmail( ) console.log(validator.isEmail('foo@bar.com')); true console.log(validator.isEmail('foo@.com')); false USEFUL LIBRARIES chalk.js https://www.npmjs.com/package/chalk import chalk from 'chalk'; console.log(chalk.blue('Hello') + ' World' + chalk.red('!')); console.log(chalk.yellow.bgRed('Warning')); console.log(chalk.whiteBright.bgGreen('Success')); nodemon.js https://www.npmjs.com/package/nodemon Thư viện giúp NodeJS tự restarting khi có file thay đổi. Như bạn thấy, Node Application luôn running và bất kỳ thay đổi gì nó cũng hiện trên terminal DEPLOY APPLICATION COMMON ERROR problem process.dlopen(module, path.toNamespacedPath(filename)) solve npm rebuild bcrypt --build-from-source