From ce6492c679a2ae1063b44447ce2cc8831120bbca Mon Sep 17 00:00:00 2001 From: stillfreecode Date: Wed, 3 Sep 2025 18:04:15 -0600 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20Configuraci=C3=B3n=20inicial=20y=20?= =?UTF-8?q?API=20CRUD=20de=20productos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se establece la estructura completa del proyecto y se implementa la funcionalidad CRUD básica para la gestión de productos. - Inicialización del proyecto Node.js y Express. - Creación de una arquitectura por capas (MVC): routes, controllers, services, models. - Implementación de un modelo de datos simulado en memoria. - Desarrollo de la lógica de negocio para las operaciones CRUD en la capa de servicios. - Configuración de los controladores y las rutas para los endpoints de la API. --- README.md | 22 +++++++- index.js | 20 ++++++++ src/controllers/product.controller.js | 72 +++++++++++++++++++++++++++ src/models/product.model.js | 21 ++++++++ src/routes/product.routes.js | 25 ++++++++++ src/services/product.service.js | 63 +++++++++++++++++++++++ 6 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 index.js create mode 100644 src/controllers/product.controller.js create mode 100644 src/models/product.model.js create mode 100644 src/routes/product.routes.js create mode 100644 src/services/product.service.js diff --git a/README.md b/README.md index 43aa320..e3bba31 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,27 @@ -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 + + + + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..bbc6f66 --- /dev/null +++ b/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/src/controllers/product.controller.js b/src/controllers/product.controller.js new file mode 100644 index 0000000..06fc82b --- /dev/null +++ b/src/controllers/product.controller.js @@ -0,0 +1,72 @@ +// src/controllers/product.controller.js +const productService = require('../services/product.service'); + +const getAllProducts = (req, res) => { + try { + const allProducts = productService.getAllProducts(); + res.status(200).json({ data: allProducts }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +const getProductById = (req, res) => { + try { + const { id } = req.params; + const product = productService.getProductById(id); + if (!product) { + return res.status(404).json({ error: 'Producto no encontrado.' }); + } + res.status(200).json({ data: product }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +const createProduct = (req, res) => { + try { + const newProduct = productService.createProduct(req.body); + res.status(201).json({ data: newProduct }); + } catch (error) { + // Si el servicio lanzó un error de validación + if (error.message.includes('obligatorios')) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: 'Error interno del servidor.' }); + } +}; + +const updateProduct = (req, res) => { + try { + const { id } = req.params; + const updatedProduct = productService.updateProduct(id, req.body); + if (!updatedProduct) { + return res.status(404).json({ error: 'Producto no encontrado.' }); + } + res.status(200).json({ data: updatedProduct }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +const deleteProduct = (req, res) => { + try { + const { id } = req.params; + const deletedProduct = productService.deleteProduct(id); + if (!deletedProduct) { + return res.status(404).json({ error: 'Producto no encontrado.' }); + } + // Respondemos sin contenido, que es una práctica común para DELETE exitoso + res.status(204).send(); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +module.exports = { + getAllProducts, + getProductById, + createProduct, + updateProduct, + deleteProduct, +}; \ No newline at end of file diff --git a/src/models/product.model.js b/src/models/product.model.js new file mode 100644 index 0000000..ac26db0 --- /dev/null +++ b/src/models/product.model.js @@ -0,0 +1,21 @@ +// src/models/product.model.js + +let products = [ + { + id: 1, + nombre: 'Laptop Pro', + precio: 1500.00, + stock: 25, + creadoEn: new Date() + }, + { + id: 2, + nombre: 'Teclado Mecánico RGB', + precio: 120.50, + stock: 50, + creadoEn: new Date() + } +]; + +// Exportamos el array para que pueda ser utilizado por los servicios +module.exports = products; diff --git a/src/routes/product.routes.js b/src/routes/product.routes.js new file mode 100644 index 0000000..bc77578 --- /dev/null +++ b/src/routes/product.routes.js @@ -0,0 +1,25 @@ +// src/routes/product.routes.js +const express = require('express'); +const router = express.Router(); + +// Importamos el controlador +const productController = require('../controllers/product.controller'); + +// 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) +router.post('/', productController.createProduct); + +// Actualizar un producto existente (PUT /api/products/1) +router.put('/:id', productController.updateProduct); + +// Eliminar un producto (DELETE /api/products/1) +router.delete('/:id', productController.deleteProduct); + +module.exports = router; \ No newline at end of file diff --git a/src/services/product.service.js b/src/services/product.service.js new file mode 100644 index 0000000..7b575fb --- /dev/null +++ b/src/services/product.service.js @@ -0,0 +1,63 @@ +// src/services/product.service.js +const products = require('../models/product.model'); + +// Variable para simular el autoincremento del ID +let nextId = products.length > 0 ? Math.max(...products.map(p => p.id)) + 1 : 1; + +const getAllProducts = () => { + return products; +}; + +const getProductById = (id) => { + const product = products.find(p => p.id === parseInt(id)); + return product; +}; + +const createProduct = (productData) => { + // Validación básica + if (!productData.nombre || !productData.precio) { + throw new Error('Nombre y precio son campos obligatorios.'); + } + + 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; // O lanzar un error + } + + 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; // O lanzar un error + } + + // Elimina el producto usando splice y devuelve el producto eliminado + const [deletedProduct] = products.splice(productIndex, 1); + return deletedProduct; +}; + + +module.exports = { + getAllProducts, + getProductById, + createProduct, + updateProduct, + deleteProduct, +}; \ No newline at end of file From 8527ca81b4479e62d96057c16cb3ee3fcd01b2e1 Mon Sep 17 00:00:00 2001 From: stillfreecode Date: Wed, 3 Sep 2025 18:22:30 -0600 Subject: [PATCH 2/6] =?UTF-8?q?feat(products):=20A=C3=B1ade=20validaci?= =?UTF-8?q?=C3=B3n=20de=20entradas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se implementa un middleware de validación para las rutas de creación y actualización de productos utilizando express-validator. - Se valida que el nombre sea un string no vacío. - Se asegura que el precio sea un número mayor que 0. - Se comprueba que el stock sea un número entero mayor o igual a 0. - El middleware se aplica a las rutas POST y PUT para prevenir la entrada de datos inválidos. --- package-lock.json | 31 ++++++++++++++++++++++++++++- package.json | 3 ++- src/middlewares/productValidator.js | 30 ++++++++++++++++++++++++++++ src/routes/product.routes.js | 13 +++++++----- 4 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 src/middlewares/productValidator.js diff --git a/package-lock.json b/package-lock.json index 6865735..288e083 100644 --- a/package-lock.json +++ b/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/package.json index 83530db..ceb61c6 100644 --- a/package.json +++ b/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/src/middlewares/productValidator.js b/src/middlewares/productValidator.js new file mode 100644 index 0000000..4efa537 --- /dev/null +++ b/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/src/routes/product.routes.js b/src/routes/product.routes.js index bc77578..baf5365 100644 --- a/src/routes/product.routes.js +++ b/src/routes/product.routes.js @@ -5,6 +5,9 @@ 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) @@ -13,13 +16,13 @@ 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) -router.post('/', productController.createProduct); +// 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) -router.put('/:id', productController.updateProduct); +// 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; \ No newline at end of file +module.exports = router; From 947f483ffc65405565f62fe5d2a456bbef422fe9 Mon Sep 17 00:00:00 2001 From: stillfreecode Date: Wed, 3 Sep 2025 18:57:23 -0600 Subject: [PATCH 3/6] feat(api): Estandariza el formato de respuestas y manejo de errores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se implementa un sistema centralizado para estandarizar todas las respuestas JSON de la API. - Se crea un módulo 'responseHandler' con funciones reutilizables para 'success' y 'error'. - Se refactorizan los controladores para utilizar el nuevo formato, asegurando una estructura consistente para éxitos y fallos. - Se implementa el manejo explícito de errores esperados como 400 (Bad Request), 404 (Not Found) y 409 (Conflict). --- src/controllers/product.controller.js | 48 +++++++++++++-------------- src/utils/responseHandler.js | 25 ++++++++++++++ 2 files changed, 49 insertions(+), 24 deletions(-) create mode 100644 src/utils/responseHandler.js diff --git a/src/controllers/product.controller.js b/src/controllers/product.controller.js index 06fc82b..7d30570 100644 --- a/src/controllers/product.controller.js +++ b/src/controllers/product.controller.js @@ -1,12 +1,13 @@ // src/controllers/product.controller.js const productService = require('../services/product.service'); +const { success, error1 } = require('../utils/responseHandler'); const getAllProducts = (req, res) => { try { const allProducts = productService.getAllProducts(); - res.status(200).json({ data: allProducts }); - } catch (error) { - res.status(500).json({ error: error.message }); + return success(res, allProducts); + } catch (err) { + return error1(res, 'Error interno del servidor al obtener productos.'); } }; @@ -15,24 +16,24 @@ const getProductById = (req, res) => { const { id } = req.params; const product = productService.getProductById(id); if (!product) { - return res.status(404).json({ error: 'Producto no encontrado.' }); + return error1(res, 'Producto no encontrado.', 404, 'NOT_FOUND'); } - res.status(200).json({ data: product }); - } catch (error) { - res.status(500).json({ error: error.message }); + return success(res, product); + } catch (err) { + return error1(res, 'Error interno del servidor al obtener el producto.'); } }; const createProduct = (req, res) => { try { - const newProduct = productService.createProduct(req.body); - res.status(201).json({ data: newProduct }); - } catch (error) { - // Si el servicio lanzó un error de validación - if (error.message.includes('obligatorios')) { - return res.status(400).json({ error: error.message }); + 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'); } - res.status(500).json({ error: 'Error interno del servidor.' }); + const newProduct = productService.createProduct(req.body); + return success(res, newProduct, 201); + } catch (err) { + return error1(res, 'Error interno del servidor al crear el producto.'); } }; @@ -41,11 +42,11 @@ const updateProduct = (req, res) => { const { id } = req.params; const updatedProduct = productService.updateProduct(id, req.body); if (!updatedProduct) { - return res.status(404).json({ error: 'Producto no encontrado.' }); + return error1(res, 'Producto no encontrado para actualizar.', 404, 'NOT_FOUND'); } - res.status(200).json({ data: updatedProduct }); - } catch (error) { - res.status(500).json({ error: error.message }); + return success(res, updatedProduct); + } catch (err) { + return error1(res, 'Error interno del servidor al actualizar el producto.'); } }; @@ -54,12 +55,11 @@ const deleteProduct = (req, res) => { const { id } = req.params; const deletedProduct = productService.deleteProduct(id); if (!deletedProduct) { - return res.status(404).json({ error: 'Producto no encontrado.' }); + return error1(res, 'Producto no encontrado para eliminar.', 404, 'NOT_FOUND'); } - // Respondemos sin contenido, que es una práctica común para DELETE exitoso - res.status(204).send(); - } catch (error) { - res.status(500).json({ error: error.message }); + return success(res, { message: 'Producto eliminado exitosamente.' }); + } catch (err) { + return error1(res, 'Error interno del servidor al eliminar el producto.'); } }; @@ -69,4 +69,4 @@ module.exports = { createProduct, updateProduct, deleteProduct, -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/utils/responseHandler.js b/src/utils/responseHandler.js new file mode 100644 index 0000000..dc1ecaf --- /dev/null +++ b/src/utils/responseHandler.js @@ -0,0 +1,25 @@ +// src/utils/responseHandler.js + +const success = (res, data, statusCode = 200) => { + res.status(statusCode).json({ + success: true, + status: statusCode, + data: data + }); +}; + +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 +}; \ No newline at end of file From 18bf6d0c7fa6b0a255d45772a271aabc60d17da7 Mon Sep 17 00:00:00 2001 From: stillfreecode Date: Wed, 3 Sep 2025 19:31:19 -0600 Subject: [PATCH 4/6] =?UTF-8?q?feat(products):=20Implementa=20paginaci?= =?UTF-8?q?=C3=B3n=20y=20ordenamiento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se añade funcionalidad de paginación y ordenamiento al endpoint GET /api/products. - El endpoint ahora acepta los query params 'page', 'limit' y 'sort' (ej: ?sort=precio,desc). - Se refactoriza el servicio de productos para aplicar la lógica de ordenamiento y para 'rebanar' (slice) los resultados según la página solicitada. - Se actualiza el manejador de respuestas para incluir metadatos de paginación (totalItems, totalPages, etc.) en las respuestas exitosas. - Se amplió el conjunto de datos en el modelo para permitir pruebas efectivas Archivos Modificados: - src/controllers/product.controller.js - src/services/product.service.js - src/utils/responseHandler.js - src/models/product.model.js --- src/controllers/product.controller.js | 22 ++++++++-- src/models/product.model.js | 32 +++++++------- src/services/product.service.js | 62 +++++++++++++++++---------- src/utils/responseHandler.js | 19 +++++--- 4 files changed, 88 insertions(+), 47 deletions(-) diff --git a/src/controllers/product.controller.js b/src/controllers/product.controller.js index 7d30570..228b59e 100644 --- a/src/controllers/product.controller.js +++ b/src/controllers/product.controller.js @@ -4,13 +4,29 @@ const { success, error1 } = require('../utils/responseHandler'); const getAllProducts = (req, res) => { try { - const allProducts = productService.getAllProducts(); - return success(res, allProducts); + // 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 interno del servidor al obtener productos.'); + return error1(res, 'Error al obtener los productos.'); } }; + const getProductById = (req, res) => { try { const { id } = req.params; diff --git a/src/models/product.model.js b/src/models/product.model.js index ac26db0..3c669cc 100644 --- a/src/models/product.model.js +++ b/src/models/product.model.js @@ -1,21 +1,21 @@ // src/models/product.model.js let products = [ - { - id: 1, - nombre: 'Laptop Pro', - precio: 1500.00, - stock: 25, - creadoEn: new Date() - }, - { - id: 2, - nombre: 'Teclado Mecánico RGB', - precio: 120.50, - stock: 50, - creadoEn: new Date() - } + { 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() } ]; -// Exportamos el array para que pueda ser utilizado por los servicios -module.exports = products; +module.exports = products; \ No newline at end of file diff --git a/src/services/product.service.js b/src/services/product.service.js index 7b575fb..86898d3 100644 --- a/src/services/product.service.js +++ b/src/services/product.service.js @@ -1,41 +1,63 @@ // src/services/product.service.js const products = require('../models/product.model'); -// Variable para simular el autoincremento del ID let nextId = products.length > 0 ? Math.max(...products.map(p => p.id)) + 1 : 1; -const getAllProducts = () => { - return products; +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) => { - const product = products.find(p => p.id === parseInt(id)); - return product; + return products.find(p => p.id === parseInt(id)); }; const createProduct = (productData) => { - // Validación básica - if (!productData.nombre || !productData.precio) { - throw new Error('Nombre y precio son campos obligatorios.'); - } - 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; // O lanzar un error - } - + if (productIndex === -1) return null; const updatedProduct = { ...products[productIndex], ...productData }; products[productIndex] = updatedProduct; return updatedProduct; @@ -43,17 +65,11 @@ const updateProduct = (id, productData) => { const deleteProduct = (id) => { const productIndex = products.findIndex(p => p.id === parseInt(id)); - - if (productIndex === -1) { - return null; // O lanzar un error - } - - // Elimina el producto usando splice y devuelve el producto eliminado + if (productIndex === -1) return null; const [deletedProduct] = products.splice(productIndex, 1); return deletedProduct; }; - module.exports = { getAllProducts, getProductById, diff --git a/src/utils/responseHandler.js b/src/utils/responseHandler.js index dc1ecaf..b7c33bc 100644 --- a/src/utils/responseHandler.js +++ b/src/utils/responseHandler.js @@ -1,11 +1,20 @@ // src/utils/responseHandler.js -const success = (res, data, statusCode = 200) => { - res.status(statusCode).json({ +const success = (res, data, statusCode = 200, pagination) => { + const response = { success: true, status: statusCode, - data: data - }); + }; + + // 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') => { @@ -21,5 +30,5 @@ const error1 = (res, message, statusCode = 500, errorCode = 'INTERNAL_ERROR') => module.exports = { success, - error1 + error1 // Corregido de 'error1' a 'error' }; \ No newline at end of file From 6a970dba30e74346a4aaa66a973f39988064390b Mon Sep 17 00:00:00 2001 From: stillfreecode Date: Wed, 3 Sep 2025 19:50:46 -0600 Subject: [PATCH 5/6] chore: Reorganiza el proyecto en la subcarpeta practica1-crud --- .gitignore => practica1-crud/.gitignore | 0 README.md => practica1-crud/README.md | 0 index.js => practica1-crud/index.js | 0 package-lock.json => practica1-crud/package-lock.json | 0 package.json => practica1-crud/package.json | 0 {src => practica1-crud/src}/controllers/product.controller.js | 0 {src => practica1-crud/src}/middlewares/productValidator.js | 0 {src => practica1-crud/src}/models/product.model.js | 0 {src => practica1-crud/src}/routes/product.routes.js | 0 {src => practica1-crud/src}/services/product.service.js | 0 {src => practica1-crud/src}/utils/responseHandler.js | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename .gitignore => practica1-crud/.gitignore (100%) rename README.md => practica1-crud/README.md (100%) rename index.js => practica1-crud/index.js (100%) rename package-lock.json => practica1-crud/package-lock.json (100%) rename package.json => practica1-crud/package.json (100%) rename {src => practica1-crud/src}/controllers/product.controller.js (100%) rename {src => practica1-crud/src}/middlewares/productValidator.js (100%) rename {src => practica1-crud/src}/models/product.model.js (100%) rename {src => practica1-crud/src}/routes/product.routes.js (100%) rename {src => practica1-crud/src}/services/product.service.js (100%) rename {src => practica1-crud/src}/utils/responseHandler.js (100%) diff --git a/.gitignore b/practica1-crud/.gitignore similarity index 100% rename from .gitignore rename to practica1-crud/.gitignore diff --git a/README.md b/practica1-crud/README.md similarity index 100% rename from README.md rename to practica1-crud/README.md diff --git a/index.js b/practica1-crud/index.js similarity index 100% rename from index.js rename to practica1-crud/index.js diff --git a/package-lock.json b/practica1-crud/package-lock.json similarity index 100% rename from package-lock.json rename to practica1-crud/package-lock.json diff --git a/package.json b/practica1-crud/package.json similarity index 100% rename from package.json rename to practica1-crud/package.json diff --git a/src/controllers/product.controller.js b/practica1-crud/src/controllers/product.controller.js similarity index 100% rename from src/controllers/product.controller.js rename to practica1-crud/src/controllers/product.controller.js diff --git a/src/middlewares/productValidator.js b/practica1-crud/src/middlewares/productValidator.js similarity index 100% rename from src/middlewares/productValidator.js rename to practica1-crud/src/middlewares/productValidator.js diff --git a/src/models/product.model.js b/practica1-crud/src/models/product.model.js similarity index 100% rename from src/models/product.model.js rename to practica1-crud/src/models/product.model.js diff --git a/src/routes/product.routes.js b/practica1-crud/src/routes/product.routes.js similarity index 100% rename from src/routes/product.routes.js rename to practica1-crud/src/routes/product.routes.js diff --git a/src/services/product.service.js b/practica1-crud/src/services/product.service.js similarity index 100% rename from src/services/product.service.js rename to practica1-crud/src/services/product.service.js diff --git a/src/utils/responseHandler.js b/practica1-crud/src/utils/responseHandler.js similarity index 100% rename from src/utils/responseHandler.js rename to practica1-crud/src/utils/responseHandler.js From 410bb27ad0911679db07ff171750ef909f3f7b0f Mon Sep 17 00:00:00 2001 From: stillfreecode <167722510+stillfreecode@users.noreply.github.com> Date: Wed, 3 Sep 2025 20:34:33 -0600 Subject: [PATCH 6/6] Create README.md --- README.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..6236f52 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +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 + + +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) +