Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Node — Inventory System

Fastify · CQRS · Event-Driven · SQLite nativo


La idea central

Este proyecto combina dos patrones:

CQRS (Command Query Responsibility Segregation): separa estrictamente las operaciones que mutan el estado (Commands) de las que solo leen (Queries). Cada una tiene su propio flujo y nunca se mezclan.

Event-Driven: cuando un Command completa su acción, publica un evento en el bus. Múltiples handlers reaccionan de forma independiente sin que el Command los conozca.

┌─────────────────────────────────────────────────────────────┐
│  REQUEST HTTP                                               │
│       ↓                                                     │
│  POST /inventory/items     →  CreateItemHandler (Command)   │
│                                      ↓                      │
│                              muta la base de datos          │
│                                      ↓                      │
│                         eventBus.publish('item.created')    │
│                                      ↓                      │
│                        ┌─────────────────────────┐          │
│                        │  EventLogHandler        │  ← persiste evento en DB
│                        │  ItemCreatedAuditHandler│  ← log en consola
│                        └─────────────────────────┘          │
│                                                             │
│  GET /inventory/items      →  GetAllItemsHandler (Query)    │
│                                      ↓                      │
│                              solo lee, nunca muta           │
│                              nunca publica eventos          │
└─────────────────────────────────────────────────────────────┘

Estructura del proyecto

node-fastify-cqrs/
├── src/
│   ├── server.js                          # Punto de entrada + wiring
│   ├── commands/
│   │   └── handlers/
│   │       └── index.js                   # CreateItem, UpdateStock, UpdateItem, DeleteItem
│   ├── queries/
│   │   └── handlers/
│   │       └── index.js                   # GetAllItems, GetItemById, GetLowStock, GetMovements, GetEventLog
│   ├── events/
│   │   ├── bus/
│   │   │   └── EventBus.js                # Bus de eventos (EventEmitter)
│   │   └── handlers/
│   │       ├── index.js                   # eventLogHandler, stockAlertHandler, auditHandlers
│   │       └── register.js                # Registra handlers en el bus al arrancar
│   ├── infrastructure/
│   │   ├── database/
│   │   │   └── sqlite.js                  # SQLite nativo Node 22
│   │   └── repositories/
│   │       └── ItemRepository.js          # Acceso a datos: items, movements, event_log
│   └── interfaces/
│       ├── routes/
│       │   └── inventory.js               # Rutas Fastify — GET=queries, POST/PUT/DELETE=commands
│       └── middleware/
│           └── errorHandler.js

Tablas en la base de datos

Tabla Propósito
items Catálogo de ítems del inventario
stock_movements Historial de cada entrada/salida/ajuste de stock
event_log Registro persistente de todos los eventos del bus

Requisitos previos

  • Node.js 22+ Verifica con: node --version

Usa el módulo SQLite nativo de Node 22 (node:sqlite). No necesitas drivers adicionales.


Cómo correr el proyecto

1. Instala las dependencias

npm install

2. Corre el servidor

# Modo desarrollo (reinicia al guardar cambios)
npm run dev

# O modo normal
npm start

El servidor arranca en: http://localhost:3001

La base de datos inventory.db se crea automáticamente al primer arranque.

3. Verifica que funciona

GET http://localhost:3001/

Respuesta:

{
  "status": "ok",
  "project": "Node - Fastify CQRS + Event-Driven",
  "architecture": "CQRS + EventEmitter Bus"
}

En la consola del servidor verás:

✅ [EventBus] 4 handlers registrados
🚀 Servidor CQRS corriendo en http://localhost:3001
📡 Bus de eventos activo

Cómo probarlo en Postman

URL base

http://localhost:3001

COMMANDS (mutan estado + disparan eventos)

1. Crear un ítem

POST /inventory/items

{
  "sku": "LAP-001",
  "name": "Laptop Pro 15",
  "description": "Laptop de alto rendimiento",
  "quantity": 10,
  "minStock": 5,
  "unitPrice": 25999.99,
  "category": "electronica"
}

En consola verás: 📦 [AUDIT] Nuevo ítem creado: "Laptop Pro 15" | SKU: LAP-001

2. Entrada de stock

PUT /inventory/items/{id}/stock

{
  "type": "in",
  "quantity": 20,
  "reason": "Reposición de mercancía"
}

3. Salida de stock

PUT /inventory/items/{id}/stock

{
  "type": "out",
  "quantity": 8,
  "reason": "Venta al cliente #1042"
}

Si el stock baja del minStock, verás en consola: ⚠️ [STOCK ALERT] "Laptop Pro 15" (SKU: LAP-001) | Stock actual: 2 | Mínimo: 5

4. Ajuste de stock (inventario físico)

PUT /inventory/items/{id}/stock

{
  "type": "adjust",
  "quantity": 15,
  "reason": "Ajuste tras conteo físico"
}

Valores válidos para type: "in" · "out" · "adjust"

5. Actualizar datos del ítem

PUT /inventory/items/{id}

{
  "name": "Laptop Pro 15 v2",
  "unitPrice": 23999.99,
  "minStock": 8
}

Todos los campos son opcionales.

6. Eliminar ítem

DELETE /inventory/items/{id}


QUERIES (solo leen, sin efectos)

7. Listar todos los ítems

GET /inventory/items

Query params opcionales:

  • category → filtra por categoría
  • lowStock=true → solo los que tienen stock bajo o en mínimo
  • skip / take → paginación
GET /inventory/items
GET /inventory/items?category=electronica
GET /inventory/items?lowStock=true
GET /inventory/items?skip=0&take=10

8. Ver ítem específico

GET /inventory/items/{id}

9. Ítems con stock bajo

GET /inventory/items/low-stock

Devuelve todos los ítems donde quantity <= minStock.

10. Historial de movimientos de un ítem

GET /inventory/items/{id}/movements

Muestra el historial completo de entradas, salidas y ajustes de ese ítem.

11. Log de eventos del sistema

GET /inventory/events/log

Query params:

  • limit → cantidad máxima de eventos (default 50)

Muestra todos los eventos que han pasado por el bus, en orden cronológico descendente. Verás item.created, stock.updated, item.deleted, etc. con su payload completo.


Flujo completo recomendado en Postman

  1. POST /inventory/items — crea un ítem con quantity: 8 y minStock: 5
  2. GET /inventory/items — confirma que aparece con isLowStock: false
  3. PUT /inventory/items/{id}/stock con type: "out", quantity: 4 — baja a 4 (bajo del mínimo)
  4. Revisa la consola — verás el ⚠️ STOCK ALERT
  5. GET /inventory/items/low-stock — aparece en la lista de stock bajo
  6. PUT /inventory/items/{id}/stock con type: "in", quantity: 20 — repones stock
  7. GET /inventory/items/{id}/movements — ves el historial de los 2 movimientos
  8. GET /inventory/events/log — ves todos los eventos: item.created, stock.updated x2

Escenarios de error para probar

Escenario Request Error esperado
SKU duplicado POST con el mismo sku 409
Stock insuficiente type: "out" con más quantity que stock 409
Ítem no existe GET /inventory/items/id-falso 404
type inválido PUT stock con type: "entrada" 422
Campos faltantes POST sin sku o category 422

Colección Postman (importar)

Guarda como n2-inventory.postman_collection.json:

{
  "info": {
    "name": "Node - Fastify CQRS + Event-Driven",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    { "key": "base_url", "value": "http://localhost:3001" },
    { "key": "item_id", "value": "" }
  ],
  "item": [
    {
      "name": "Health Check",
      "request": { "method": "GET", "url": "{{base_url}}/" }
    },
    {
      "name": "Commands",
      "item": [
        {
          "name": "Create Item",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "const r = pm.response.json(); if(r.id) pm.collectionVariables.set('item_id', r.id);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "url": "{{base_url}}/inventory/items",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"sku\": \"LAP-001\", \"name\": \"Laptop Pro\", \"quantity\": 10, \"minStock\": 5, \"unitPrice\": 25999.99, \"category\": \"electronica\"}"
            }
          }
        },
        {
          "name": "Stock IN",
          "request": {
            "method": "PUT",
            "url": "{{base_url}}/inventory/items/{{item_id}}/stock",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"type\": \"in\", \"quantity\": 20, \"reason\": \"Reposicion\"}"
            }
          }
        },
        {
          "name": "Stock OUT",
          "request": {
            "method": "PUT",
            "url": "{{base_url}}/inventory/items/{{item_id}}/stock",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"type\": \"out\", \"quantity\": 8, \"reason\": \"Venta al cliente\"}"
            }
          }
        },
        {
          "name": "Stock ADJUST",
          "request": {
            "method": "PUT",
            "url": "{{base_url}}/inventory/items/{{item_id}}/stock",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"type\": \"adjust\", \"quantity\": 15, \"reason\": \"Conteo fisico\"}"
            }
          }
        },
        {
          "name": "Update Item",
          "request": {
            "method": "PUT",
            "url": "{{base_url}}/inventory/items/{{item_id}}",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"unitPrice\": 23999.99, \"minStock\": 8}"
            }
          }
        },
        {
          "name": "Delete Item",
          "request": {
            "method": "DELETE",
            "url": "{{base_url}}/inventory/items/{{item_id}}"
          }
        }
      ]
    },
    {
      "name": "Queries",
      "item": [
        {
          "name": "List All Items",
          "request": { "method": "GET", "url": "{{base_url}}/inventory/items" }
        },
        {
          "name": "Filter by Category",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/inventory/items?category=electronica"
          }
        },
        {
          "name": "Get Item",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/inventory/items/{{item_id}}"
          }
        },
        {
          "name": "Low Stock Items",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/inventory/items/low-stock"
          }
        },
        {
          "name": "Stock Movements",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/inventory/items/{{item_id}}/movements"
          }
        },
        {
          "name": "Event Log",
          "request": {
            "method": "GET",
            "url": "{{base_url}}/inventory/events/log"
          }
        }
      ]
    }
  ]
}

Despliegue en Render

Este proyecto está desplegado como un Web Service en Render.

Pasos para desplegar

  1. En el dashboard de Render, crea un nuevo Web Service y conecta el repositorio.

  2. Configura el servicio:

    Campo Valor
    Environment Node
    Build Command npm install
    Start Command npm start
  3. No se requieren variables de entorno. La base de datos inventory.db se crea automáticamente al primer arranque en el filesystem del contenedor.

El plan gratuito de Render usa un filesystem efímero: el log de eventos y el inventario se reinician con cada nuevo deploy. Para producción real se recomienda una base de datos externa.

  1. Una vez desplegado, copia la URL pública (ej. https://node-fastify-cqrs.onrender.com) y pégala en el panel de ajustes de API Explorer para apuntar al entorno de producción.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages