diff --git a/README.md b/README.md index 43aa320..6236f52 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,91 @@ -1. Inicializamos el proyecto de Node.JS: +1. Inicializar el Proyecto Node.js: Se crea el archivo package.json para gestionar metadatos y dependencias. npm init -y npm init -y contiene informacion basica y lleva un registro de todas las "dependencias" y paquetes del proyecto -2. Instalacion de express +2. Instalar Express: Se añade el framework Express al proyecto. Esto crea la carpeta node_modules y agrega express a las dependencias en package.json. + npm install express Se crea una carpeta node_modules donde se guardan fisicamente los paquetes instalados y el archivo package.json se actualiza y contiene una nueva seccion llamada dependencies para registrar que el proyecto ahora depende de express + +3. Creamos el archivo .gitignore para indicar que recursos del proyecto se deben ignorar en el proyecto + +4. Inicializamos el Repositorio Local de Git con: git init + +5. Añadir y Confirmar Archivos: Se añaden los archivos al área de preparación y se crea un commit inicial. (Commit): git add - git commit -m "Initial commit" + +6. Conectar y subir a github Se enlaza el repositorio local con el remoto en GitHub y se suben los cambios. La autenticación se realiza con un Token de Acceso Personal (PAT) en lugar de la contraseña. + + git remote add origin https://github.com/stillfreecode/backend.git + git branch -M main + git push -u origin main <- esta opcion nos pide autenticar la cuenta para ello generamos un token desde git + + +La url baase para todas las peticiones sera: https://localhost:3000/api/products + +Prueba: GET: http://localhost:3000/api/products +Pedir solo el producto con id: 1 + http://localhost:3000/api/products/1 + +Prueba POST: http://localhost:3000/api/products +Añadir un nuevo producto a nuestro catalogo: + + { + "nombre": "Mouse Inalámbrico", + "precio": 49.99, + "stock": 200 + } + + + + +PRUEBA PUT: +Cambiar el precio y stock del producto con id: 2 +http://localhost:3000/api/products/2 + +{ + "precio": 110.00, + "stock": 40 +} + +PRUEBA DELETE: +ELiminar el producto con id:1 + +http://localhost:3000/api/products/1 + +NOTA IMPORTANTE: Los cambios que hacemos con Postman sí afectan y modifican el array de productos, pero lo hacen únicamente en la memoria del servidor mientras este se encuentra en ejecución. + + +Prueba para validaciones middleware con express-validator: + { + "nombre": "", + "precio": -10, + "stock": "mucho" + } + +Prueba para la estandarizacion de codigos de estado: +GET A http://localhost:3000/api/products/99 – 404 NOT FOUND +POST A http://localhost:3000/api/products – nombre que ya existe → 409 CONFLICT +GET a http://localhost:3000/api/products – nueva estructura de éxito en el campo data + +Pruebas para paginacion y ordenamiento basicos + +Prueba 1: Por defecto +GET: +http://localhost:3000/api/products +Solo veremos la pagina 1 con los primeros 10 productos + +Prueba 2: Prueba por paginacion (segunda pag) +http://localhost:3000/api/products?page=2 +Solo veremos la pagina 2 + +Prueba 3: Cambiando el limite: +http://localhost:3000/api/products?limit=1 +Solo veremos la cantidad de productos establecidos + +Prueba 4: Prueba combinada (Paginacion y ordenamiento) +http://localhost:3000/api/products?page=2&limit=5&sort=precio,desc +El servidor ordenara los 15 productos por precio, del mas caro al mas barato. +Luego devuelve el segundo bloque de 5 productos de esa lista ordenada (los productos del 6 al 10 mas caros) + diff --git a/.gitignore b/practica1-crud/.gitignore similarity index 100% rename from .gitignore rename to practica1-crud/.gitignore diff --git a/practica1-crud/README.md b/practica1-crud/README.md new file mode 100644 index 0000000..e3bba31 --- /dev/null +++ b/practica1-crud/README.md @@ -0,0 +1,27 @@ +1. Inicializar el Proyecto Node.js: Se crea el archivo package.json para gestionar metadatos y dependencias. + npm init -y +npm init -y contiene informacion basica y lleva un registro de todas las "dependencias" y paquetes del proyecto + +2. Instalar Express: Se añade el framework Express al proyecto. Esto crea la carpeta node_modules y agrega express a las dependencias en package.json. + + npm install express +Se crea una carpeta node_modules donde se guardan fisicamente los paquetes instalados y el archivo +package.json se actualiza y contiene una nueva seccion llamada dependencies para registrar que +el proyecto ahora depende de express + +3. Creamos el archivo .gitignore para indicar que recursos del proyecto se deben ignorar en el proyecto + +4. Inicializamos el Repositorio Local de Git con: git init + +5. Añadir y Confirmar Archivos: Se añaden los archivos al área de preparación y se crea un commit inicial. (Commit): git add - git commit -m "Initial commit" + +6. Conectar y subir a github Se enlaza el repositorio local con el remoto en GitHub y se suben los cambios. La autenticación se realiza con un Token de Acceso Personal (PAT) en lugar de la contraseña. + + git remote add origin https://github.com/stillfreecode/backend.git + git branch -M main + git push -u origin main <- esta opcion nos pide autenticar la cuenta para ello generamos un token desde git + + + + + diff --git a/practica1-crud/index.js b/practica1-crud/index.js new file mode 100644 index 0000000..bbc6f66 --- /dev/null +++ b/practica1-crud/index.js @@ -0,0 +1,20 @@ +// index.js + +const express = require('express'); +const productRoutes = require('./src/routes/product.routes'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(express.json()); + +app.get('/', (req, res) => { + res.send('¡API funcionando!'); +}); + +// Usamos las rutas de productos con un prefijo +app.use('/api/products', productRoutes); + +app.listen(PORT, () => { + console.log(`Servidor escuchando en http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/package-lock.json b/practica1-crud/package-lock.json similarity index 96% rename from package-lock.json rename to practica1-crud/package-lock.json index 6865735..288e083 100644 --- a/package-lock.json +++ b/practica1-crud/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "express": "^5.1.0" + "express": "^5.1.0", + "express-validator": "^7.2.1" } }, "node_modules/accepts": { @@ -264,6 +265,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -439,6 +453,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -806,6 +826,15 @@ "node": ">= 0.8" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/practica1-crud/package.json similarity index 84% rename from package.json rename to practica1-crud/package.json index 83530db..ceb61c6 100644 --- a/package.json +++ b/practica1-crud/package.json @@ -11,6 +11,7 @@ "license": "ISC", "type": "commonjs", "dependencies": { - "express": "^5.1.0" + "express": "^5.1.0", + "express-validator": "^7.2.1" } } diff --git a/practica1-crud/src/controllers/product.controller.js b/practica1-crud/src/controllers/product.controller.js new file mode 100644 index 0000000..228b59e --- /dev/null +++ b/practica1-crud/src/controllers/product.controller.js @@ -0,0 +1,88 @@ +// src/controllers/product.controller.js +const productService = require('../services/product.service'); +const { success, error1 } = require('../utils/responseHandler'); + +const getAllProducts = (req, res) => { + try { + // Extraemos los query params de la URL + const { page, limit, sort } = req.query; + + let sortBy; + let sortOrder; + + if (sort) { + // El formato es 'campo,orden' (ej: 'precio,desc') + [sortBy, sortOrder] = sort.split(','); + } + + const options = { page, limit, sortBy, sortOrder }; + + const result = productService.getAllProducts(options); + + // Devolvemos tanto los datos como la info de paginación + return success(res, result.data, 200, result.pagination); + } catch (err) { + return error1(res, 'Error al obtener los productos.'); + } +}; + + +const getProductById = (req, res) => { + try { + const { id } = req.params; + const product = productService.getProductById(id); + if (!product) { + return error1(res, 'Producto no encontrado.', 404, 'NOT_FOUND'); + } + return success(res, product); + } catch (err) { + return error1(res, 'Error interno del servidor al obtener el producto.'); + } +}; + +const createProduct = (req, res) => { + try { + const existingProduct = productService.getAllProducts().find(p => p.nombre.toLowerCase() === req.body.nombre.toLowerCase()); + if (existingProduct) { + return error1(res, `Ya existe un producto con el nombre '${req.body.nombre}'.`, 409, 'CONFLICT'); + } + const newProduct = productService.createProduct(req.body); + return success(res, newProduct, 201); + } catch (err) { + return error1(res, 'Error interno del servidor al crear el producto.'); + } +}; + +const updateProduct = (req, res) => { + try { + const { id } = req.params; + const updatedProduct = productService.updateProduct(id, req.body); + if (!updatedProduct) { + return error1(res, 'Producto no encontrado para actualizar.', 404, 'NOT_FOUND'); + } + return success(res, updatedProduct); + } catch (err) { + return error1(res, 'Error interno del servidor al actualizar el producto.'); + } +}; + +const deleteProduct = (req, res) => { + try { + const { id } = req.params; + const deletedProduct = productService.deleteProduct(id); + if (!deletedProduct) { + return error1(res, 'Producto no encontrado para eliminar.', 404, 'NOT_FOUND'); + } + return success(res, { message: 'Producto eliminado exitosamente.' }); + } catch (err) { + return error1(res, 'Error interno del servidor al eliminar el producto.'); + } +}; + +module.exports = { + getAllProducts, + getProductById, + createProduct, + updateProduct, + deleteProduct, +}; \ No newline at end of file diff --git a/practica1-crud/src/middlewares/productValidator.js b/practica1-crud/src/middlewares/productValidator.js new file mode 100644 index 0000000..4efa537 --- /dev/null +++ b/practica1-crud/src/middlewares/productValidator.js @@ -0,0 +1,30 @@ +// src/middlewares/productValidator.js +const { body, validationResult } = require('express-validator'); + +const productValidationRules = [ + // El nombre no debe estar vacío y debe ser texto. + body('nombre') + .notEmpty().withMessage('El nombre es obligatorio.') + .isString().withMessage('El nombre debe ser un texto.'), + + // El precio debe ser un número flotante mayor que 0. + body('precio') + .isFloat({ gt: 0 }).withMessage('El precio debe ser un número mayor que 0.'), + + // El stock debe ser un número entero mayor o igual a 0. + body('stock') + .isInt({ min: 0 }).withMessage('El stock debe ser un número entero mayor o igual a 0.') +]; + +const handleValidationErrors = (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + next(); // Si no hay errores, continúa al controlador. +}; + +module.exports = { + productValidationRules, + handleValidationErrors +}; \ No newline at end of file diff --git a/practica1-crud/src/models/product.model.js b/practica1-crud/src/models/product.model.js new file mode 100644 index 0000000..3c669cc --- /dev/null +++ b/practica1-crud/src/models/product.model.js @@ -0,0 +1,21 @@ +// src/models/product.model.js + +let products = [ + { id: 1, nombre: 'Laptop Gamer Pro', precio: 2100.00, stock: 15, creadoEn: new Date() }, + { id: 2, nombre: 'Teclado Mecánico RGB', precio: 125.50, stock: 40, creadoEn: new Date() }, + { id: 3, nombre: 'Mouse Óptico', precio: 35.00, stock: 150, creadoEn: new Date() }, + { id: 4, nombre: 'Monitor Curvo 27"', precio: 450.00, stock: 25, creadoEn: new Date() }, + { id: 5, nombre: 'Silla Ergonómica', precio: 320.75, stock: 10, creadoEn: new Date() }, + { id: 6, nombre: 'Webcam HD 1080p', precio: 80.00, stock: 60, creadoEn: new Date() }, + { id: 7, nombre: 'Micrófono de Condensador', precio: 150.00, stock: 30, creadoEn: new Date() }, + { id: 8, nombre: 'Disco Duro Externo 2TB', precio: 95.99, stock: 80, creadoEn: new Date() }, + { id: 9, nombre: 'Memoria RAM 16GB DDR4', precio: 75.00, stock: 120, creadoEn: new Date() }, + { id: 10, nombre: 'Tarjeta Gráfica RTX 4070', precio: 950.00, stock: 8, creadoEn: new Date() }, + { id: 11, nombre: 'Alfombrilla XL', precio: 25.00, stock: 200, creadoEn: new Date() }, + { id: 12, nombre: 'Auriculares Inalámbricos', precio: 180.25, stock: 55, creadoEn: new Date() }, + { id: 13, nombre: 'Router Wi-Fi 6', precio: 110.00, stock: 22, creadoEn: new Date() }, + { id: 14, nombre: 'Impresora Multifuncional', precio: 230.50, stock: 18, creadoEn: new Date() }, + { id: 15, nombre: 'Tableta Gráfica', precio: 300.00, stock: 28, creadoEn: new Date() } +]; + +module.exports = products; \ No newline at end of file diff --git a/practica1-crud/src/routes/product.routes.js b/practica1-crud/src/routes/product.routes.js new file mode 100644 index 0000000..baf5365 --- /dev/null +++ b/practica1-crud/src/routes/product.routes.js @@ -0,0 +1,28 @@ +// src/routes/product.routes.js +const express = require('express'); +const router = express.Router(); + +// Importamos el controlador +const productController = require('../controllers/product.controller'); + +// Importamos nuestro middleware de validación +const { productValidationRules, handleValidationErrors } = require('../middlewares/productValidator'); + +// Definimos las rutas y las asociamos a las funciones del controlador + +// Obtener todos los productos (GET /api/products) +router.get('/', productController.getAllProducts); + +// Obtener un producto por ID (GET /api/products/1) +router.get('/:id', productController.getProductById); + +// Crear un nuevo producto (POST /api/products) contiene validación middleware con productValidationRules -> expres-validator +router.post('/', productValidationRules, handleValidationErrors, productController.createProduct); + +// Actualizar un producto existente (PUT /api/products/1) contiene validación middleware con productValidationRules -> expres-validator +router.put('/:id', productValidationRules, handleValidationErrors, productController.updateProduct); + +// Eliminar un producto (DELETE /api/products/1) +router.delete('/:id', productController.deleteProduct); + +module.exports = router; diff --git a/practica1-crud/src/services/product.service.js b/practica1-crud/src/services/product.service.js new file mode 100644 index 0000000..86898d3 --- /dev/null +++ b/practica1-crud/src/services/product.service.js @@ -0,0 +1,79 @@ +// src/services/product.service.js +const products = require('../models/product.model'); + +let nextId = products.length > 0 ? Math.max(...products.map(p => p.id)) + 1 : 1; + +const getAllProducts = (options = {}) => { + // Desestructuramos las opciones con valores por defecto + const { page = 1, limit = 10, sortBy = 'id', sortOrder = 'asc' } = options; + + // Hacemos una copia para no modificar el array original + let filteredProducts = [...products]; + + // 1. Lógica de Ordenamiento + filteredProducts.sort((a, b) => { + if (a[sortBy] < b[sortBy]) { + return sortOrder === 'asc' ? -1 : 1; + } + if (a[sortBy] > b[sortBy]) { + return sortOrder === 'asc' ? 1 : -1; + } + return 0; + }); + + // 2. Lógica de Paginación + const startIndex = (page - 1) * limit; + const endIndex = page * limit; + const paginatedProducts = filteredProducts.slice(startIndex, endIndex); + + // 3. Metadatos de paginación + const totalItems = products.length; + const totalPages = Math.ceil(totalItems / limit); + + return { + data: paginatedProducts, + pagination: { + totalItems, + totalPages, + currentPage: parseInt(page), + itemsPerPage: parseInt(limit) + } + }; +}; + +const getProductById = (id) => { + return products.find(p => p.id === parseInt(id)); +}; + +const createProduct = (productData) => { + const newProduct = { + id: nextId++, + ...productData, + creadoEn: new Date() + }; + products.push(newProduct); + return newProduct; +}; + +const updateProduct = (id, productData) => { + const productIndex = products.findIndex(p => p.id === parseInt(id)); + if (productIndex === -1) return null; + const updatedProduct = { ...products[productIndex], ...productData }; + products[productIndex] = updatedProduct; + return updatedProduct; +}; + +const deleteProduct = (id) => { + const productIndex = products.findIndex(p => p.id === parseInt(id)); + if (productIndex === -1) return null; + const [deletedProduct] = products.splice(productIndex, 1); + return deletedProduct; +}; + +module.exports = { + getAllProducts, + getProductById, + createProduct, + updateProduct, + deleteProduct, +}; \ No newline at end of file diff --git a/practica1-crud/src/utils/responseHandler.js b/practica1-crud/src/utils/responseHandler.js new file mode 100644 index 0000000..b7c33bc --- /dev/null +++ b/practica1-crud/src/utils/responseHandler.js @@ -0,0 +1,34 @@ +// src/utils/responseHandler.js + +const success = (res, data, statusCode = 200, pagination) => { + const response = { + success: true, + status: statusCode, + }; + + // Añadimos el objeto de paginación a la respuesta solo si existe + if (pagination) { + response.pagination = pagination; + } + + // Siempre añadimos los datos + response.data = data; + + res.status(statusCode).json(response); +}; + +const error1 = (res, message, statusCode = 500, errorCode = 'INTERNAL_ERROR') => { + res.status(statusCode).json({ + success: false, + status: statusCode, + error: { + code: errorCode, + message: message + } + }); +}; + +module.exports = { + success, + error1 // Corregido de 'error1' a 'error' +}; \ No newline at end of file