├── clase-5 ├── .gitignore ├── test │ └── test.js ├── utils.js ├── server-with-mysql.js ├── server-with-local.js ├── middlewares │ └── cors.js ├── routes │ └── movies.js ├── package.json ├── app.js ├── api.http ├── schemas │ └── movies.js ├── models │ ├── local-file-system │ │ └── movie.js │ ├── mongodb │ │ └── movie.js │ └── mysql │ │ └── movie.js ├── web │ └── index.html ├── controllers │ └── movies.js ├── movies.json └── pnpm-lock.yaml ├── clase-4 ├── models │ ├── mysql │ │ └── movie.js │ ├── local-file-system │ │ └── movie.js │ └── mongodb │ │ └── movie.js ├── utils.js ├── routes │ └── movies.js ├── package.json ├── app.js ├── middlewares │ └── cors.js ├── api.http ├── schemas │ └── movies.js ├── web │ └── index.html ├── controllers │ └── movies.js └── movies.json ├── clase-1 ├── archivo.txt ├── archivo2.txt ├── cjs │ ├── index.js │ └── sum.js ├── mjs │ ├── sum.mjs │ └── index.mjs ├── 6.ls.js ├── 2.fs-stat.js ├── 7.process.js ├── 3.fs-readFile-sync.js ├── 3.fs-readFile.js ├── 4.fs-async-await-parallel.mjs ├── 1.os-info.mjs ├── 1.os-info.js ├── 9.http.js ├── 5.path.js ├── 4.fs-async-await.mjs ├── 10.free-port.js ├── 4.fs-promises.js ├── package.json ├── 4.fs-async-await.js └── 8.ls-advanced.js ├── .gitignore ├── clase-2 ├── placa.png ├── api.http ├── package.json ├── 1.http.js ├── 3.express.js ├── 2.routing.js └── pokemon │ └── ditto.json ├── clase-6 ├── 02-http.webp ├── 01-comparativa.webp ├── 03-websockets.webp ├── .env ├── package.json ├── server │ └── index.js ├── client │ └── index.html └── README.md ├── clase-3 ├── package.json ├── api.http ├── schemas │ └── movies.js ├── web │ └── index.html ├── app.js └── movies.json ├── package.json └── README.md /clase-5/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /clase-5/test/test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clase-4/models/mysql/movie.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clase-1/archivo.txt: -------------------------------------------------------------------------------- 1 | lorem ipsum ooooooh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/package-lock.json -------------------------------------------------------------------------------- /clase-2/placa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/curso-node-js/main/clase-2/placa.png -------------------------------------------------------------------------------- /clase-6/02-http.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/curso-node-js/main/clase-6/02-http.webp -------------------------------------------------------------------------------- /clase-1/archivo2.txt: -------------------------------------------------------------------------------- 1 | vamoacodear 2 | maikychplcok 3 | pheralb 4 | meisstykun 5 | rumertovar 6 | obsynotfound -------------------------------------------------------------------------------- /clase-6/01-comparativa.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/curso-node-js/main/clase-6/01-comparativa.webp -------------------------------------------------------------------------------- /clase-6/03-websockets.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/curso-node-js/main/clase-6/03-websockets.webp -------------------------------------------------------------------------------- /clase-1/cjs/index.js: -------------------------------------------------------------------------------- 1 | // CommonJS require module 2 | const { sum } = require('./sum') 3 | 4 | console.log(sum(1, 2)) -------------------------------------------------------------------------------- /clase-1/cjs/sum.js: -------------------------------------------------------------------------------- 1 | function sum (a, b) { 2 | return a + b 3 | } 4 | 5 | // CommonJS Module Export 6 | module.exports = { 7 | sum 8 | } -------------------------------------------------------------------------------- /clase-4/utils.js: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | const require = createRequire(import.meta.url) 3 | 4 | export const readJSON = (path) => require(path) 5 | -------------------------------------------------------------------------------- /clase-5/utils.js: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | const require = createRequire(import.meta.url) 3 | 4 | export const readJSON = (path) => require(path) 5 | -------------------------------------------------------------------------------- /clase-5/server-with-mysql.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app.js' 2 | 3 | import { MovieModel } from './models/mysql/movie.js' 4 | 5 | createApp({ movieModel: MovieModel }) 6 | -------------------------------------------------------------------------------- /clase-5/server-with-local.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app.js' 2 | 3 | import { MovieModel } from './models/local-file-system/movie.js' 4 | 5 | createApp({ movieModel: MovieModel }) 6 | -------------------------------------------------------------------------------- /clase-1/mjs/sum.mjs: -------------------------------------------------------------------------------- 1 | export function sum (a, b) { 2 | return a + b 3 | } 4 | 5 | export function sub (a, b) { 6 | return a - b 7 | } 8 | 9 | export function mult (a, b) { 10 | return a * b 11 | } -------------------------------------------------------------------------------- /clase-6/.env: -------------------------------------------------------------------------------- 1 | DB_TOKEN="eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIyMDIzLTEwLTEyVDE3OjE3OjU5Ljc1MTM5NDUxMloiLCJpZCI6ImM1NmU5YjFmLTY5MjItMTFlZS04NTUzLWVhYTc3MTVhZWFmMiJ9.FRtOTGsCb_OJQeIYnPkiX-2nG3vfejJalCaaq3lJ-1A1R4jqO5XNsTjNkN0unj_9Pfky0fu-l-9NNi_Uk7jxAQ" -------------------------------------------------------------------------------- /clase-1/mjs/index.mjs: -------------------------------------------------------------------------------- 1 | // .js -> por defecto utiliza CommonJS 2 | // .mjs -> para utilizar ES Modules 3 | // .cjs -> para utilizar CommonJS 4 | 5 | import { sum, sub, mult } from './sum.mjs' 6 | 7 | console.log(sum(1, 2)) 8 | console.log(sub(1, 2)) 9 | console.log(mult(1, 2)) -------------------------------------------------------------------------------- /clase-1/6.ls.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs/promises') 2 | 3 | fs.readdir('.') 4 | .then(files => { 5 | files.forEach(file => { 6 | console.log(file) 7 | }) 8 | }) 9 | .catch(err => { 10 | if (err) { 11 | console.error('Error al leer el directorio: ', err) 12 | return; 13 | } 14 | }) -------------------------------------------------------------------------------- /clase-2/api.http: -------------------------------------------------------------------------------- 1 | ### Recuperar información del pokemon ditto 2 | GET http://localhost:1234/pokemon/ditto 3 | 4 | ### Para crear un Pokemon 5 | POST http://localhost:1234/pokemon 6 | Content-Type: application/json 7 | 8 | { 9 | "name": "ditto", 10 | "type": "normal", 11 | "moves": [ 12 | "transform" 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /clase-1/2.fs-stat.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs') // a partir de Node 16, se recomienda poner node: 2 | 3 | const stats = fs.statSync('./archivo.txt') 4 | 5 | console.log( 6 | stats.isFile(), // si es un fichero 7 | stats.isDirectory(), // si es un directorio 8 | stats.isSymbolicLink(), // si es un enlace simbólico 9 | stats.size // tamaño en bytes 10 | ) 11 | -------------------------------------------------------------------------------- /clase-1/7.process.js: -------------------------------------------------------------------------------- 1 | // argumentos de entrada 2 | // console.log(process.argv) 3 | 4 | // controlar el proceso y su salida 5 | // process.exit(1) 6 | 7 | // podemos controlar eventos del proceso 8 | // process.on('exit', () => { 9 | // // limpiar los recursos 10 | // }) 11 | 12 | // current working directory 13 | console.log(process.cwd()) 14 | 15 | // platform 16 | console.log(process.env.PEPITO) -------------------------------------------------------------------------------- /clase-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clase-3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "cors": "2.8.5", 14 | "express": "4.18.2", 15 | "zod": "3.21.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "curso-node", 3 | "version": "1.0.0", 4 | "description": "
", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "standard": "^17.1.0" 14 | }, 15 | "eslintConfig": { 16 | "extends": "standard" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /clase-1/3.fs-readFile-sync.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs') 2 | 3 | console.log('Leyendo el primer archivo...') 4 | const text = fs.readFileSync('./archivo.txt', 'utf-8') 5 | console.log('primer texto:', text) 6 | 7 | console.log('--> Hacer cosas mientras lee el archivo...') 8 | 9 | console.log('Leyendo el segundo archivo...') 10 | const secondText = fs.readFileSync('./archivo2.txt', 'utf-8') 11 | console.log('segundo texto:', secondText) 12 | -------------------------------------------------------------------------------- /clase-4/routes/movies.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | import { MovieController } from '../controllers/movies.js' 4 | 5 | export const moviesRouter = Router() 6 | 7 | moviesRouter.get('/', MovieController.getAll) 8 | moviesRouter.post('/', MovieController.create) 9 | 10 | moviesRouter.get('/:id', MovieController.getById) 11 | moviesRouter.delete('/:id', MovieController.delete) 12 | moviesRouter.patch('/:id', MovieController.update) 13 | -------------------------------------------------------------------------------- /clase-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clase-3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "cors": "2.8.5", 15 | "express": "4.18.2", 16 | "mongodb": "5.7.0", 17 | "zod": "3.21.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /clase-1/3.fs-readFile.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs') 2 | 3 | console.log('Leyendo el primer archivo...') 4 | fs.readFile('./archivo.txt', 'utf-8', (err, text) => { // <---- ejecutas este callback 5 | console.log('primer texto:', text) 6 | }) 7 | 8 | console.log('--> Hacer cosas mientras lee el archivo...') 9 | 10 | console.log('Leyendo el segundo archivo...') 11 | fs.readFile('./archivo2.txt', 'utf-8', (err, text) => { 12 | console.log('segundo texto:', text) 13 | }) 14 | -------------------------------------------------------------------------------- /clase-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clase-2", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "1.http.js", 6 | "scripts": { 7 | "dev:1": "nodemon 1.http.js", 8 | "dev:2": "nodemon 2.routing.js", 9 | "dev:3": "node --watch 3.express.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "nodemon": "^3.0.1" 16 | }, 17 | "dependencies": { 18 | "express": "4.18.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /clase-4/app.js: -------------------------------------------------------------------------------- 1 | import express, { json } from 'express' // require -> commonJS 2 | import { moviesRouter } from './routes/movies.js' 3 | import { corsMiddleware } from './middlewares/cors.js' 4 | 5 | const app = express() 6 | app.use(json()) 7 | app.disable('x-powered-by') 8 | 9 | app.use('/movies', moviesRouter) 10 | 11 | const PORT = process.env.PORT ?? 1234 12 | 13 | app.listen(PORT, () => { 14 | console.log(`server listening on port http://localhost:${PORT}`) 15 | }) 16 | -------------------------------------------------------------------------------- /clase-1/4.fs-async-await-parallel.mjs: -------------------------------------------------------------------------------- 1 | // Esto sólo en los módulos nativos 2 | // que no tienen promesas nativas 3 | 4 | // const { promisify } = require('node:util') 5 | // const readFilePromise = promisify(fs.readFile) 6 | 7 | import { readFile } from 'node:fs/promises' 8 | 9 | Promise.all([ 10 | readFile('./archivo.txt', 'utf-8'), 11 | readFile('./archivo2.txt', 'utf-8') 12 | ]).then(([text, secondText]) => { 13 | console.log('primer texto:', text) 14 | console.log('segundo texto:', secondText) 15 | }) -------------------------------------------------------------------------------- /clase-1/1.os-info.mjs: -------------------------------------------------------------------------------- 1 | import os from 'node:os' 2 | 3 | console.log('Información del sistema operativo:') 4 | console.log('-------------------') 5 | 6 | console.log('Nombre del sistema operativo', os.platform()) 7 | console.log('Versión del sistema operativo', os.release()) 8 | console.log('Arquitectura', os.arch()) 9 | console.log('CPUs', os.cpus()) 10 | console.log('Memoria libre', os.freemem() / 1024 / 1024) 11 | console.log('Memoria total', os.totalmem() / 1024 / 1024) 12 | console.log('uptime', os.uptime() / 60 / 60) 13 | -------------------------------------------------------------------------------- /clase-1/1.os-info.js: -------------------------------------------------------------------------------- 1 | const os = require('node:os') 2 | 3 | console.log('Información del sistema operativo:') 4 | console.log('-------------------') 5 | 6 | console.log('Nombre del sistema operativo', os.platform()) 7 | console.log('Versión del sistema operativo', os.release()) 8 | console.log('Arquitectura', os.arch()) 9 | console.log('CPUs', os.cpus()) 10 | console.log('Memoria libre', os.freemem() / 1024 / 1024) 11 | console.log('Memoria total', os.totalmem() / 1024 / 1024) 12 | console.log('uptime', os.uptime() / 60 / 60) 13 | -------------------------------------------------------------------------------- /clase-1/9.http.js: -------------------------------------------------------------------------------- 1 | const http = require('node:http') // protocolo HTTP 2 | const { findAvailablePort } = require('./10.free-port.js') 3 | 4 | console.log(process.env) 5 | 6 | const desiredPort = process.env.PORT ?? 3000 7 | 8 | const server = http.createServer((req, res) => { 9 | console.log('request received') 10 | res.end('Hola mundo') 11 | }) 12 | 13 | findAvailablePort(desiredPort).then(port => { 14 | server.listen(port, () => { 15 | console.log(`server listening on port http://localhost:${port}`) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /clase-6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clase-6", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "node --watch ./server/index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@libsql/client": "^0.3.5", 16 | "dotenv": "^16.3.1", 17 | "express": "4.18.2", 18 | "morgan": "1.10.0", 19 | "socket.io": "4.7.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /clase-1/5.path.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path') 2 | 3 | // barra separadora de carpetas segun SO 4 | console.log(path.sep) 5 | 6 | // unir rutas con path.join 7 | const filePath = path.join('content', 'subfolder', 'test.txt') 8 | console.log(filePath) 9 | 10 | const base = path.basename('/tmp/midu-secret-files/password.txt') 11 | console.log(base) 12 | 13 | const filename = path.basename('/tmp/midu-secret-files/password.txt', '.txt') 14 | console.log(filename) 15 | 16 | const extension = path.extname('my.super.image.jpg') 17 | console.log(extension) -------------------------------------------------------------------------------- /clase-4/middlewares/cors.js: -------------------------------------------------------------------------------- 1 | import cors from 'cors' 2 | 3 | const ACCEPTED_ORIGINS = [ 4 | 'http://localhost:8080', 5 | 'http://localhost:1234', 6 | 'https://movies.com', 7 | 'https://midu.dev' 8 | ] 9 | 10 | export const corsMiddleware = ({ acceptedOrigins = ACCEPTED_ORIGINS } = {}) => cors({ 11 | origin: (origin, callback) => { 12 | if (acceptedOrigins.includes(origin)) { 13 | return callback(null, true) 14 | } 15 | 16 | if (!origin) { 17 | return callback(null, true) 18 | } 19 | 20 | return callback(new Error('Not allowed by CORS')) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /clase-5/middlewares/cors.js: -------------------------------------------------------------------------------- 1 | import cors from 'cors' 2 | 3 | const ACCEPTED_ORIGINS = [ 4 | 'http://localhost:8080', 5 | 'http://localhost:1234', 6 | 'https://movies.com', 7 | 'https://midu.dev' 8 | ] 9 | 10 | export const corsMiddleware = ({ acceptedOrigins = ACCEPTED_ORIGINS } = {}) => cors({ 11 | origin: (origin, callback) => { 12 | if (acceptedOrigins.includes(origin)) { 13 | return callback(null, true) 14 | } 15 | 16 | if (!origin) { 17 | return callback(null, true) 18 | } 19 | 20 | return callback(new Error('Not allowed by CORS')) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /clase-5/routes/movies.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { MovieController } from '../controllers/movies.js' 3 | 4 | export const createMovieRouter = ({ movieModel }) => { 5 | const moviesRouter = Router() 6 | 7 | const movieController = new MovieController({ movieModel }) 8 | 9 | moviesRouter.get('/', movieController.getAll) 10 | moviesRouter.post('/', movieController.create) 11 | 12 | moviesRouter.get('/:id', movieController.getById) 13 | moviesRouter.delete('/:id', movieController.delete) 14 | moviesRouter.patch('/:id', movieController.update) 15 | 16 | return moviesRouter 17 | } 18 | -------------------------------------------------------------------------------- /clase-5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clase-3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "type": "module", 7 | "scripts": { 8 | "start:mysql": "node --watch server-with-mysql.js", 9 | "start:local": "node --watch server-with-local.js", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "cors": "2.8.5", 17 | "dotenv": "16.3.1", 18 | "express": "4.18.2", 19 | "mongodb": "5.7.0", 20 | "mysql2": "^3.6.0", 21 | "zod": "3.21.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /clase-1/4.fs-async-await.mjs: -------------------------------------------------------------------------------- 1 | // Esto sólo en los módulos nativos 2 | // que no tienen promesas nativas 3 | 4 | // const { promisify } = require('node:util') 5 | // const readFilePromise = promisify(fs.readFile) 6 | 7 | import { readFile } from 'node:fs/promises' 8 | 9 | console.log('Leyendo el primer archivo...') 10 | const text = await readFile('./archivo.txt', 'utf-8') 11 | console.log('primer texto:', text) 12 | console.log('--> Hacer cosas mientras lee el archivo...') 13 | 14 | console.log('Leyendo el segundo archivo...') 15 | const secondText = await readFile('./archivo2.txt', 'utf-8') 16 | console.log('segundo texto:', secondText) 17 | -------------------------------------------------------------------------------- /clase-5/app.js: -------------------------------------------------------------------------------- 1 | import express, { json } from 'express' // require -> commonJS 2 | import { createMovieRouter } from './routes/movies.js' 3 | import { corsMiddleware } from './middlewares/cors.js' 4 | import 'dotenv/config' 5 | 6 | // después 7 | export const createApp = ({ movieModel }) => { 8 | const app = express() 9 | app.use(json()) 10 | app.use(corsMiddleware()) 11 | app.disable('x-powered-by') 12 | 13 | app.use('/movies', createMovieRouter({ movieModel })) 14 | 15 | const PORT = process.env.PORT ?? 1234 16 | 17 | app.listen(PORT, () => { 18 | console.log(`server listening on port http://localhost:${PORT}`) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /clase-1/10.free-port.js: -------------------------------------------------------------------------------- 1 | const net = require('node:net') 2 | 3 | function findAvailablePort (desiredPort) { 4 | return new Promise((resolve, reject) => { 5 | const server = net.createServer() 6 | 7 | server.listen(desiredPort, () => { 8 | const { port } = server.address() 9 | server.close(() => { 10 | resolve(port) 11 | }) 12 | }) 13 | 14 | server.on('error', (err) => { 15 | if (err.code === 'EADDRINUSE') { 16 | findAvailablePort(0).then(port => resolve(port)) 17 | } else { 18 | reject(err) 19 | } 20 | }) 21 | }) 22 | } 23 | 24 | module.exports = { findAvailablePort } 25 | -------------------------------------------------------------------------------- /clase-1/4.fs-promises.js: -------------------------------------------------------------------------------- 1 | // Esto sólo en los módulos nativos 2 | // que no tienen promesas nativas 3 | 4 | // const { promisify } = require('node:util') 5 | // const readFilePromise = promisify(fs.readFile) 6 | 7 | const fs = require('node:fs/promises') 8 | 9 | console.log('Leyendo el primer archivo...') 10 | fs.readFile('./archivo.txt', 'utf-8') 11 | .then(text => { 12 | console.log('primer texto:', text) 13 | }) 14 | 15 | console.log('--> Hacer cosas mientras lee el archivo...') 16 | 17 | console.log('Leyendo el segundo archivo...') 18 | fs.readFile('./archivo2.txt', 'utf-8') 19 | .then(text => { 20 | console.log('segundo texto:', text) 21 | }) 22 | -------------------------------------------------------------------------------- /clase-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "curso-node", 3 | "version": "1.0.0", 4 | "description": "El mejor curso de Node por 0€", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/midudev/curso-node-js.git" 12 | }, 13 | "keywords": [ 14 | "curso", 15 | "node", 16 | "nodejs", 17 | "midudev" 18 | ], 19 | "author": "Miguel Ángel Durán García - midudev", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/midudev/curso-node-js/issues" 23 | }, 24 | "homepage": "https://github.com/midudev/curso-node-js#readme", 25 | "dependencies": { 26 | "picocolors": "1.0.0" 27 | }, 28 | "devDependencies": { 29 | "standard": "17.1.0" 30 | }, 31 | "eslintConfig": { 32 | "extends": "standard" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /clase-3/api.http: -------------------------------------------------------------------------------- 1 | ### Recuperar todas las películas 2 | GET http://localhost:1234/movies 3 | 4 | ### Recuperar una película por id 5 | GET http://localhost:1234/movies/c8a7d63f-3b04-44d3-9d95-8782fd7dcfaf 6 | 7 | ### Recuperar todas las películas por un género 8 | GET http://localhost:1234/movies?genre=ACTION 9 | 10 | ### Crear una película con POST 11 | POST http://localhost:1234/movies 12 | Content-Type: application/json 13 | 14 | { 15 | "sql": "SELECT * FROM users", 16 | "title": "The Godfather", 17 | "year": 1975, 18 | "director": "Francis Ford Coppola", 19 | "duration": 175, 20 | "poster": "https://img.fruugo.com/product/4/49/14441494_max.jpg", 21 | "genre": [ 22 | "Crime", 23 | "Drama" 24 | ] 25 | } 26 | 27 | ### Actualizar una película 28 | PATCH http://localhost:1234/movies/dcdd0fad-a94c-4810-8acc-5f108d3b18c3 29 | Content-Type: application/json 30 | 31 | { 32 | "year": 2022 33 | } -------------------------------------------------------------------------------- /clase-4/api.http: -------------------------------------------------------------------------------- 1 | ### Recuperar todas las películas 2 | GET http://localhost:1234/movies 3 | 4 | ### Recuperar una película por id 5 | GET http://localhost:1234/movies/64d4d8825d614a4ec5984ae3 6 | 7 | ### Recuperar todas las películas por un género 8 | GET http://localhost:1234/movies?genre=ACTION 9 | 10 | ### Crear una película con POST 11 | POST http://localhost:1234/movies 12 | Content-Type: application/json 13 | 14 | { 15 | "sql": "SELECT * FROM users", 16 | "title": "The Godfather", 17 | "year": 1975, 18 | "director": "Francis Ford Coppola", 19 | "duration": 175, 20 | "poster": "https://img.fruugo.com/product/4/49/14441494_max.jpg", 21 | "genre": [ 22 | "Crime", 23 | "Drama" 24 | ] 25 | } 26 | 27 | ### Borrar una película 28 | DELETE http://localhost:1234/movies/64d4d8825d614a4ec5984ae3 29 | 30 | ### Actualizar una película 31 | PATCH http://localhost:1234/movies/64d4da882f83563ab4f40632 32 | Content-Type: application/json 33 | 34 | { 35 | "year": 2022 36 | } -------------------------------------------------------------------------------- /clase-5/api.http: -------------------------------------------------------------------------------- 1 | ### Recuperar todas las películas 2 | GET http://localhost:1234/movies 3 | 4 | ### Recuperar una película por id 5 | GET http://localhost:1234/movies/1990df63-4da7-11ee-aedf-32323730fdc3 6 | 7 | ### Recuperar todas las películas por un género 8 | GET http://localhost:1234/movies?genre=ACTION 9 | 10 | ### Crear una película con POST 11 | POST http://localhost:1234/movies 12 | Content-Type: application/json 13 | 14 | { 15 | "sql": "SELECT * FROM users", 16 | "title": "The Godfather", 17 | "year": 1975, 18 | "director": "Francis Ford Coppola", 19 | "duration": 175, 20 | "poster": "https://img.fruugo.com/product/4/49/14441494_max.jpg", 21 | "genre": [ 22 | "Crime", 23 | "Drama" 24 | ] 25 | } 26 | 27 | ### Borrar una película 28 | DELETE http://localhost:1234/movies/64d4d8825d614a4ec5984ae3 29 | 30 | ### Actualizar una película 31 | PATCH http://localhost:1234/movies/64d4da882f83563ab4f40632 32 | Content-Type: application/json 33 | 34 | { 35 | "year": 2022 36 | } -------------------------------------------------------------------------------- /clase-4/schemas/movies.js: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | 3 | const movieSchema = z.object({ 4 | title: z.string({ 5 | invalid_type_error: 'Movie title must be a string', 6 | required_error: 'Movie title is required.' 7 | }), 8 | year: z.number().int().min(1900).max(2024), 9 | director: z.string(), 10 | duration: z.number().int().positive(), 11 | rate: z.number().min(0).max(10).default(5), 12 | poster: z.string().url({ 13 | message: 'Poster must be a valid URL' 14 | }), 15 | genre: z.array( 16 | z.enum(['Action', 'Adventure', 'Crime', 'Comedy', 'Drama', 'Fantasy', 'Horror', 'Thriller', 'Sci-Fi']), 17 | { 18 | required_error: 'Movie genre is required.', 19 | invalid_type_error: 'Movie genre must be an array of enum Genre' 20 | } 21 | ) 22 | }) 23 | 24 | export function validateMovie (input) { 25 | return movieSchema.safeParse(input) 26 | } 27 | 28 | export function validatePartialMovie (input) { 29 | return movieSchema.partial().safeParse(input) 30 | } 31 | -------------------------------------------------------------------------------- /clase-5/schemas/movies.js: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | 3 | const movieSchema = z.object({ 4 | title: z.string({ 5 | invalid_type_error: 'Movie title must be a string', 6 | required_error: 'Movie title is required.' 7 | }), 8 | year: z.number().int().min(1900).max(2024), 9 | director: z.string(), 10 | duration: z.number().int().positive(), 11 | rate: z.number().min(0).max(10).default(5), 12 | poster: z.string().url({ 13 | message: 'Poster must be a valid URL' 14 | }), 15 | genre: z.array( 16 | z.enum(['Action', 'Adventure', 'Crime', 'Comedy', 'Drama', 'Fantasy', 'Horror', 'Thriller', 'Sci-Fi']), 17 | { 18 | required_error: 'Movie genre is required.', 19 | invalid_type_error: 'Movie genre must be an array of enum Genre' 20 | } 21 | ) 22 | }) 23 | 24 | export function validateMovie (input) { 25 | return movieSchema.safeParse(input) 26 | } 27 | 28 | export function validatePartialMovie (input) { 29 | return movieSchema.partial().safeParse(input) 30 | } 31 | -------------------------------------------------------------------------------- /clase-2/1.http.js: -------------------------------------------------------------------------------- 1 | const http = require('node:http') // protocolo HTTP 2 | const fs = require('node:fs') 3 | 4 | const desiredPort = process.env.PORT ?? 1234 5 | 6 | const processRequest = (req, res) => { 7 | res.setHeader('Content-Type', 'text/html; charset=utf-8') 8 | 9 | if (req.url === '/') { 10 | res.end('

Mi página

') 11 | } else if (req.url === '/imagen-super-bonita.png') { 12 | fs.readFile('./placa.png', (err, data) => { 13 | if (err) { 14 | res.statusCode = 500 15 | res.end('

500 Internal Server Error

') 16 | } else { 17 | res.setHeader('Content-Type', 'image/png') 18 | res.end(data) 19 | } 20 | }) 21 | } else if (req.url === '/contacto') { 22 | res.end('

Contacto

') 23 | } else { 24 | res.statusCode = 404 // Not Found 25 | res.end('

404

') 26 | } 27 | } 28 | 29 | const server = http.createServer(processRequest) 30 | 31 | server.listen(desiredPort, () => { 32 | console.log(`server listening on port http://localhost:${desiredPort}`) 33 | }) 34 | -------------------------------------------------------------------------------- /clase-3/schemas/movies.js: -------------------------------------------------------------------------------- 1 | const z = require('zod') 2 | 3 | const movieSchema = z.object({ 4 | title: z.string({ 5 | invalid_type_error: 'Movie title must be a string', 6 | required_error: 'Movie title is required.' 7 | }), 8 | year: z.number().int().min(1900).max(2024), 9 | director: z.string(), 10 | duration: z.number().int().positive(), 11 | rate: z.number().min(0).max(10).default(5), 12 | poster: z.string().url({ 13 | message: 'Poster must be a valid URL' 14 | }), 15 | genre: z.array( 16 | z.enum(['Action', 'Adventure', 'Crime', 'Comedy', 'Drama', 'Fantasy', 'Horror', 'Thriller', 'Sci-Fi']), 17 | { 18 | required_error: 'Movie genre is required.', 19 | invalid_type_error: 'Movie genre must be an array of enum Genre' 20 | } 21 | ) 22 | }) 23 | 24 | function validateMovie (input) { 25 | return movieSchema.safeParse(input) 26 | } 27 | 28 | function validatePartialMovie (input) { 29 | return movieSchema.partial().safeParse(input) 30 | } 31 | 32 | module.exports = { 33 | validateMovie, 34 | validatePartialMovie 35 | } 36 | -------------------------------------------------------------------------------- /clase-1/4.fs-async-await.js: -------------------------------------------------------------------------------- 1 | // Esto sólo en los módulos nativos 2 | // que no tienen promesas nativas 3 | 4 | // const { promisify } = require('node:util') 5 | // const readFilePromise = promisify(fs.readFile) 6 | 7 | const { readFile } = require('node:fs/promises') 8 | 9 | async function init () { 10 | console.log('Leyendo el primer archivo...') 11 | const text = await readFile('./archivo.txt', 'utf-8') 12 | console.log('primer texto:', text) 13 | console.log('--> Hacer cosas mientras lee el archivo...') 14 | 15 | console.log('Leyendo el segundo archivo...') 16 | const secondText = await readFile('./archivo2.txt', 'utf-8') 17 | console.log('segundo texto:', secondText) 18 | } 19 | 20 | init() 21 | 22 | // IIFE - Inmediatly Invoked Function Expression 23 | // ;( 24 | // async () => { 25 | // console.log('Leyendo el primer archivo...') 26 | // const text = await readFile('./archivo.txt', 'utf-8') 27 | // console.log('primer texto:', text) 28 | // console.log('--> Hacer cosas mientras lee el archivo...') 29 | 30 | // console.log('Leyendo el segundo archivo...') 31 | // const secondText = await readFile('./archivo2.txt', 'utf-8') 32 | // console.log('segundo texto:', secondText) 33 | // } 34 | // )() 35 | 36 | -------------------------------------------------------------------------------- /clase-1/8.ls-advanced.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs/promises') 2 | const path = require('node:path') 3 | const pc = require('picocolors') 4 | 5 | const folder = process.argv[2] ?? '.' 6 | 7 | async function ls (folder) { 8 | let files 9 | try { 10 | files = await fs.readdir(folder) 11 | } catch { 12 | console.error(pc.red(`❌ No se pudo leer el directorio ${folder}`)) 13 | process.exit(1) 14 | } 15 | 16 | const filesPromises = files.map(async file => { 17 | const filePath = path.join(folder, file) 18 | let stats 19 | 20 | try { 21 | stats = await fs.stat(filePath) // status - información del archivo 22 | } catch { 23 | console.error(`No se pudo leer el archivo ${filePath}`) 24 | process.exit(1) 25 | } 26 | 27 | const isDirectory = stats.isDirectory() 28 | const fileType = isDirectory ? 'd' : 'f' 29 | const fileSize = stats.size.toString() 30 | const fileModified = stats.mtime.toLocaleString() 31 | 32 | return `${pc.bgMagenta(fileType)} ${pc.blue(file.padEnd(20))} ${pc.green(fileSize.padStart(10))} ${pc.yellow(fileModified)}` 33 | }) 34 | 35 | const filesInfo = await Promise.all(filesPromises) 36 | 37 | filesInfo.forEach(fileInfo => console.log(fileInfo)) 38 | } 39 | 40 | ls(folder) 41 | -------------------------------------------------------------------------------- /clase-4/models/local-file-system/movie.js: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | import { readJSON } from '../../utils.js' 3 | 4 | const movies = readJSON('./movies.json') 5 | 6 | export class MovieModel { 7 | static async getAll ({ genre }) { 8 | if (genre) { 9 | return movies.filter( 10 | movie => movie.genre.some(g => g.toLowerCase() === genre.toLowerCase()) 11 | ) 12 | } 13 | 14 | return movies 15 | } 16 | 17 | static async getById ({ id }) { 18 | const movie = movies.find(movie => movie.id === id) 19 | return movie 20 | } 21 | 22 | static async create ({ input }) { 23 | const newMovie = { 24 | id: randomUUID(), 25 | ...input 26 | } 27 | 28 | movies.push(newMovie) 29 | 30 | return newMovie 31 | } 32 | 33 | static async delete ({ id }) { 34 | const movieIndex = movies.findIndex(movie => movie.id === id) 35 | if (movieIndex === -1) return false 36 | 37 | movies.splice(movieIndex, 1) 38 | return true 39 | } 40 | 41 | static async update ({ id, input }) { 42 | const movieIndex = movies.findIndex(movie => movie.id === id) 43 | if (movieIndex === -1) return false 44 | 45 | movies[movieIndex] = { 46 | ...movies[movieIndex], 47 | ...input 48 | } 49 | 50 | return movies[movieIndex] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /clase-5/models/local-file-system/movie.js: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | import { readJSON } from '../../utils.js' 3 | 4 | const movies = readJSON('./movies.json') 5 | 6 | export class MovieModel { 7 | static async getAll ({ genre }) { 8 | if (genre) { 9 | return movies.filter( 10 | movie => movie.genre.some(g => g.toLowerCase() === genre.toLowerCase()) 11 | ) 12 | } 13 | 14 | return movies 15 | } 16 | 17 | static async getById ({ id }) { 18 | const movie = movies.find(movie => movie.id === id) 19 | return movie 20 | } 21 | 22 | static async create ({ input }) { 23 | const newMovie = { 24 | id: randomUUID(), 25 | ...input 26 | } 27 | 28 | movies.push(newMovie) 29 | 30 | return newMovie 31 | } 32 | 33 | static async delete ({ id }) { 34 | const movieIndex = movies.findIndex(movie => movie.id === id) 35 | if (movieIndex === -1) return false 36 | 37 | movies.splice(movieIndex, 1) 38 | return true 39 | } 40 | 41 | static async update ({ id, input }) { 42 | const movieIndex = movies.findIndex(movie => movie.id === id) 43 | if (movieIndex === -1) return false 44 | 45 | movies[movieIndex] = { 46 | ...movies[movieIndex], 47 | ...input 48 | } 49 | 50 | return movies[movieIndex] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /clase-2/3.express.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const ditto = require('./pokemon/ditto.json') 3 | const path = require('path') 4 | 5 | const PORT = process.env.PORT ?? 1234 6 | 7 | const app = express() 8 | app.disable('x-powered-by') 9 | 10 | app.use(express.json()) 11 | 12 | // app.use((req, res, next) => { 13 | // if (req.method !== 'POST') return next() 14 | // if (req.headers['content-type'] !== 'application/json') return next() 15 | 16 | // // solo llegan request que son POST y que tienen el header Content-Type: application/json 17 | // let body = '' 18 | 19 | // // escuchar el evento data 20 | // req.on('data', chunk => { 21 | // body += chunk.toString() 22 | // }) 23 | 24 | // req.on('end', () => { 25 | // const data = JSON.parse(body) 26 | // data.timestamp = Date.now() 27 | // // mutar la request y meter la información en el req.body 28 | // req.body = data 29 | // next() 30 | // }) 31 | // }) 32 | 33 | app.get('/pokemon/ditto', (req, res) => { 34 | res.json(ditto) 35 | }) 36 | 37 | app.post('/pokemon', (req, res) => { 38 | // req.body deberíamos guardar en bbdd 39 | res.status(201).json(req.body) 40 | }) 41 | 42 | // la última a la que va a llegar 43 | app.use((req, res) => { 44 | res.status(404).send('

404

') 45 | }) 46 | 47 | app.listen(PORT, () => { 48 | console.log(`server listening on port http://localhost:${PORT}`) 49 | }) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Curso de Node.js desde cero y práctico 4 | 5 | # Curso de Node.js desde cero ❇️ 6 | 7 | Curso para aprender **Node** de forma práctica 8 | **[Todos los jueves a las 18PM 🇪🇸 en Twitch](https://twitch.tv/midudev)** 9 |
10 | 11 | ## 📹 Videos con las clases 12 | 13 | - 01: [Introducción a Node.js y módulos](https://www.youtube.com/watch?v=yB4n_K7dZV8) 14 | - 02: [Creamos una API desde cero](https://www.youtube.com/watch?v=YmZE1HXjpd4) 15 | - 03: [Solución de CORS y desarrollo de API](https://www.youtube.com/watch?v=-9d3KhCqOtU) 16 | - 04: [Arquitectura MVC y despligue de API](https://www.youtube.com/watch?v=ev3Yxva4wI4) 17 | - 05: [Crea un Base de Datos MySQL y evita los hackers (buenas prácticas)](https://www.youtube.com/watch?v=eCWNQfzuuso&list=RDCMUC3aj05GEEyzdOqYM5FLSFeg&index=3) 18 | - 06: [Chat en TIEMPO REAL con Node.js, Socket.io, SQL, HTML y CSS](https://www.youtube.com/watch?v=WpbBhTx5R9Q&list=PLUofhDIg_38qm2oPOV-IRTTEKyrVBBaU7&index=6) 19 | - 07: [Aprende Autenticación de Usuario, Sesión, Cookies y JWT con Node.js](https://www.youtube.com/watch?v=UqnnhAZxRac&list=PLUofhDIg_38qm2oPOV-IRTTEKyrVBBaU7&index=7) 20 | 21 | ## Horario por países: 22 | 23 | - 18H 🇪🇸 24 | - 17H 🇮🇨 25 | - 13H 🇺🇾 🇦🇷 🇨🇱 26 | - 12H 🇵🇾 🇹🇹 🇧🇴 🇻🇪 🇩🇴 27 | - 11H 🇨🇴 🇵🇪 🇪🇨 🇵🇦 28 | - 10H 🇲🇽 🇨🇷 🇳🇮 🇸🇻 🇭🇳 29 | -------------------------------------------------------------------------------- /clase-3/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Probar API Rest 7 | 20 | 56 | 57 | 58 |
59 | 60 | -------------------------------------------------------------------------------- /clase-4/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Probar API Rest 7 | 20 | 56 | 57 | 58 |
59 | 60 | -------------------------------------------------------------------------------- /clase-5/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Probar API Rest 7 | 20 | 56 | 57 | 58 |
59 | 60 | -------------------------------------------------------------------------------- /clase-2/2.routing.js: -------------------------------------------------------------------------------- 1 | const http = require('node:http') 2 | 3 | // commonJS -> modulos clásicos de node 4 | const dittoJSON = require('./pokemon/ditto.json') 5 | 6 | const processRequest = (req, res) => { 7 | const { method, url } = req 8 | 9 | switch (method) { 10 | case 'GET': 11 | switch (url) { 12 | case '/pokemon/ditto': 13 | 14 | res.setHeader('Content-Type', 'application/json; charset=utf-8') 15 | return res.end(JSON.stringify(dittoJSON)) 16 | default: 17 | res.statusCode = 404 18 | res.setHeader('Content-Type', 'text/html; charset=utf-8') 19 | return res.end('

404

') 20 | } 21 | 22 | case 'POST': 23 | switch (url) { 24 | case '/pokemon': { 25 | let body = '' 26 | 27 | // escuchar el evento data 28 | req.on('data', chunk => { 29 | body += chunk.toString() 30 | }) 31 | 32 | req.on('end', () => { 33 | const data = JSON.parse(body) 34 | // llamar a una base de datos para guardar la info 35 | res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' }) 36 | 37 | data.timestamp = Date.now() 38 | res.end(JSON.stringify(data)) 39 | }) 40 | 41 | break 42 | } 43 | 44 | default: 45 | res.statusCode = 404 46 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 47 | return res.end('404 Not Found') 48 | } 49 | } 50 | } 51 | 52 | const server = http.createServer(processRequest) 53 | 54 | server.listen(1234, () => { 55 | console.log('server listening on port http://localhost:1234') 56 | }) 57 | -------------------------------------------------------------------------------- /clase-5/controllers/movies.js: -------------------------------------------------------------------------------- 1 | import { validateMovie, validatePartialMovie } from '../schemas/movies.js' 2 | 3 | export class MovieController { 4 | constructor ({ movieModel }) { 5 | this.movieModel = movieModel 6 | } 7 | 8 | getAll = async (req, res) => { 9 | const { genre } = req.query 10 | const movies = await this.movieModel.getAll({ genre }) 11 | res.json(movies) 12 | } 13 | 14 | getById = async (req, res) => { 15 | const { id } = req.params 16 | const movie = await this.movieModel.getById({ id }) 17 | if (movie) return res.json(movie) 18 | res.status(404).json({ message: 'Movie not found' }) 19 | } 20 | 21 | create = async (req, res) => { 22 | const result = validateMovie(req.body) 23 | 24 | if (!result.success) { 25 | // 422 Unprocessable Entity 26 | return res.status(400).json({ error: JSON.parse(result.error.message) }) 27 | } 28 | 29 | const newMovie = await this.movieModel.create({ input: result.data }) 30 | 31 | res.status(201).json(newMovie) 32 | } 33 | 34 | delete = async (req, res) => { 35 | const { id } = req.params 36 | 37 | const result = await this.movieModel.delete({ id }) 38 | 39 | if (result === false) { 40 | return res.status(404).json({ message: 'Movie not found' }) 41 | } 42 | 43 | return res.json({ message: 'Movie deleted' }) 44 | } 45 | 46 | update = async (req, res) => { 47 | const result = validatePartialMovie(req.body) 48 | 49 | if (!result.success) { 50 | return res.status(400).json({ error: JSON.parse(result.error.message) }) 51 | } 52 | 53 | const { id } = req.params 54 | 55 | const updatedMovie = await this.movieModel.update({ id, input: result.data }) 56 | 57 | return res.json(updatedMovie) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /clase-4/controllers/movies.js: -------------------------------------------------------------------------------- 1 | import { MovieModel } from '../models/local-file-system/movie.js' 2 | // import { MovieModel } from '../models/database/movie.js' 3 | import { validateMovie, validatePartialMovie } from '../schemas/movies.js' 4 | 5 | export class MovieController { 6 | static async getAll (req, res) { 7 | const { genre } = req.query 8 | const movies = await MovieModel.getAll({ genre }) 9 | res.json(movies) 10 | } 11 | 12 | static async getById (req, res) { 13 | const { id } = req.params 14 | const movie = await MovieModel.getById({ id }) 15 | if (movie) return res.json(movie) 16 | res.status(404).json({ message: 'Movie not found' }) 17 | } 18 | 19 | static async create (req, res) { 20 | const result = validateMovie(req.body) 21 | 22 | if (!result.success) { 23 | // 422 Unprocessable Entity 24 | return res.status(400).json({ error: JSON.parse(result.error.message) }) 25 | } 26 | 27 | const newMovie = await MovieModel.create({ input: result.data }) 28 | 29 | res.status(201).json(newMovie) 30 | } 31 | 32 | static async delete (req, res) { 33 | const { id } = req.params 34 | 35 | const result = await MovieModel.delete({ id }) 36 | 37 | if (result === false) { 38 | return res.status(404).json({ message: 'Movie not found' }) 39 | } 40 | 41 | return res.json({ message: 'Movie deleted' }) 42 | } 43 | 44 | static async update (req, res) { 45 | const result = validatePartialMovie(req.body) 46 | 47 | if (!result.success) { 48 | return res.status(400).json({ error: JSON.parse(result.error.message) }) 49 | } 50 | 51 | const { id } = req.params 52 | 53 | const updatedMovie = await MovieModel.update({ id, input: result.data }) 54 | 55 | return res.json(updatedMovie) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /clase-4/models/mongodb/movie.js: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId, ServerApiVersion } from 'mongodb' 2 | const uri = 'mongodb+srv://user:???@cluster0.dhwmu.mongodb.net/?retryWrites=true&w=majority' 3 | 4 | // Create a MongoClient with a MongoClientOptions object to set the Stable API version 5 | const client = new MongoClient(uri, { 6 | serverApi: { 7 | version: ServerApiVersion.v1, 8 | strict: true, 9 | deprecationErrors: true 10 | } 11 | }) 12 | 13 | async function connect () { 14 | try { 15 | await client.connect() 16 | const database = client.db('database') 17 | return database.collection('movies') 18 | } catch (error) { 19 | console.error('Error connecting to the database') 20 | console.error(error) 21 | await client.close() 22 | } 23 | } 24 | 25 | export class MovieModel { 26 | static async getAll ({ genre }) { 27 | const db = await connect() 28 | 29 | if (genre) { 30 | return db.find({ 31 | genre: { 32 | $elemMatch: { 33 | $regex: genre, 34 | $options: 'i' 35 | } 36 | } 37 | }).toArray() 38 | } 39 | 40 | return db.find({}).toArray() 41 | } 42 | 43 | static async getById ({ id }) { 44 | const db = await connect() 45 | const objectId = new ObjectId(id) 46 | return db.findOne({ _id: objectId }) 47 | } 48 | 49 | static async create ({ input }) { 50 | const db = await connect() 51 | 52 | const { insertedId } = await db.insertOne(input) 53 | 54 | return { 55 | id: insertedId, 56 | ...input 57 | } 58 | } 59 | 60 | static async delete ({ id }) { 61 | const db = await connect() 62 | const objectId = new ObjectId(id) 63 | const { deletedCount } = await db.deleteOne({ _id: objectId }) 64 | return deletedCount > 0 65 | } 66 | 67 | static async update ({ id, input }) { 68 | const db = await connect() 69 | const objectId = new ObjectId(id) 70 | 71 | const { ok, value } = await db.findOneAndUpdate({ _id: objectId }, { $set: input }, { returnNewDocument: true }) 72 | 73 | if (!ok) return false 74 | 75 | return value 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /clase-5/models/mongodb/movie.js: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId, ServerApiVersion } from 'mongodb' 2 | const uri = 'mongodb+srv://user:???@cluster0.dhwmu.mongodb.net/?retryWrites=true&w=majority' 3 | 4 | // Create a MongoClient with a MongoClientOptions object to set the Stable API version 5 | const client = new MongoClient(uri, { 6 | serverApi: { 7 | version: ServerApiVersion.v1, 8 | strict: true, 9 | deprecationErrors: true 10 | } 11 | }) 12 | 13 | async function connect () { 14 | try { 15 | await client.connect() 16 | const database = client.db('database') 17 | return database.collection('movies') 18 | } catch (error) { 19 | console.error('Error connecting to the database') 20 | console.error(error) 21 | await client.close() 22 | } 23 | } 24 | 25 | export class MovieModel { 26 | static async getAll ({ genre }) { 27 | const db = await connect() 28 | 29 | if (genre) { 30 | return db.find({ 31 | genre: { 32 | $elemMatch: { 33 | $regex: genre, 34 | $options: 'i' 35 | } 36 | } 37 | }).toArray() 38 | } 39 | 40 | return db.find({}).toArray() 41 | } 42 | 43 | static async getById ({ id }) { 44 | const db = await connect() 45 | const objectId = new ObjectId(id) 46 | return db.findOne({ _id: objectId }) 47 | } 48 | 49 | static async create ({ input }) { 50 | const db = await connect() 51 | 52 | const { insertedId } = await db.insertOne(input) 53 | 54 | return { 55 | id: insertedId, 56 | ...input 57 | } 58 | } 59 | 60 | static async delete ({ id }) { 61 | const db = await connect() 62 | const objectId = new ObjectId(id) 63 | const { deletedCount } = await db.deleteOne({ _id: objectId }) 64 | return deletedCount > 0 65 | } 66 | 67 | static async update ({ id, input }) { 68 | const db = await connect() 69 | const objectId = new ObjectId(id) 70 | 71 | const { ok, value } = await db.findOneAndUpdate({ _id: objectId }, { $set: input }, { returnNewDocument: true }) 72 | 73 | if (!ok) return false 74 | 75 | return value 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /clase-6/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import logger from 'morgan' 3 | import dotenv from 'dotenv' 4 | import { createClient } from '@libsql/client' 5 | 6 | import { Server } from 'socket.io' 7 | import { createServer } from 'node:http' 8 | 9 | dotenv.config() 10 | 11 | const port = process.env.PORT ?? 3000 12 | 13 | const app = express() 14 | const server = createServer(app) 15 | const io = new Server(server, { 16 | connectionStateRecovery: {} 17 | }) 18 | 19 | const db = createClient({ 20 | url: 'libsql://cuddly-wasp-midudev.turso.io', 21 | authToken: process.env.DB_TOKEN 22 | }) 23 | 24 | await db.execute(` 25 | CREATE TABLE IF NOT EXISTS messages ( 26 | id INTEGER PRIMARY KEY AUTOINCREMENT, 27 | content TEXT, 28 | user TEXT 29 | ) 30 | `) 31 | 32 | io.on('connection', async (socket) => { 33 | console.log('a user has connected!') 34 | 35 | socket.on('disconnect', () => { 36 | console.log('an user has disconnected') 37 | }) 38 | 39 | socket.on('chat message', async (msg) => { 40 | let result 41 | const username = socket.handshake.auth.username ?? 'anonymous' 42 | console.log({ username }) 43 | try { 44 | result = await db.execute({ 45 | sql: 'INSERT INTO messages (content, user) VALUES (:msg, :username)', 46 | args: { msg, username } 47 | }) 48 | } catch (e) { 49 | console.error(e) 50 | return 51 | } 52 | 53 | io.emit('chat message', msg, result.lastInsertRowid.toString(), username) 54 | }) 55 | 56 | if (!socket.recovered) { // <- recuperase los mensajes sin conexión 57 | try { 58 | const results = await db.execute({ 59 | sql: 'SELECT id, content, user FROM messages WHERE id > ?', 60 | args: [socket.handshake.auth.serverOffset ?? 0] 61 | }) 62 | 63 | results.rows.forEach(row => { 64 | socket.emit('chat message', row.content, row.id.toString(), row.user) 65 | }) 66 | } catch (e) { 67 | console.error(e) 68 | } 69 | } 70 | }) 71 | 72 | app.use(logger('dev')) 73 | 74 | app.get('/', (req, res) => { 75 | res.sendFile(process.cwd() + '/client/index.html') 76 | }) 77 | 78 | server.listen(port, () => { 79 | console.log(`Server running on port ${port}`) 80 | }) 81 | -------------------------------------------------------------------------------- /clase-5/models/mysql/movie.js: -------------------------------------------------------------------------------- 1 | import mysql from 'mysql2/promise' 2 | 3 | const DEFAULT_CONFIG = { 4 | host: 'localhost', 5 | user: 'root', 6 | port: 3306, 7 | password: '', 8 | database: 'moviesdb' 9 | } 10 | const connectionString = process.env.DATABASE_URL ?? DEFAULT_CONFIG 11 | 12 | const connection = await mysql.createConnection(connectionString) 13 | 14 | export class MovieModel { 15 | static async getAll ({ genre }) { 16 | console.log('getAll') 17 | 18 | if (genre) { 19 | const lowerCaseGenre = genre.toLowerCase() 20 | 21 | // get genre ids from database table using genre names 22 | const [genres] = await connection.query( 23 | 'SELECT id, name FROM genre WHERE LOWER(name) = ?;', 24 | [lowerCaseGenre] 25 | ) 26 | 27 | // no genre found 28 | if (genres.length === 0) return [] 29 | 30 | // get the id from the first genre result 31 | const [{ id }] = genres 32 | 33 | // get all movies ids from database table 34 | // la query a movie_genres 35 | // join 36 | // y devolver resultados.. 37 | return [] 38 | } 39 | 40 | const [movies] = await connection.query( 41 | 'SELECT title, year, director, duration, poster, rate, BIN_TO_UUID(id) id FROM movie;' 42 | ) 43 | 44 | return movies 45 | } 46 | 47 | static async getById ({ id }) { 48 | const [movies] = await connection.query( 49 | `SELECT title, year, director, duration, poster, rate, BIN_TO_UUID(id) id 50 | FROM movie WHERE id = UUID_TO_BIN(?);`, 51 | [id] 52 | ) 53 | 54 | if (movies.length === 0) return null 55 | 56 | return movies[0] 57 | } 58 | 59 | static async create ({ input }) { 60 | const { 61 | genre: genreInput, // genre is an array 62 | title, 63 | year, 64 | duration, 65 | director, 66 | rate, 67 | poster 68 | } = input 69 | 70 | // todo: crear la conexión de genre 71 | 72 | // crypto.randomUUID() 73 | const [uuidResult] = await connection.query('SELECT UUID() uuid;') 74 | const [{ uuid }] = uuidResult 75 | 76 | try { 77 | await connection.query( 78 | `INSERT INTO movie (id, title, year, director, duration, poster, rate) 79 | VALUES (UUID_TO_BIN("${uuid}"), ?, ?, ?, ?, ?, ?);`, 80 | [title, year, director, duration, poster, rate] 81 | ) 82 | } catch (e) { 83 | // puede enviarle información sensible 84 | throw new Error('Error creating movie') 85 | // enviar la traza a un servicio interno 86 | // sendLog(e) 87 | } 88 | 89 | const [movies] = await connection.query( 90 | `SELECT title, year, director, duration, poster, rate, BIN_TO_UUID(id) id 91 | FROM movie WHERE id = UUID_TO_BIN(?);`, 92 | [uuid] 93 | ) 94 | 95 | return movies[0] 96 | } 97 | 98 | static async delete ({ id }) { 99 | // ejercio fácil: crear el delete 100 | } 101 | 102 | static async update ({ id, input }) { 103 | // ejercicio fácil: crear el update 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /clase-3/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') // require -> commonJS 2 | const crypto = require('node:crypto') 3 | const cors = require('cors') 4 | 5 | const movies = require('./movies.json') 6 | const { validateMovie, validatePartialMovie } = require('./schemas/movies') 7 | 8 | const app = express() 9 | app.use(express.json()) 10 | app.use(cors({ 11 | origin: (origin, callback) => { 12 | const ACCEPTED_ORIGINS = [ 13 | 'http://localhost:8080', 14 | 'http://localhost:1234', 15 | 'https://movies.com', 16 | 'https://midu.dev' 17 | ] 18 | 19 | if (ACCEPTED_ORIGINS.includes(origin)) { 20 | return callback(null, true) 21 | } 22 | 23 | if (!origin) { 24 | return callback(null, true) 25 | } 26 | 27 | return callback(new Error('Not allowed by CORS')) 28 | } 29 | })) 30 | app.disable('x-powered-by') // deshabilitar el header X-Powered-By: Express 31 | 32 | // métodos normales: GET/HEAD/POST 33 | // métodos complejos: PUT/PATCH/DELETE 34 | 35 | // CORS PRE-Flight 36 | // OPTIONS 37 | 38 | // Todos los recursos que sean MOVIES se identifica con /movies 39 | app.get('/movies', (req, res) => { 40 | const { genre } = req.query 41 | if (genre) { 42 | const filteredMovies = movies.filter( 43 | movie => movie.genre.some(g => g.toLowerCase() === genre.toLowerCase()) 44 | ) 45 | return res.json(filteredMovies) 46 | } 47 | res.json(movies) 48 | }) 49 | 50 | app.get('/movies/:id', (req, res) => { 51 | const { id } = req.params 52 | const movie = movies.find(movie => movie.id === id) 53 | if (movie) return res.json(movie) 54 | res.status(404).json({ message: 'Movie not found' }) 55 | }) 56 | 57 | app.post('/movies', (req, res) => { 58 | const result = validateMovie(req.body) 59 | 60 | if (!result.success) { 61 | // 422 Unprocessable Entity 62 | return res.status(400).json({ error: JSON.parse(result.error.message) }) 63 | } 64 | 65 | // en base de datos 66 | const newMovie = { 67 | id: crypto.randomUUID(), // uuid v4 68 | ...result.data 69 | } 70 | 71 | // Esto no sería REST, porque estamos guardando 72 | // el estado de la aplicación en memoria 73 | movies.push(newMovie) 74 | 75 | res.status(201).json(newMovie) 76 | }) 77 | 78 | app.delete('/movies/:id', (req, res) => { 79 | const { id } = req.params 80 | const movieIndex = movies.findIndex(movie => movie.id === id) 81 | 82 | if (movieIndex === -1) { 83 | return res.status(404).json({ message: 'Movie not found' }) 84 | } 85 | 86 | movies.splice(movieIndex, 1) 87 | 88 | return res.json({ message: 'Movie deleted' }) 89 | }) 90 | 91 | app.patch('/movies/:id', (req, res) => { 92 | const result = validatePartialMovie(req.body) 93 | 94 | if (!result.success) { 95 | return res.status(400).json({ error: JSON.parse(result.error.message) }) 96 | } 97 | 98 | const { id } = req.params 99 | const movieIndex = movies.findIndex(movie => movie.id === id) 100 | 101 | if (movieIndex === -1) { 102 | return res.status(404).json({ message: 'Movie not found' }) 103 | } 104 | 105 | const updateMovie = { 106 | ...movies[movieIndex], 107 | ...result.data 108 | } 109 | 110 | movies[movieIndex] = updateMovie 111 | 112 | return res.json(updateMovie) 113 | }) 114 | 115 | const PORT = process.env.PORT ?? 1234 116 | 117 | app.listen(PORT, () => { 118 | console.log(`server listening on port http://localhost:${PORT}`) 119 | }) 120 | -------------------------------------------------------------------------------- /clase-6/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | miduChat 8 | 9 | 57 | 58 | 140 | 141 | 142 | 143 |
144 | 145 |
146 | 147 | 148 |
149 |
150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /clase-3/movies.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "dcdd0fad-a94c-4810-8acc-5f108d3b18c3", 3 | "title": "The Shawshank Redemption", 4 | "year": 1994, 5 | "director": "Frank Darabont", 6 | "duration": 142, 7 | "poster": "https://i.ebayimg.com/images/g/4goAAOSwMyBe7hnQ/s-l1200.webp", 8 | "genre": ["Drama"], 9 | "rate": 9.3 10 | }, 11 | { 12 | "id": "c8a7d63f-3b04-44d3-9d95-8782fd7dcfaf", 13 | "title": "The Dark Knight", 14 | "year": 2008, 15 | "director": "Christopher Nolan", 16 | "duration": 152, 17 | "poster": "https://i.ebayimg.com/images/g/yokAAOSw8w1YARbm/s-l1200.jpg", 18 | "genre": ["Action", "Crime", "Drama"], 19 | "rate": 9.0 20 | }, 21 | { 22 | "id": "5ad1a235-0d9c-410a-b32b-220d91689a08", 23 | "title": "Inception", 24 | "year": 2010, 25 | "director": "Christopher Nolan", 26 | "duration": 148, 27 | "poster": "https://m.media-amazon.com/images/I/91Rc8cAmnAL._AC_UF1000,1000_QL80_.jpg", 28 | "genre": ["Action", "Adventure", "Sci-Fi"], 29 | "rate": 8.8 30 | }, 31 | { 32 | "id": "241bf55d-b649-4109-af7c-0e6890ded3fc", 33 | "title": "Pulp Fiction", 34 | "year": 1994, 35 | "director": "Quentin Tarantino", 36 | "duration": 154, 37 | "poster": "https://www.themoviedb.org/t/p/original/vQWk5YBFWF4bZaofAbv0tShwBvQ.jpg", 38 | "genre": ["Crime", "Drama"], 39 | "rate": 8.9 40 | }, 41 | { 42 | "id": "9e6106f0-848b-4810-a11a-3d832a5610f9", 43 | "title": "Forrest Gump", 44 | "year": 1994, 45 | "director": "Robert Zemeckis", 46 | "duration": 142, 47 | "poster": "https://i.ebayimg.com/images/g/qR8AAOSwkvRZzuMD/s-l1600.jpg", 48 | "genre": ["Drama", "Romance"], 49 | "rate": 8.8 50 | }, 51 | { 52 | "id": "7e3fd5ab-60ff-4ae2-92b6-9597f0308d1", 53 | "title": "Gladiator", 54 | "year": 2000, 55 | "director": "Ridley Scott", 56 | "duration": 155, 57 | "poster": "https://img.fruugo.com/product/0/60/14417600_max.jpg", 58 | "genre": ["Action", "Adventure", "Drama"], 59 | "rate": 8.5 60 | }, 61 | { 62 | "id": "c906673b-3948-4402-ac7f-73ac3a9e3105", 63 | "title": "The Matrix", 64 | "year": 1999, 65 | "director": "Lana Wachowski", 66 | "duration": 136, 67 | "poster": "https://i.ebayimg.com/images/g/QFQAAOSwAQpfjaA6/s-l1200.jpg", 68 | "genre": ["Action", "Sci-Fi"], 69 | "rate": 8.7 70 | }, 71 | { 72 | "id": "b6e03689-cccd-478e-8565-d92f40813b13", 73 | "title": "Interstellar", 74 | "year": 2014, 75 | "director": "Christopher Nolan", 76 | "duration": 169, 77 | "poster": "https://m.media-amazon.com/images/I/91obuWzA3XL._AC_UF1000,1000_QL80_.jpg", 78 | "genre": ["Adventure", "Drama", "Sci-Fi"], 79 | "rate": 8.6 80 | }, 81 | { 82 | "id": "aa391090-b938-42eb-b520-86ea0aa3917b", 83 | "title": "The Lord of the Rings: The Return of the King", 84 | "year": 2003, 85 | "director": "Peter Jackson", 86 | "duration": 201, 87 | "poster": "https://i.ebayimg.com/images/g/0hoAAOSwe7peaMLW/s-l1600.jpg", 88 | "genre": ["Action", "Adventure", "Drama"], 89 | "rate": 8.9 90 | }, 91 | { 92 | "id": "2e6900e2-0b48-4fb6-ad48-09c7086e54fe", 93 | "title": "The Lion King", 94 | "year": 1994, 95 | "director": "Roger Allers, Rob Minkoff", 96 | "duration": 88, 97 | "poster": "https://m.media-amazon.com/images/I/81BMmrwSFOL._AC_UF1000,1000_QL80_.jpg", 98 | "genre": ["Animation", "Adventure", "Drama"], 99 | "rate": 8.5 100 | }, 101 | { 102 | "id": "04986507-b3ed-442c-8ae7-4c5df804f896", 103 | "title": "The Avengers", 104 | "year": 2012, 105 | "director": "Joss Whedon", 106 | "duration": 143, 107 | "poster": "https://img.fruugo.com/product/7/41/14532417_max.jpg", 108 | "genre": ["Action", "Adventure", "Sci-Fi"], 109 | "rate": 8.0 110 | }, 111 | { 112 | "id": "7d2832f8-c70a-410e-8963-4c93bf36cc9c", 113 | "title": "Jurassic Park", 114 | "year": 1993, 115 | "director": "Steven Spielberg", 116 | "duration": 127, 117 | "poster": "https://vice-press.com/cdn/shop/products/Jurassic-Park-Editions-poster-florey.jpg?v=1654518755&width=1024", 118 | "genre": ["Adventure", "Sci-Fi"], 119 | "rate": 8.1 120 | }, 121 | { 122 | "id": "ccf36f2e-8566-47f7-912d-9f4647250bc7", 123 | "title": "Titanic", 124 | "year": 1997, 125 | "director": "James Cameron", 126 | "duration": 195, 127 | "poster": "https://i.pinimg.com/originals/42/42/65/4242658e6f1b0d6322a4a93e0383108b.png", 128 | "genre": ["Drama", "Romance"], 129 | "rate": 7.8 130 | }, 131 | { 132 | "id": "8fb17ae1-bdfe-45e5-a871-4772d7e526b8", 133 | "title": "The Social Network", 134 | "year": 2010, 135 | "director": "David Fincher", 136 | "duration": 120, 137 | "poster": "https://i.pinimg.com/originals/7e/37/b9/7e37b994b613e94cba64f307b1983e39.jpg", 138 | "genre": ["Biography", "Drama"], 139 | "rate": 7.7 140 | }, 141 | { 142 | "id": "6a360a18-c645-4b47-9a7b-2a71babbf3e0", 143 | "title": "Avatar", 144 | "year": 2009, 145 | "director": "James Cameron", 146 | "duration": 162, 147 | "poster": "https://i.etsystatic.com/35681979/r/il/dfe3ba/3957859451/il_fullxfull.3957859451_h27r.jpg", 148 | "genre": ["Action", "Adventure", "Fantasy"], 149 | "rate": 7.8 150 | }] -------------------------------------------------------------------------------- /clase-4/movies.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "dcdd0fad-a94c-4810-8acc-5f108d3b18c3", 3 | "title": "The Shawshank Redemption", 4 | "year": 1994, 5 | "director": "Frank Darabont", 6 | "duration": 142, 7 | "poster": "https://i.ebayimg.com/images/g/4goAAOSwMyBe7hnQ/s-l1200.webp", 8 | "genre": ["Drama"], 9 | "rate": 9.3 10 | }, 11 | { 12 | "id": "c8a7d63f-3b04-44d3-9d95-8782fd7dcfaf", 13 | "title": "The Dark Knight", 14 | "year": 2008, 15 | "director": "Christopher Nolan", 16 | "duration": 152, 17 | "poster": "https://i.ebayimg.com/images/g/yokAAOSw8w1YARbm/s-l1200.jpg", 18 | "genre": ["Action", "Crime", "Drama"], 19 | "rate": 9.0 20 | }, 21 | { 22 | "id": "5ad1a235-0d9c-410a-b32b-220d91689a08", 23 | "title": "Inception", 24 | "year": 2010, 25 | "director": "Christopher Nolan", 26 | "duration": 148, 27 | "poster": "https://m.media-amazon.com/images/I/91Rc8cAmnAL._AC_UF1000,1000_QL80_.jpg", 28 | "genre": ["Action", "Adventure", "Sci-Fi"], 29 | "rate": 8.8 30 | }, 31 | { 32 | "id": "241bf55d-b649-4109-af7c-0e6890ded3fc", 33 | "title": "Pulp Fiction", 34 | "year": 1994, 35 | "director": "Quentin Tarantino", 36 | "duration": 154, 37 | "poster": "https://www.themoviedb.org/t/p/original/vQWk5YBFWF4bZaofAbv0tShwBvQ.jpg", 38 | "genre": ["Crime", "Drama"], 39 | "rate": 8.9 40 | }, 41 | { 42 | "id": "9e6106f0-848b-4810-a11a-3d832a5610f9", 43 | "title": "Forrest Gump", 44 | "year": 1994, 45 | "director": "Robert Zemeckis", 46 | "duration": 142, 47 | "poster": "https://i.ebayimg.com/images/g/qR8AAOSwkvRZzuMD/s-l1600.jpg", 48 | "genre": ["Drama", "Romance"], 49 | "rate": 8.8 50 | }, 51 | { 52 | "id": "7e3fd5ab-60ff-4ae2-92b6-9597f0308d1", 53 | "title": "Gladiator", 54 | "year": 2000, 55 | "director": "Ridley Scott", 56 | "duration": 155, 57 | "poster": "https://img.fruugo.com/product/0/60/14417600_max.jpg", 58 | "genre": ["Action", "Adventure", "Drama"], 59 | "rate": 8.5 60 | }, 61 | { 62 | "id": "c906673b-3948-4402-ac7f-73ac3a9e3105", 63 | "title": "The Matrix", 64 | "year": 1999, 65 | "director": "Lana Wachowski", 66 | "duration": 136, 67 | "poster": "https://i.ebayimg.com/images/g/QFQAAOSwAQpfjaA6/s-l1200.jpg", 68 | "genre": ["Action", "Sci-Fi"], 69 | "rate": 8.7 70 | }, 71 | { 72 | "id": "b6e03689-cccd-478e-8565-d92f40813b13", 73 | "title": "Interstellar", 74 | "year": 2014, 75 | "director": "Christopher Nolan", 76 | "duration": 169, 77 | "poster": "https://m.media-amazon.com/images/I/91obuWzA3XL._AC_UF1000,1000_QL80_.jpg", 78 | "genre": ["Adventure", "Drama", "Sci-Fi"], 79 | "rate": 8.6 80 | }, 81 | { 82 | "id": "aa391090-b938-42eb-b520-86ea0aa3917b", 83 | "title": "The Lord of the Rings: The Return of the King", 84 | "year": 2003, 85 | "director": "Peter Jackson", 86 | "duration": 201, 87 | "poster": "https://i.ebayimg.com/images/g/0hoAAOSwe7peaMLW/s-l1600.jpg", 88 | "genre": ["Action", "Adventure", "Drama"], 89 | "rate": 8.9 90 | }, 91 | { 92 | "id": "2e6900e2-0b48-4fb6-ad48-09c7086e54fe", 93 | "title": "The Lion King", 94 | "year": 1994, 95 | "director": "Roger Allers, Rob Minkoff", 96 | "duration": 88, 97 | "poster": "https://m.media-amazon.com/images/I/81BMmrwSFOL._AC_UF1000,1000_QL80_.jpg", 98 | "genre": ["Animation", "Adventure", "Drama"], 99 | "rate": 8.5 100 | }, 101 | { 102 | "id": "04986507-b3ed-442c-8ae7-4c5df804f896", 103 | "title": "The Avengers", 104 | "year": 2012, 105 | "director": "Joss Whedon", 106 | "duration": 143, 107 | "poster": "https://img.fruugo.com/product/7/41/14532417_max.jpg", 108 | "genre": ["Action", "Adventure", "Sci-Fi"], 109 | "rate": 8.0 110 | }, 111 | { 112 | "id": "7d2832f8-c70a-410e-8963-4c93bf36cc9c", 113 | "title": "Jurassic Park", 114 | "year": 1993, 115 | "director": "Steven Spielberg", 116 | "duration": 127, 117 | "poster": "https://vice-press.com/cdn/shop/products/Jurassic-Park-Editions-poster-florey.jpg?v=1654518755&width=1024", 118 | "genre": ["Adventure", "Sci-Fi"], 119 | "rate": 8.1 120 | }, 121 | { 122 | "id": "ccf36f2e-8566-47f7-912d-9f4647250bc7", 123 | "title": "Titanic", 124 | "year": 1997, 125 | "director": "James Cameron", 126 | "duration": 195, 127 | "poster": "https://i.pinimg.com/originals/42/42/65/4242658e6f1b0d6322a4a93e0383108b.png", 128 | "genre": ["Drama", "Romance"], 129 | "rate": 7.8 130 | }, 131 | { 132 | "id": "8fb17ae1-bdfe-45e5-a871-4772d7e526b8", 133 | "title": "The Social Network", 134 | "year": 2010, 135 | "director": "David Fincher", 136 | "duration": 120, 137 | "poster": "https://i.pinimg.com/originals/7e/37/b9/7e37b994b613e94cba64f307b1983e39.jpg", 138 | "genre": ["Biography", "Drama"], 139 | "rate": 7.7 140 | }, 141 | { 142 | "id": "6a360a18-c645-4b47-9a7b-2a71babbf3e0", 143 | "title": "Avatar", 144 | "year": 2009, 145 | "director": "James Cameron", 146 | "duration": 162, 147 | "poster": "https://i.etsystatic.com/35681979/r/il/dfe3ba/3957859451/il_fullxfull.3957859451_h27r.jpg", 148 | "genre": ["Action", "Adventure", "Fantasy"], 149 | "rate": 7.8 150 | }] -------------------------------------------------------------------------------- /clase-5/movies.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "dcdd0fad-a94c-4810-8acc-5f108d3b18c3", 3 | "title": "The Shawshank Redemption", 4 | "year": 1994, 5 | "director": "Frank Darabont", 6 | "duration": 142, 7 | "poster": "https://i.ebayimg.com/images/g/4goAAOSwMyBe7hnQ/s-l1200.webp", 8 | "genre": ["Drama"], 9 | "rate": 9.3 10 | }, 11 | { 12 | "id": "c8a7d63f-3b04-44d3-9d95-8782fd7dcfaf", 13 | "title": "The Dark Knight", 14 | "year": 2008, 15 | "director": "Christopher Nolan", 16 | "duration": 152, 17 | "poster": "https://i.ebayimg.com/images/g/yokAAOSw8w1YARbm/s-l1200.jpg", 18 | "genre": ["Action", "Crime", "Drama"], 19 | "rate": 9.0 20 | }, 21 | { 22 | "id": "5ad1a235-0d9c-410a-b32b-220d91689a08", 23 | "title": "Inception", 24 | "year": 2010, 25 | "director": "Christopher Nolan", 26 | "duration": 148, 27 | "poster": "https://m.media-amazon.com/images/I/91Rc8cAmnAL._AC_UF1000,1000_QL80_.jpg", 28 | "genre": ["Action", "Adventure", "Sci-Fi"], 29 | "rate": 8.8 30 | }, 31 | { 32 | "id": "241bf55d-b649-4109-af7c-0e6890ded3fc", 33 | "title": "Pulp Fiction", 34 | "year": 1994, 35 | "director": "Quentin Tarantino", 36 | "duration": 154, 37 | "poster": "https://www.themoviedb.org/t/p/original/vQWk5YBFWF4bZaofAbv0tShwBvQ.jpg", 38 | "genre": ["Crime", "Drama"], 39 | "rate": 8.9 40 | }, 41 | { 42 | "id": "9e6106f0-848b-4810-a11a-3d832a5610f9", 43 | "title": "Forrest Gump", 44 | "year": 1994, 45 | "director": "Robert Zemeckis", 46 | "duration": 142, 47 | "poster": "https://i.ebayimg.com/images/g/qR8AAOSwkvRZzuMD/s-l1600.jpg", 48 | "genre": ["Drama", "Romance"], 49 | "rate": 8.8 50 | }, 51 | { 52 | "id": "7e3fd5ab-60ff-4ae2-92b6-9597f0308d1", 53 | "title": "Gladiator", 54 | "year": 2000, 55 | "director": "Ridley Scott", 56 | "duration": 155, 57 | "poster": "https://img.fruugo.com/product/0/60/14417600_max.jpg", 58 | "genre": ["Action", "Adventure", "Drama"], 59 | "rate": 8.5 60 | }, 61 | { 62 | "id": "c906673b-3948-4402-ac7f-73ac3a9e3105", 63 | "title": "The Matrix", 64 | "year": 1999, 65 | "director": "Lana Wachowski", 66 | "duration": 136, 67 | "poster": "https://i.ebayimg.com/images/g/QFQAAOSwAQpfjaA6/s-l1200.jpg", 68 | "genre": ["Action", "Sci-Fi"], 69 | "rate": 8.7 70 | }, 71 | { 72 | "id": "b6e03689-cccd-478e-8565-d92f40813b13", 73 | "title": "Interstellar", 74 | "year": 2014, 75 | "director": "Christopher Nolan", 76 | "duration": 169, 77 | "poster": "https://m.media-amazon.com/images/I/91obuWzA3XL._AC_UF1000,1000_QL80_.jpg", 78 | "genre": ["Adventure", "Drama", "Sci-Fi"], 79 | "rate": 8.6 80 | }, 81 | { 82 | "id": "aa391090-b938-42eb-b520-86ea0aa3917b", 83 | "title": "The Lord of the Rings: The Return of the King", 84 | "year": 2003, 85 | "director": "Peter Jackson", 86 | "duration": 201, 87 | "poster": "https://i.ebayimg.com/images/g/0hoAAOSwe7peaMLW/s-l1600.jpg", 88 | "genre": ["Action", "Adventure", "Drama"], 89 | "rate": 8.9 90 | }, 91 | { 92 | "id": "2e6900e2-0b48-4fb6-ad48-09c7086e54fe", 93 | "title": "The Lion King", 94 | "year": 1994, 95 | "director": "Roger Allers, Rob Minkoff", 96 | "duration": 88, 97 | "poster": "https://m.media-amazon.com/images/I/81BMmrwSFOL._AC_UF1000,1000_QL80_.jpg", 98 | "genre": ["Animation", "Adventure", "Drama"], 99 | "rate": 8.5 100 | }, 101 | { 102 | "id": "04986507-b3ed-442c-8ae7-4c5df804f896", 103 | "title": "The Avengers", 104 | "year": 2012, 105 | "director": "Joss Whedon", 106 | "duration": 143, 107 | "poster": "https://img.fruugo.com/product/7/41/14532417_max.jpg", 108 | "genre": ["Action", "Adventure", "Sci-Fi"], 109 | "rate": 8.0 110 | }, 111 | { 112 | "id": "7d2832f8-c70a-410e-8963-4c93bf36cc9c", 113 | "title": "Jurassic Park", 114 | "year": 1993, 115 | "director": "Steven Spielberg", 116 | "duration": 127, 117 | "poster": "https://vice-press.com/cdn/shop/products/Jurassic-Park-Editions-poster-florey.jpg?v=1654518755&width=1024", 118 | "genre": ["Adventure", "Sci-Fi"], 119 | "rate": 8.1 120 | }, 121 | { 122 | "id": "ccf36f2e-8566-47f7-912d-9f4647250bc7", 123 | "title": "Titanic", 124 | "year": 1997, 125 | "director": "James Cameron", 126 | "duration": 195, 127 | "poster": "https://i.pinimg.com/originals/42/42/65/4242658e6f1b0d6322a4a93e0383108b.png", 128 | "genre": ["Drama", "Romance"], 129 | "rate": 7.8 130 | }, 131 | { 132 | "id": "8fb17ae1-bdfe-45e5-a871-4772d7e526b8", 133 | "title": "The Social Network", 134 | "year": 2010, 135 | "director": "David Fincher", 136 | "duration": 120, 137 | "poster": "https://i.pinimg.com/originals/7e/37/b9/7e37b994b613e94cba64f307b1983e39.jpg", 138 | "genre": ["Biography", "Drama"], 139 | "rate": 7.7 140 | }, 141 | { 142 | "id": "6a360a18-c645-4b47-9a7b-2a71babbf3e0", 143 | "title": "Avatar", 144 | "year": 2009, 145 | "director": "James Cameron", 146 | "duration": 162, 147 | "poster": "https://i.etsystatic.com/35681979/r/il/dfe3ba/3957859451/il_fullxfull.3957859451_h27r.jpg", 148 | "genre": ["Action", "Adventure", "Fantasy"], 149 | "rate": 7.8 150 | }] -------------------------------------------------------------------------------- /clase-6/README.md: -------------------------------------------------------------------------------- 1 | HTTP vs Web Sockets 2 | ➡️ https://ably.com/topic/websockets-vs-http 3 | 4 | Comparativa: 01-comparativa.webp 5 | 6 | HTTP es un protocolo de comunicación que se basa en el intercambio de mensajes entre un cliente y un servidor. Usa TCP como protocolo de transporte. 7 | 8 | El cliente realiza una petición y el servidor responde con una respuesta. 9 | 10 | Explicación: 02-http.webp 11 | 12 | Web Sockets es un protocolo de comunicación que se basa en el intercambio de mensajes entre un cliente y un servidor. Usa TCP como protocolo de transporte. 13 | 14 | La diferencia es que el cliente y el servidor pueden intercambiar mensajes en cualquier momento. 15 | 16 | Explicación: 03-websockets.webp 17 | 18 | Cuando HTTP es mejor: 19 | - Para pedir recursos 20 | - Recursos cacheables 21 | - Para REST API y uso de metodos HTTP 22 | - Sincronizar eventos 23 | 24 | Cuando Web Sockets es mejor: 25 | - Para comunicación bidireccional 26 | - Para comunicación en tiempo real 27 | - Para comunicación de alta frecuencia 28 | - Actualización con poca latencia 29 | 30 | # Instalación de Express 31 | 32 | npm install express 33 | 34 | ```javascript 35 | import express from 'express' 36 | 37 | const port = process.env.PORT ?? 3000; 38 | 39 | const app = express(); 40 | 41 | app.get('/', (req, res) => { 42 | res.send('

Front Page

'); 43 | }); 44 | 45 | // no usar app, si no server 46 | server.listen(port, () => { 47 | console.log(`Started at ${port}`); 48 | }); 49 | ``` 50 | 51 | # Instalando para mejorar el logging de Express 52 | 53 | ```javascript 54 | import express from 'express' 55 | import logger from 'morgan' 56 | 57 | const port = process.env.PORT ?? 3000; 58 | 59 | const app = express(); 60 | app.use(logger('dev')); 61 | 62 | app.get('/', (req, res) => { 63 | res.send('

Front Page

'); 64 | }); 65 | 66 | app.listen(port, () => { 67 | console.log(`Started at ${port}`); 68 | }); 69 | ``` 70 | 71 | # Servir nuestro HTML 72 | 73 | ```javascript 74 | app.get('/', (req, res) => { 75 | res.sendFile(process.cwd() + '/client/index.html'); 76 | }); 77 | ``` 78 | 79 | # Crear el HTML 80 | 81 | ```html 82 | 83 | 84 | 85 | 86 | 87 | Socket.IO chat 88 | 114 | 115 | 116 | 117 |
118 |
119 | 120 |
121 |
122 | 123 | 124 | 125 | ``` 126 | 127 | # Instalando socket.io 128 | 129 | `npm install socket.io` 130 | 131 | ```js 132 | import { createServer } from 'node:http' 133 | import { Server } from 'socket.io' 134 | 135 | const server = createServer(app) 136 | const io = new Server(server) 137 | 138 | io.on('connection', (socket) => { 139 | console.log('a user connected') 140 | }) 141 | ``` 142 | 143 | # Cargamos socket en el client 144 | 145 | ```html 146 | 151 | ``` 152 | 153 | Vemos que ahora cuando se conecta un usuario, se muestra en el servidor "a user connected". Vamos a ver también cuando se desconecta. 154 | 155 | ```js 156 | io.on('connection', (socket) => { 157 | console.log('a user connected') 158 | 159 | socket.on('disconnect', () => { 160 | console.log('user disconnected') 161 | }) 162 | }) 163 | ``` 164 | 165 | # Mejorando la apariencia del form: 166 | 167 | ```html 168 | 203 | ``` 204 | 205 | # Enviar eventos 206 | 207 | ```html 208 | 221 | ``` 222 | 223 | # Escuchar evento en el servidor 224 | 225 | ```javascript 226 | io.on('connection', (socket) => { 227 | console.log('a user connected') 228 | 229 | socket.on('chat message', (msg) => { 230 | console.log('message: ' + msg) 231 | }) 232 | 233 | socket.on('disconnect', () => { 234 | console.log('user disconnected') 235 | }) 236 | }) 237 | ``` 238 | 239 | # Enviar broadcast desde el servidor 240 | 241 | Emitir el evento a todos los clientes que tenemos conectados: 242 | 243 | ```javascript 244 | io.on('connection', (socket) => { 245 | console.log('a user connected') 246 | 247 | socket.on('chat message', (msg) => { 248 | io.emit('chat message', msg) // <----- 249 | }) 250 | 251 | socket.on('disconnect', () => { 252 | console.log('user disconnected') 253 | }) 254 | }) 255 | ``` 256 | 257 | # Escuchar broadcasting en cliente 258 | 259 | ```html 260 | 268 | ``` 269 | 270 | # Mejorar estilos de los mensajes 271 | 272 | ```html 273 | 288 | ``` 289 | 290 | # Recuperar usuario desconectado 291 |   292 | ¿Qué pasa si el usuario se desconecta? 293 | Los mensajes se pierden. 294 | 295 | Cómo evitarlo: 296 | 297 | ```javascript 298 | const io = new Server(server, { 299 | connectionStateRecovery: {} 300 | }) 301 | ``` 302 | 303 | # Mostrar al usuario cuando pierde conexión 304 | 305 | ```html 306 | 316 | 317 | 328 | ``` 329 | 330 | # Sincronizar con base de datos 331 | 332 | https://turso.tech/ 333 | 334 | ```sh 335 | $ brew install tursodatabase/tap/turso 336 | $ turso auth login 337 | $ turso db create 338 | $ turso db show humble-spectrum 339 | ``` 340 | 341 | ```sh 342 | $ npm install @libsql/client dotenv 343 | ``` 344 | 345 | Crear la conexión: 346 | 347 | ```javascript 348 | import dotenv from 'dotenv' 349 | import { createClient } from "@libsql/client" 350 | 351 | dotenv.config() 352 | 353 | const db = createClient({ 354 | url: "libsql://humble-spectrum-midudev.turso.io", 355 | authToken: process.env.DB_TOKEN 356 | }); 357 | ``` 358 | 359 | ```sh 360 | $ turso db show humble-spectrum 361 | $ turso db tokens create humble-spectrum | pbcopy 362 | ``` 363 | 364 | Crear la tabla si no existe: 365 | ```javascript 366 | await db.execute(` 367 | CREATE TABLE IF NOT EXISTS messages ( 368 | id INTEGER PRIMARY KEY AUTOINCREMENT, 369 | content TEXT 370 | ); 371 | `); 372 | ``` 373 | 374 | Guarda el mensaje cada vez que se envía: 375 | ```javascript 376 | socket.on('chat message', async (msg) => { 377 | let result; 378 | try { 379 | console.log(msg) 380 | 381 | result = await db.execute({ 382 | sql: `INSERT INTO messages (content) VALUES (:content)`, 383 | args: { content: msg } 384 | }); 385 | } catch (e) { 386 | console.error(e) 387 | return 388 | } 389 | 390 | io.emit('chat message', msg, result.lastInsertRowid.toString()) 391 | }) 392 | ``` 393 | 394 | # Saber en qué mensaje se quedó el cliente 395 | 396 | ```javascript 397 | const socket = io({ 398 | auth: { 399 | serverOffset: 0 400 | } 401 | }) 402 | 403 | socket.on('chat message', (msg, serverOffset) => { 404 | const item = `
  • ${msg}
  • `; 405 | messages.insertAdjacentHTML('beforeend', item); 406 | socket.auth.serverOffset = serverOffset // <---- 407 | }) 408 | ``` 409 | 410 | # Recuperar mensajes anteriores 411 | 412 | ```javascript 413 | io.on('connection', async (socket) => { 414 | console.log('a user connected') 415 | 416 | socket.on('chat message', async (msg) => { 417 | // ... 418 | }) 419 | 420 | if (!socket.recovered) { 421 | try { 422 | const results = await db.execute({ 423 | sql: `SELECT id, content FROM messages WHERE id > ?`, 424 | args: [socket.handshake.auth.serverOffset ?? 0] 425 | }) 426 | 427 | results.rows.forEach(row => { 428 | socket.emit('chat message', row.content, row.id) 429 | }) 430 | } catch (e) { 431 | console.error(e) 432 | } 433 | } 434 | ``` 435 | 436 | # Tener usuario random 437 | 438 | `$ turso db shell humble-spectrum` 439 | 440 | ```javascript 441 | await db.execute(` 442 | CREATE TABLE IF NOT EXISTS messages ( 443 | id INTEGER PRIMARY KEY AUTOINCREMENT, 444 | content TEXT, 445 | user TEXT 446 | ); 447 | `); 448 | ``` 449 | 450 | # Crear usuario random en el cliente: 451 | 452 | ```javascript 453 | const getUsername = async () => { 454 | const username = localStorage.getItem('username') 455 | if (username) { 456 | logMessage(`generated username ${username}`) 457 | return username 458 | } 459 | 460 | const res = await fetch('https://random-data-api.com/api/v2/users') 461 | const { username: randomUsername } = await res.json() 462 | 463 | localStorage.setItem('username', randomUsername) 464 | logMessage(`generated username ${randomUsername}`) 465 | 466 | return randomUsername 467 | } 468 | 469 | const socket = io({ 470 | auth: { 471 | username: await getUsername(), 472 | serverOffset: 0 473 | } 474 | }) 475 | ``` 476 | 477 | # Mostrar usuario visualmente en el chat 478 | 479 | ```javascript [cliente] 480 | socket.on('chat message', (msg, serverOffset, username) => { 481 | const item = `
  • 482 |

    ${msg}

    enviado por ${username} 483 |
  • `; 484 | ``` 485 | 486 | # Guardar usuario en la base de datos 487 | 488 | ```javascript [servidor] 489 | socket.on('chat message', async (msg) => { 490 | let result; 491 | // ⬇️ 492 | const user = socket.handshake.auth.username ?? 'anonymous' 493 | 494 | try { 495 | result = await db.execute({ // ⬇️ 496 | sql: `INSERT INTO messages (content, user) VALUES (:content, :user)`, 497 | args: { content: msg, user } 498 | }); 499 | catch {} 500 | 501 | io.emit('chat message', msg, result.lastInsertRowid.toString(), user) // <------ 502 | ``` 503 | 504 | ```javascript [servidor] 505 | if (!socket.recovered) { 506 | try { 507 | const results = await db.execute({ // ⬇️ 508 | sql: `SELECT id, content, user FROM messages WHERE id > ?`, 509 | args: [socket.handshake.auth.serverOffset ?? 0] 510 | }) 511 | 512 | results.rows.forEach(row => { // ⬇️ 513 | socket.emit('chat message', row.content, row.id, row.user) 514 | }) 515 | } catch (e) { 516 | console.error(e) 517 | } 518 | } 519 | ``` -------------------------------------------------------------------------------- /clase-2/pokemon/ditto.json: -------------------------------------------------------------------------------- 1 | {"abilities":[{"ability":{"name":"limber","url":"https://pokeapi.co/api/v2/ability/7/"},"is_hidden":false,"slot":1},{"ability":{"name":"imposter","url":"https://pokeapi.co/api/v2/ability/150/"},"is_hidden":true,"slot":3}],"base_experience":101,"forms":[{"name":"ditto","url":"https://pokeapi.co/api/v2/pokemon-form/132/"}],"game_indices":[{"game_index":76,"version":{"name":"red","url":"https://pokeapi.co/api/v2/version/1/"}},{"game_index":76,"version":{"name":"blue","url":"https://pokeapi.co/api/v2/version/2/"}},{"game_index":76,"version":{"name":"yellow","url":"https://pokeapi.co/api/v2/version/3/"}},{"game_index":132,"version":{"name":"gold","url":"https://pokeapi.co/api/v2/version/4/"}},{"game_index":132,"version":{"name":"silver","url":"https://pokeapi.co/api/v2/version/5/"}},{"game_index":132,"version":{"name":"crystal","url":"https://pokeapi.co/api/v2/version/6/"}},{"game_index":132,"version":{"name":"ruby","url":"https://pokeapi.co/api/v2/version/7/"}},{"game_index":132,"version":{"name":"sapphire","url":"https://pokeapi.co/api/v2/version/8/"}},{"game_index":132,"version":{"name":"emerald","url":"https://pokeapi.co/api/v2/version/9/"}},{"game_index":132,"version":{"name":"firered","url":"https://pokeapi.co/api/v2/version/10/"}},{"game_index":132,"version":{"name":"leafgreen","url":"https://pokeapi.co/api/v2/version/11/"}},{"game_index":132,"version":{"name":"diamond","url":"https://pokeapi.co/api/v2/version/12/"}},{"game_index":132,"version":{"name":"pearl","url":"https://pokeapi.co/api/v2/version/13/"}},{"game_index":132,"version":{"name":"platinum","url":"https://pokeapi.co/api/v2/version/14/"}},{"game_index":132,"version":{"name":"heartgold","url":"https://pokeapi.co/api/v2/version/15/"}},{"game_index":132,"version":{"name":"soulsilver","url":"https://pokeapi.co/api/v2/version/16/"}},{"game_index":132,"version":{"name":"black","url":"https://pokeapi.co/api/v2/version/17/"}},{"game_index":132,"version":{"name":"white","url":"https://pokeapi.co/api/v2/version/18/"}},{"game_index":132,"version":{"name":"black-2","url":"https://pokeapi.co/api/v2/version/21/"}},{"game_index":132,"version":{"name":"white-2","url":"https://pokeapi.co/api/v2/version/22/"}}],"height":3,"held_items":[{"item":{"name":"metal-powder","url":"https://pokeapi.co/api/v2/item/234/"},"version_details":[{"rarity":5,"version":{"name":"ruby","url":"https://pokeapi.co/api/v2/version/7/"}},{"rarity":5,"version":{"name":"sapphire","url":"https://pokeapi.co/api/v2/version/8/"}},{"rarity":5,"version":{"name":"emerald","url":"https://pokeapi.co/api/v2/version/9/"}},{"rarity":5,"version":{"name":"firered","url":"https://pokeapi.co/api/v2/version/10/"}},{"rarity":5,"version":{"name":"leafgreen","url":"https://pokeapi.co/api/v2/version/11/"}},{"rarity":5,"version":{"name":"diamond","url":"https://pokeapi.co/api/v2/version/12/"}},{"rarity":5,"version":{"name":"pearl","url":"https://pokeapi.co/api/v2/version/13/"}},{"rarity":5,"version":{"name":"platinum","url":"https://pokeapi.co/api/v2/version/14/"}},{"rarity":5,"version":{"name":"heartgold","url":"https://pokeapi.co/api/v2/version/15/"}},{"rarity":5,"version":{"name":"soulsilver","url":"https://pokeapi.co/api/v2/version/16/"}},{"rarity":5,"version":{"name":"black","url":"https://pokeapi.co/api/v2/version/17/"}},{"rarity":5,"version":{"name":"white","url":"https://pokeapi.co/api/v2/version/18/"}},{"rarity":5,"version":{"name":"black-2","url":"https://pokeapi.co/api/v2/version/21/"}},{"rarity":5,"version":{"name":"white-2","url":"https://pokeapi.co/api/v2/version/22/"}},{"rarity":5,"version":{"name":"x","url":"https://pokeapi.co/api/v2/version/23/"}},{"rarity":5,"version":{"name":"y","url":"https://pokeapi.co/api/v2/version/24/"}},{"rarity":5,"version":{"name":"omega-ruby","url":"https://pokeapi.co/api/v2/version/25/"}},{"rarity":5,"version":{"name":"alpha-sapphire","url":"https://pokeapi.co/api/v2/version/26/"}},{"rarity":5,"version":{"name":"sun","url":"https://pokeapi.co/api/v2/version/27/"}},{"rarity":5,"version":{"name":"moon","url":"https://pokeapi.co/api/v2/version/28/"}},{"rarity":5,"version":{"name":"ultra-sun","url":"https://pokeapi.co/api/v2/version/29/"}},{"rarity":5,"version":{"name":"ultra-moon","url":"https://pokeapi.co/api/v2/version/30/"}}]},{"item":{"name":"quick-powder","url":"https://pokeapi.co/api/v2/item/251/"},"version_details":[{"rarity":50,"version":{"name":"diamond","url":"https://pokeapi.co/api/v2/version/12/"}},{"rarity":50,"version":{"name":"pearl","url":"https://pokeapi.co/api/v2/version/13/"}},{"rarity":50,"version":{"name":"platinum","url":"https://pokeapi.co/api/v2/version/14/"}},{"rarity":50,"version":{"name":"heartgold","url":"https://pokeapi.co/api/v2/version/15/"}},{"rarity":50,"version":{"name":"soulsilver","url":"https://pokeapi.co/api/v2/version/16/"}},{"rarity":50,"version":{"name":"black","url":"https://pokeapi.co/api/v2/version/17/"}},{"rarity":50,"version":{"name":"white","url":"https://pokeapi.co/api/v2/version/18/"}},{"rarity":50,"version":{"name":"black-2","url":"https://pokeapi.co/api/v2/version/21/"}},{"rarity":50,"version":{"name":"white-2","url":"https://pokeapi.co/api/v2/version/22/"}},{"rarity":50,"version":{"name":"x","url":"https://pokeapi.co/api/v2/version/23/"}},{"rarity":50,"version":{"name":"y","url":"https://pokeapi.co/api/v2/version/24/"}},{"rarity":50,"version":{"name":"omega-ruby","url":"https://pokeapi.co/api/v2/version/25/"}},{"rarity":50,"version":{"name":"alpha-sapphire","url":"https://pokeapi.co/api/v2/version/26/"}},{"rarity":50,"version":{"name":"sun","url":"https://pokeapi.co/api/v2/version/27/"}},{"rarity":50,"version":{"name":"moon","url":"https://pokeapi.co/api/v2/version/28/"}},{"rarity":50,"version":{"name":"ultra-sun","url":"https://pokeapi.co/api/v2/version/29/"}},{"rarity":50,"version":{"name":"ultra-moon","url":"https://pokeapi.co/api/v2/version/30/"}}]}],"id":132,"is_default":true,"location_area_encounters":"https://pokeapi.co/api/v2/pokemon/132/encounters","moves":[{"move":{"name":"transform","url":"https://pokeapi.co/api/v2/move/144/"},"version_group_details":[{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"red-blue","url":"https://pokeapi.co/api/v2/version-group/1/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"yellow","url":"https://pokeapi.co/api/v2/version-group/2/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"gold-silver","url":"https://pokeapi.co/api/v2/version-group/3/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"crystal","url":"https://pokeapi.co/api/v2/version-group/4/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ruby-sapphire","url":"https://pokeapi.co/api/v2/version-group/5/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"emerald","url":"https://pokeapi.co/api/v2/version-group/6/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"firered-leafgreen","url":"https://pokeapi.co/api/v2/version-group/7/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"diamond-pearl","url":"https://pokeapi.co/api/v2/version-group/8/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"platinum","url":"https://pokeapi.co/api/v2/version-group/9/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"heartgold-soulsilver","url":"https://pokeapi.co/api/v2/version-group/10/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-white","url":"https://pokeapi.co/api/v2/version-group/11/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"colosseum","url":"https://pokeapi.co/api/v2/version-group/12/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"xd","url":"https://pokeapi.co/api/v2/version-group/13/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"black-2-white-2","url":"https://pokeapi.co/api/v2/version-group/14/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"x-y","url":"https://pokeapi.co/api/v2/version-group/15/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"omega-ruby-alpha-sapphire","url":"https://pokeapi.co/api/v2/version-group/16/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sun-moon","url":"https://pokeapi.co/api/v2/version-group/17/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"ultra-sun-ultra-moon","url":"https://pokeapi.co/api/v2/version-group/18/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"lets-go-pikachu-lets-go-eevee","url":"https://pokeapi.co/api/v2/version-group/19/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"sword-shield","url":"https://pokeapi.co/api/v2/version-group/20/"}},{"level_learned_at":1,"move_learn_method":{"name":"level-up","url":"https://pokeapi.co/api/v2/move-learn-method/1/"},"version_group":{"name":"scarlet-violet","url":"https://pokeapi.co/api/v2/version-group/25/"}}]}],"name":"ditto","order":214,"past_types":[],"species":{"name":"ditto","url":"https://pokeapi.co/api/v2/pokemon-species/132/"},"sprites":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/132.png","back_female":null,"back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/132.png","back_shiny_female":null,"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/132.png","front_female":null,"front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/132.png","front_shiny_female":null,"other":{"dream_world":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/132.svg","front_female":null},"home":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/132.png","front_female":null,"front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/shiny/132.png","front_shiny_female":null},"official-artwork":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/132.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/shiny/132.png"}},"versions":{"generation-i":{"red-blue":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/back/132.png","back_gray":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/back/gray/132.png","back_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/transparent/back/132.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/132.png","front_gray":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/gray/132.png","front_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/transparent/132.png"},"yellow":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/back/132.png","back_gray":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/back/gray/132.png","back_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/transparent/back/132.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/132.png","front_gray":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/gray/132.png","front_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/yellow/transparent/132.png"}},"generation-ii":{"crystal":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/back/132.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/back/shiny/132.png","back_shiny_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/back/shiny/132.png","back_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/back/132.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/132.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/shiny/132.png","front_shiny_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/shiny/132.png","front_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/crystal/transparent/132.png"},"gold":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/back/132.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/back/shiny/132.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/132.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/shiny/132.png","front_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/gold/transparent/132.png"},"silver":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/back/132.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/back/shiny/132.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/132.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/shiny/132.png","front_transparent":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-ii/silver/transparent/132.png"}},"generation-iii":{"emerald":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/emerald/132.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/emerald/shiny/132.png"},"firered-leafgreen":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/back/132.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/back/shiny/132.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/132.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/firered-leafgreen/shiny/132.png"},"ruby-sapphire":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/back/132.png","back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/back/shiny/132.png","front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/132.png","front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iii/ruby-sapphire/shiny/132.png"}},"generation-iv":{"diamond-pearl":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/back/132.png","back_female":null,"back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/back/shiny/132.png","back_shiny_female":null,"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/132.png","front_female":null,"front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/diamond-pearl/shiny/132.png","front_shiny_female":null},"heartgold-soulsilver":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/back/132.png","back_female":null,"back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/back/shiny/132.png","back_shiny_female":null,"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/132.png","front_female":null,"front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/shiny/132.png","front_shiny_female":null},"platinum":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/back/132.png","back_female":null,"back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/back/shiny/132.png","back_shiny_female":null,"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/132.png","front_female":null,"front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/platinum/shiny/132.png","front_shiny_female":null}},"generation-v":{"black-white":{"animated":{"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/back/132.gif","back_female":null,"back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/back/shiny/132.gif","back_shiny_female":null,"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/132.gif","front_female":null,"front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated/shiny/132.gif","front_shiny_female":null},"back_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/back/132.png","back_female":null,"back_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/back/shiny/132.png","back_shiny_female":null,"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/132.png","front_female":null,"front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/shiny/132.png","front_shiny_female":null}},"generation-vi":{"omegaruby-alphasapphire":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/omegaruby-alphasapphire/132.png","front_female":null,"front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/omegaruby-alphasapphire/shiny/132.png","front_shiny_female":null},"x-y":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/x-y/132.png","front_female":null,"front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vi/x-y/shiny/132.png","front_shiny_female":null}},"generation-vii":{"icons":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/icons/132.png","front_female":null},"ultra-sun-ultra-moon":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/ultra-sun-ultra-moon/132.png","front_female":null,"front_shiny":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/ultra-sun-ultra-moon/shiny/132.png","front_shiny_female":null}},"generation-viii":{"icons":{"front_default":"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-viii/icons/132.png","front_female":null}}}},"stats":[{"base_stat":48,"effort":1,"stat":{"name":"hp","url":"https://pokeapi.co/api/v2/stat/1/"}},{"base_stat":48,"effort":0,"stat":{"name":"attack","url":"https://pokeapi.co/api/v2/stat/2/"}},{"base_stat":48,"effort":0,"stat":{"name":"defense","url":"https://pokeapi.co/api/v2/stat/3/"}},{"base_stat":48,"effort":0,"stat":{"name":"special-attack","url":"https://pokeapi.co/api/v2/stat/4/"}},{"base_stat":48,"effort":0,"stat":{"name":"special-defense","url":"https://pokeapi.co/api/v2/stat/5/"}},{"base_stat":48,"effort":0,"stat":{"name":"speed","url":"https://pokeapi.co/api/v2/stat/6/"}}],"types":[{"slot":1,"type":{"name":"normal","url":"https://pokeapi.co/api/v2/type/1/"}}],"weight":40} -------------------------------------------------------------------------------- /clase-5/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.1' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | cors: 9 | specifier: 2.8.5 10 | version: 2.8.5 11 | dotenv: 12 | specifier: 16.3.1 13 | version: 16.3.1 14 | express: 15 | specifier: 4.18.2 16 | version: 4.18.2 17 | mongodb: 18 | specifier: 5.7.0 19 | version: 5.7.0 20 | mysql2: 21 | specifier: ^3.6.0 22 | version: 3.6.0 23 | zod: 24 | specifier: 3.21.4 25 | version: 3.21.4 26 | 27 | packages: 28 | 29 | /@types/node@20.5.9: 30 | resolution: {integrity: sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==} 31 | dev: false 32 | 33 | /@types/webidl-conversions@7.0.0: 34 | resolution: {integrity: sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==} 35 | dev: false 36 | 37 | /@types/whatwg-url@8.2.2: 38 | resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==} 39 | dependencies: 40 | '@types/node': 20.5.9 41 | '@types/webidl-conversions': 7.0.0 42 | dev: false 43 | 44 | /accepts@1.3.8: 45 | resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} 46 | engines: {node: '>= 0.6'} 47 | dependencies: 48 | mime-types: 2.1.35 49 | negotiator: 0.6.3 50 | dev: false 51 | 52 | /array-flatten@1.1.1: 53 | resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} 54 | dev: false 55 | 56 | /body-parser@1.20.1: 57 | resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} 58 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 59 | dependencies: 60 | bytes: 3.1.2 61 | content-type: 1.0.5 62 | debug: 2.6.9 63 | depd: 2.0.0 64 | destroy: 1.2.0 65 | http-errors: 2.0.0 66 | iconv-lite: 0.4.24 67 | on-finished: 2.4.1 68 | qs: 6.11.0 69 | raw-body: 2.5.1 70 | type-is: 1.6.18 71 | unpipe: 1.0.0 72 | transitivePeerDependencies: 73 | - supports-color 74 | dev: false 75 | 76 | /bson@5.4.0: 77 | resolution: {integrity: sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==} 78 | engines: {node: '>=14.20.1'} 79 | dev: false 80 | 81 | /bytes@3.1.2: 82 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 83 | engines: {node: '>= 0.8'} 84 | dev: false 85 | 86 | /call-bind@1.0.2: 87 | resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} 88 | dependencies: 89 | function-bind: 1.1.1 90 | get-intrinsic: 1.2.1 91 | dev: false 92 | 93 | /content-disposition@0.5.4: 94 | resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} 95 | engines: {node: '>= 0.6'} 96 | dependencies: 97 | safe-buffer: 5.2.1 98 | dev: false 99 | 100 | /content-type@1.0.5: 101 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 102 | engines: {node: '>= 0.6'} 103 | dev: false 104 | 105 | /cookie-signature@1.0.6: 106 | resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} 107 | dev: false 108 | 109 | /cookie@0.5.0: 110 | resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} 111 | engines: {node: '>= 0.6'} 112 | dev: false 113 | 114 | /cors@2.8.5: 115 | resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} 116 | engines: {node: '>= 0.10'} 117 | dependencies: 118 | object-assign: 4.1.1 119 | vary: 1.1.2 120 | dev: false 121 | 122 | /debug@2.6.9: 123 | resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} 124 | peerDependencies: 125 | supports-color: '*' 126 | peerDependenciesMeta: 127 | supports-color: 128 | optional: true 129 | dependencies: 130 | ms: 2.0.0 131 | dev: false 132 | 133 | /denque@2.1.0: 134 | resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} 135 | engines: {node: '>=0.10'} 136 | dev: false 137 | 138 | /depd@2.0.0: 139 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 140 | engines: {node: '>= 0.8'} 141 | dev: false 142 | 143 | /destroy@1.2.0: 144 | resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} 145 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 146 | dev: false 147 | 148 | /dotenv@16.3.1: 149 | resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} 150 | engines: {node: '>=12'} 151 | dev: false 152 | 153 | /ee-first@1.1.1: 154 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 155 | dev: false 156 | 157 | /encodeurl@1.0.2: 158 | resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} 159 | engines: {node: '>= 0.8'} 160 | dev: false 161 | 162 | /escape-html@1.0.3: 163 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 164 | dev: false 165 | 166 | /etag@1.8.1: 167 | resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 168 | engines: {node: '>= 0.6'} 169 | dev: false 170 | 171 | /express@4.18.2: 172 | resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} 173 | engines: {node: '>= 0.10.0'} 174 | dependencies: 175 | accepts: 1.3.8 176 | array-flatten: 1.1.1 177 | body-parser: 1.20.1 178 | content-disposition: 0.5.4 179 | content-type: 1.0.5 180 | cookie: 0.5.0 181 | cookie-signature: 1.0.6 182 | debug: 2.6.9 183 | depd: 2.0.0 184 | encodeurl: 1.0.2 185 | escape-html: 1.0.3 186 | etag: 1.8.1 187 | finalhandler: 1.2.0 188 | fresh: 0.5.2 189 | http-errors: 2.0.0 190 | merge-descriptors: 1.0.1 191 | methods: 1.1.2 192 | on-finished: 2.4.1 193 | parseurl: 1.3.3 194 | path-to-regexp: 0.1.7 195 | proxy-addr: 2.0.7 196 | qs: 6.11.0 197 | range-parser: 1.2.1 198 | safe-buffer: 5.2.1 199 | send: 0.18.0 200 | serve-static: 1.15.0 201 | setprototypeof: 1.2.0 202 | statuses: 2.0.1 203 | type-is: 1.6.18 204 | utils-merge: 1.0.1 205 | vary: 1.1.2 206 | transitivePeerDependencies: 207 | - supports-color 208 | dev: false 209 | 210 | /finalhandler@1.2.0: 211 | resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} 212 | engines: {node: '>= 0.8'} 213 | dependencies: 214 | debug: 2.6.9 215 | encodeurl: 1.0.2 216 | escape-html: 1.0.3 217 | on-finished: 2.4.1 218 | parseurl: 1.3.3 219 | statuses: 2.0.1 220 | unpipe: 1.0.0 221 | transitivePeerDependencies: 222 | - supports-color 223 | dev: false 224 | 225 | /forwarded@0.2.0: 226 | resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 227 | engines: {node: '>= 0.6'} 228 | dev: false 229 | 230 | /fresh@0.5.2: 231 | resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} 232 | engines: {node: '>= 0.6'} 233 | dev: false 234 | 235 | /function-bind@1.1.1: 236 | resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} 237 | dev: false 238 | 239 | /generate-function@2.3.1: 240 | resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} 241 | dependencies: 242 | is-property: 1.0.2 243 | dev: false 244 | 245 | /get-intrinsic@1.2.1: 246 | resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} 247 | dependencies: 248 | function-bind: 1.1.1 249 | has: 1.0.3 250 | has-proto: 1.0.1 251 | has-symbols: 1.0.3 252 | dev: false 253 | 254 | /has-proto@1.0.1: 255 | resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} 256 | engines: {node: '>= 0.4'} 257 | dev: false 258 | 259 | /has-symbols@1.0.3: 260 | resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} 261 | engines: {node: '>= 0.4'} 262 | dev: false 263 | 264 | /has@1.0.3: 265 | resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} 266 | engines: {node: '>= 0.4.0'} 267 | dependencies: 268 | function-bind: 1.1.1 269 | dev: false 270 | 271 | /http-errors@2.0.0: 272 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 273 | engines: {node: '>= 0.8'} 274 | dependencies: 275 | depd: 2.0.0 276 | inherits: 2.0.4 277 | setprototypeof: 1.2.0 278 | statuses: 2.0.1 279 | toidentifier: 1.0.1 280 | dev: false 281 | 282 | /iconv-lite@0.4.24: 283 | resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} 284 | engines: {node: '>=0.10.0'} 285 | dependencies: 286 | safer-buffer: 2.1.2 287 | dev: false 288 | 289 | /iconv-lite@0.6.3: 290 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 291 | engines: {node: '>=0.10.0'} 292 | dependencies: 293 | safer-buffer: 2.1.2 294 | dev: false 295 | 296 | /inherits@2.0.4: 297 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 298 | dev: false 299 | 300 | /ip@2.0.0: 301 | resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} 302 | dev: false 303 | 304 | /ipaddr.js@1.9.1: 305 | resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 306 | engines: {node: '>= 0.10'} 307 | dev: false 308 | 309 | /is-property@1.0.2: 310 | resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} 311 | dev: false 312 | 313 | /long@5.2.3: 314 | resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} 315 | dev: false 316 | 317 | /lru-cache@7.18.3: 318 | resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} 319 | engines: {node: '>=12'} 320 | dev: false 321 | 322 | /lru-cache@8.0.5: 323 | resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} 324 | engines: {node: '>=16.14'} 325 | dev: false 326 | 327 | /media-typer@0.3.0: 328 | resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} 329 | engines: {node: '>= 0.6'} 330 | dev: false 331 | 332 | /memory-pager@1.5.0: 333 | resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} 334 | dev: false 335 | optional: true 336 | 337 | /merge-descriptors@1.0.1: 338 | resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} 339 | dev: false 340 | 341 | /methods@1.1.2: 342 | resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} 343 | engines: {node: '>= 0.6'} 344 | dev: false 345 | 346 | /mime-db@1.52.0: 347 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 348 | engines: {node: '>= 0.6'} 349 | dev: false 350 | 351 | /mime-types@2.1.35: 352 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 353 | engines: {node: '>= 0.6'} 354 | dependencies: 355 | mime-db: 1.52.0 356 | dev: false 357 | 358 | /mime@1.6.0: 359 | resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} 360 | engines: {node: '>=4'} 361 | hasBin: true 362 | dev: false 363 | 364 | /mongodb-connection-string-url@2.6.0: 365 | resolution: {integrity: sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==} 366 | dependencies: 367 | '@types/whatwg-url': 8.2.2 368 | whatwg-url: 11.0.0 369 | dev: false 370 | 371 | /mongodb@5.7.0: 372 | resolution: {integrity: sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==} 373 | engines: {node: '>=14.20.1'} 374 | peerDependencies: 375 | '@aws-sdk/credential-providers': ^3.201.0 376 | '@mongodb-js/zstd': ^1.1.0 377 | kerberos: ^2.0.1 378 | mongodb-client-encryption: '>=2.3.0 <3' 379 | snappy: ^7.2.2 380 | peerDependenciesMeta: 381 | '@aws-sdk/credential-providers': 382 | optional: true 383 | '@mongodb-js/zstd': 384 | optional: true 385 | kerberos: 386 | optional: true 387 | mongodb-client-encryption: 388 | optional: true 389 | snappy: 390 | optional: true 391 | dependencies: 392 | bson: 5.4.0 393 | mongodb-connection-string-url: 2.6.0 394 | socks: 2.7.1 395 | optionalDependencies: 396 | saslprep: 1.0.3 397 | dev: false 398 | 399 | /ms@2.0.0: 400 | resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} 401 | dev: false 402 | 403 | /ms@2.1.3: 404 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 405 | dev: false 406 | 407 | /mysql2@3.6.0: 408 | resolution: {integrity: sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==} 409 | engines: {node: '>= 8.0'} 410 | dependencies: 411 | denque: 2.1.0 412 | generate-function: 2.3.1 413 | iconv-lite: 0.6.3 414 | long: 5.2.3 415 | lru-cache: 8.0.5 416 | named-placeholders: 1.1.3 417 | seq-queue: 0.0.5 418 | sqlstring: 2.3.3 419 | dev: false 420 | 421 | /named-placeholders@1.1.3: 422 | resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} 423 | engines: {node: '>=12.0.0'} 424 | dependencies: 425 | lru-cache: 7.18.3 426 | dev: false 427 | 428 | /negotiator@0.6.3: 429 | resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} 430 | engines: {node: '>= 0.6'} 431 | dev: false 432 | 433 | /object-assign@4.1.1: 434 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 435 | engines: {node: '>=0.10.0'} 436 | dev: false 437 | 438 | /object-inspect@1.12.3: 439 | resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} 440 | dev: false 441 | 442 | /on-finished@2.4.1: 443 | resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 444 | engines: {node: '>= 0.8'} 445 | dependencies: 446 | ee-first: 1.1.1 447 | dev: false 448 | 449 | /parseurl@1.3.3: 450 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 451 | engines: {node: '>= 0.8'} 452 | dev: false 453 | 454 | /path-to-regexp@0.1.7: 455 | resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} 456 | dev: false 457 | 458 | /proxy-addr@2.0.7: 459 | resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 460 | engines: {node: '>= 0.10'} 461 | dependencies: 462 | forwarded: 0.2.0 463 | ipaddr.js: 1.9.1 464 | dev: false 465 | 466 | /punycode@2.3.0: 467 | resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} 468 | engines: {node: '>=6'} 469 | dev: false 470 | 471 | /qs@6.11.0: 472 | resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} 473 | engines: {node: '>=0.6'} 474 | dependencies: 475 | side-channel: 1.0.4 476 | dev: false 477 | 478 | /range-parser@1.2.1: 479 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 480 | engines: {node: '>= 0.6'} 481 | dev: false 482 | 483 | /raw-body@2.5.1: 484 | resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} 485 | engines: {node: '>= 0.8'} 486 | dependencies: 487 | bytes: 3.1.2 488 | http-errors: 2.0.0 489 | iconv-lite: 0.4.24 490 | unpipe: 1.0.0 491 | dev: false 492 | 493 | /safe-buffer@5.2.1: 494 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 495 | dev: false 496 | 497 | /safer-buffer@2.1.2: 498 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 499 | dev: false 500 | 501 | /saslprep@1.0.3: 502 | resolution: {integrity: sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==} 503 | engines: {node: '>=6'} 504 | requiresBuild: true 505 | dependencies: 506 | sparse-bitfield: 3.0.3 507 | dev: false 508 | optional: true 509 | 510 | /send@0.18.0: 511 | resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} 512 | engines: {node: '>= 0.8.0'} 513 | dependencies: 514 | debug: 2.6.9 515 | depd: 2.0.0 516 | destroy: 1.2.0 517 | encodeurl: 1.0.2 518 | escape-html: 1.0.3 519 | etag: 1.8.1 520 | fresh: 0.5.2 521 | http-errors: 2.0.0 522 | mime: 1.6.0 523 | ms: 2.1.3 524 | on-finished: 2.4.1 525 | range-parser: 1.2.1 526 | statuses: 2.0.1 527 | transitivePeerDependencies: 528 | - supports-color 529 | dev: false 530 | 531 | /seq-queue@0.0.5: 532 | resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} 533 | dev: false 534 | 535 | /serve-static@1.15.0: 536 | resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} 537 | engines: {node: '>= 0.8.0'} 538 | dependencies: 539 | encodeurl: 1.0.2 540 | escape-html: 1.0.3 541 | parseurl: 1.3.3 542 | send: 0.18.0 543 | transitivePeerDependencies: 544 | - supports-color 545 | dev: false 546 | 547 | /setprototypeof@1.2.0: 548 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 549 | dev: false 550 | 551 | /side-channel@1.0.4: 552 | resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} 553 | dependencies: 554 | call-bind: 1.0.2 555 | get-intrinsic: 1.2.1 556 | object-inspect: 1.12.3 557 | dev: false 558 | 559 | /smart-buffer@4.2.0: 560 | resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} 561 | engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} 562 | dev: false 563 | 564 | /socks@2.7.1: 565 | resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} 566 | engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} 567 | dependencies: 568 | ip: 2.0.0 569 | smart-buffer: 4.2.0 570 | dev: false 571 | 572 | /sparse-bitfield@3.0.3: 573 | resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} 574 | dependencies: 575 | memory-pager: 1.5.0 576 | dev: false 577 | optional: true 578 | 579 | /sqlstring@2.3.3: 580 | resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} 581 | engines: {node: '>= 0.6'} 582 | dev: false 583 | 584 | /statuses@2.0.1: 585 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 586 | engines: {node: '>= 0.8'} 587 | dev: false 588 | 589 | /toidentifier@1.0.1: 590 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 591 | engines: {node: '>=0.6'} 592 | dev: false 593 | 594 | /tr46@3.0.0: 595 | resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} 596 | engines: {node: '>=12'} 597 | dependencies: 598 | punycode: 2.3.0 599 | dev: false 600 | 601 | /type-is@1.6.18: 602 | resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} 603 | engines: {node: '>= 0.6'} 604 | dependencies: 605 | media-typer: 0.3.0 606 | mime-types: 2.1.35 607 | dev: false 608 | 609 | /unpipe@1.0.0: 610 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 611 | engines: {node: '>= 0.8'} 612 | dev: false 613 | 614 | /utils-merge@1.0.1: 615 | resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} 616 | engines: {node: '>= 0.4.0'} 617 | dev: false 618 | 619 | /vary@1.1.2: 620 | resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 621 | engines: {node: '>= 0.8'} 622 | dev: false 623 | 624 | /webidl-conversions@7.0.0: 625 | resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} 626 | engines: {node: '>=12'} 627 | dev: false 628 | 629 | /whatwg-url@11.0.0: 630 | resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} 631 | engines: {node: '>=12'} 632 | dependencies: 633 | tr46: 3.0.0 634 | webidl-conversions: 7.0.0 635 | dev: false 636 | 637 | /zod@3.21.4: 638 | resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} 639 | dev: false 640 | --------------------------------------------------------------------------------