feat: add Redis cache, gzip, CI/CD via Gitea self-hosted runner
This commit is contained in:
BIN
.github/.DS_Store
vendored
BIN
.github/.DS_Store
vendored
Binary file not shown.
31
.github/workflows/ci.yml
vendored
Normal file
31
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: CI - Build and Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Generate Prisma client
|
||||
run: npx prisma generate
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint --if-present || true
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
[ -d dist ] && echo "✅ Build OK" || (echo "❌ dist not found" && exit 1)
|
||||
45
.github/workflows/deploy.yml
vendored
45
.github/workflows/deploy.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Deploy API Ubigeo
|
||||
name: Deploy API-Ubigeo
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -6,40 +6,25 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy al VPS via SSH
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: 158.220.106.131
|
||||
username: root
|
||||
password: ${{ secrets.VPS_PASSWORD }}
|
||||
script: |
|
||||
set -e
|
||||
cd /home/deployer/api-ubigeo || (mkdir -p /home/deployer/api-ubigeo && cd /home/deployer/api-ubigeo)
|
||||
- name: Copy source to server
|
||||
run: |
|
||||
rsync -av --delete \
|
||||
--exclude='.git' \
|
||||
--exclude='node_modules' \
|
||||
--exclude='dist' \
|
||||
--exclude='.env' \
|
||||
. /home/deployer/api-ubigeo/
|
||||
|
||||
# Pull o clone
|
||||
if [ -d ".git" ]; then
|
||||
git pull origin main
|
||||
else
|
||||
git clone https://github.com/${{ github.repository }} .
|
||||
fi
|
||||
- name: Build & deploy
|
||||
run: bash /home/deployer/deploy-ubigeo.sh
|
||||
|
||||
# Build imagen
|
||||
docker build -t ubigeo-api:latest .
|
||||
|
||||
# Deploy con compose
|
||||
docker compose -f docker-compose.production.yml up -d --force-recreate api
|
||||
|
||||
# Ejecutar migraciones
|
||||
docker exec ubigeo-api npx prisma migrate deploy
|
||||
|
||||
# Verificar health
|
||||
- name: Health check
|
||||
run: |
|
||||
sleep 10
|
||||
curl -f http://localhost:3200/api/v1/health || exit 1
|
||||
|
||||
echo "✅ Deploy exitoso"
|
||||
curl -f https://api-ubigeo.darkcodex.dev/api/v1/health || exit 1
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,3 +6,5 @@ dist/
|
||||
*.log
|
||||
/generated/
|
||||
prisma/seed/*.js
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
@@ -4,8 +4,12 @@ WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
COPY prisma ./prisma/
|
||||
COPY prisma.config.ts ./
|
||||
RUN npm ci
|
||||
|
||||
# Generate Prisma client before building
|
||||
RUN npx prisma generate
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
@@ -16,8 +20,12 @@ WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
COPY prisma ./prisma/
|
||||
COPY prisma.config.ts ./
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
# Generate Prisma client in production stage too
|
||||
RUN npx prisma generate
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
|
||||
|
||||
@@ -18,6 +18,26 @@ services:
|
||||
- ubigeo_network
|
||||
- easypanel
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: ubigeo-redis
|
||||
restart: unless-stopped
|
||||
command: >
|
||||
redis-server
|
||||
--maxmemory 128mb
|
||||
--maxmemory-policy allkeys-lru
|
||||
--appendonly yes
|
||||
--appendfsync everysec
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
networks:
|
||||
- ubigeo_network
|
||||
|
||||
api:
|
||||
image: ubigeo-api:latest
|
||||
container_name: ubigeo-api
|
||||
@@ -26,23 +46,21 @@ services:
|
||||
- "127.0.0.1:3200:3200"
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://ubigeo_user:UbigeoDB2026@postgres:5432/ubigeo_db"
|
||||
REDIS_URL: "redis://ubigeo-redis:6379"
|
||||
PORT: 3200
|
||||
NODE_ENV: production
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.ubigeo.rule=Host(`api-ubigeo.darkcodex.dev`)"
|
||||
- "traefik.http.routers.ubigeo.entrypoints=websecure"
|
||||
- "traefik.http.routers.ubigeo.tls=true"
|
||||
- "traefik.http.services.ubigeo.loadbalancer.server.port=3200"
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- ubigeo_network
|
||||
- easypanel
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
ubigeo_network:
|
||||
|
||||
216
package-lock.json
generated
216
package-lock.json
generated
@@ -9,6 +9,8 @@
|
||||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@keyv/redis": "^5.1.6",
|
||||
"@nestjs/cache-manager": "^3.1.0",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.3",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
@@ -17,8 +19,12 @@
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@prisma/adapter-pg": "^7.4.2",
|
||||
"@prisma/client": "^7.4.2",
|
||||
"@types/compression": "^1.8.1",
|
||||
"cache-manager": "^7.2.8",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.4",
|
||||
"compression": "^1.8.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"helmet": "^8.1.0",
|
||||
"pg": "^8.20.0",
|
||||
"prisma": "^7.4.2",
|
||||
@@ -36,7 +42,6 @@
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
@@ -724,6 +729,16 @@
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/@cacheable/utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.0.tgz",
|
||||
"integrity": "sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hashery": "^1.5.0",
|
||||
"keyv": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@chevrotain/cst-dts-gen": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz",
|
||||
@@ -2130,6 +2145,29 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@keyv/redis": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-5.1.6.tgz",
|
||||
"integrity": "sha512-eKvW6pspvVaU5dxigaIDZr635/Uw6urTXL3gNbY9WTR8d3QigZQT+r8gxYSEOsw4+1cCBsC4s7T2ptR0WC9LfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@redis/client": "^5.10.0",
|
||||
"cluster-key-slot": "^1.1.2",
|
||||
"hookified": "^1.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"keyv": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@keyv/serialize": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
|
||||
"integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lukeed/csprng": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
|
||||
@@ -2171,6 +2209,19 @@
|
||||
"@tybys/wasm-util": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cache-manager": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.1.0.tgz",
|
||||
"integrity": "sha512-pEIqYZrBcE8UdkJmZRduurvoUfdU+3kRPeO1R2muiMbZnRuqlki5klFFNllO9LyYWzrx98bd1j0PSPKSJk1Wbw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"cache-manager": ">=6",
|
||||
"keyv": ">=5",
|
||||
"rxjs": "^7.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cli": {
|
||||
"version": "11.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz",
|
||||
@@ -2937,6 +2988,26 @@
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client": {
|
||||
"version": "5.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.11.0.tgz",
|
||||
"integrity": "sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@node-rs/xxhash": "^1.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@node-rs/xxhash": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
@@ -3088,18 +3159,26 @@
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/compression": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -3146,7 +3225,6 @@
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
|
||||
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
@@ -3158,7 +3236,6 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
|
||||
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -3171,7 +3248,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
@@ -3230,7 +3306,6 @@
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -3241,14 +3316,12 @@
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
|
||||
"integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
@@ -3265,7 +3338,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -3275,7 +3347,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
|
||||
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
@@ -4690,6 +4761,17 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager": {
|
||||
"version": "7.2.8",
|
||||
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.8.tgz",
|
||||
"integrity": "sha512-0HDaDLBBY/maa/LmUVAr70XUOwsiQD+jyzCBjmUErYZUKdMS9dT59PqW59PpVqfGM7ve6H0J6307JTpkCYefHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cacheable/utils": "^2.3.3",
|
||||
"keyv": "^5.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -4985,6 +5067,15 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -5071,6 +5162,60 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/compressible": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": ">= 1.43.0 < 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/compression": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"compressible": "~2.0.18",
|
||||
"debug": "2.6.9",
|
||||
"negotiator": "~0.6.4",
|
||||
"on-headers": "~1.1.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/compression/node_modules/negotiator": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -5386,7 +5531,6 @@
|
||||
"version": "17.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -6168,6 +6312,16 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache/node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
|
||||
@@ -6650,6 +6804,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hashery": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.0.tgz",
|
||||
"integrity": "sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hookified": "^1.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -6681,6 +6847,12 @@
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hookified": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
|
||||
"integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -7941,13 +8113,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
|
||||
"integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
"@keyv/serialize": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/leven": {
|
||||
@@ -8595,6 +8767,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -10706,7 +10887,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@keyv/redis": "^5.1.6",
|
||||
"@nestjs/cache-manager": "^3.1.0",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.3",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
@@ -32,8 +34,12 @@
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@prisma/adapter-pg": "^7.4.2",
|
||||
"@prisma/client": "^7.4.2",
|
||||
"@types/compression": "^1.8.1",
|
||||
"cache-manager": "^7.2.8",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.4",
|
||||
"compression": "^1.8.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"helmet": "^8.1.0",
|
||||
"pg": "^8.20.0",
|
||||
"prisma": "^7.4.2",
|
||||
@@ -51,7 +57,6 @@
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
|
||||
BIN
src/.DS_Store
vendored
BIN
src/.DS_Store
vendored
Binary file not shown.
@@ -1,16 +1,31 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import KeyvRedis from '@keyv/redis';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { UbigeoModule } from './modules/ubigeo/ubigeo.module';
|
||||
import { PaisesModule } from './modules/paises/paises.module';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
|
||||
const TTL_24H = 24 * 60 * 60 * 1000;
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
ThrottlerModule.forRoot([{ ttl: 60000, limit: 200 }]),
|
||||
CacheModule.registerAsync({
|
||||
isGlobal: true,
|
||||
useFactory: () => {
|
||||
const redisUrl = process.env.REDIS_URL;
|
||||
if (redisUrl) {
|
||||
return { stores: [new KeyvRedis(redisUrl)], ttl: TTL_24H };
|
||||
}
|
||||
// fallback: in-memory para desarrollo local
|
||||
return { stores: [], ttl: TTL_24H };
|
||||
},
|
||||
}),
|
||||
PrismaModule,
|
||||
UbigeoModule,
|
||||
PaisesModule,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import * as helmet from 'helmet';
|
||||
import compression = require('compression');
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
@@ -12,6 +13,7 @@ async function bootstrap() {
|
||||
app.enableCors({ origin: '*' });
|
||||
|
||||
app.use(helmet.default());
|
||||
app.use(compression());
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
|
||||
BIN
src/modules/.DS_Store
vendored
BIN
src/modules/.DS_Store
vendored
Binary file not shown.
@@ -1,21 +1,26 @@
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { Controller, Get, Param, Query, Header } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||
import { PaisesService } from './paises.service';
|
||||
|
||||
const CACHE_1DAY = 'public, max-age=86400, stale-while-revalidate=3600';
|
||||
|
||||
@ApiTags('paises')
|
||||
@Controller('paises')
|
||||
export class PaisesController {
|
||||
constructor(private readonly svc: PaisesService) {}
|
||||
|
||||
@Get('stats')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Estadísticas de países' })
|
||||
getStats() { return this.svc.getStats(); }
|
||||
|
||||
@Get('regiones')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Listar regiones del mundo' })
|
||||
getRegiones() { return this.svc.getRegiones(); }
|
||||
|
||||
@Get()
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Listar países (filtrar con ?q= o ?region=)' })
|
||||
@ApiQuery({ name: 'q', required: false, description: 'Buscar por nombre o código ISO' })
|
||||
@ApiQuery({ name: 'region', required: false, description: 'Filtrar por región' })
|
||||
@@ -24,6 +29,7 @@ export class PaisesController {
|
||||
}
|
||||
|
||||
@Get(':codigo')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Obtener país por código ISO (PE, PER, etc.)' })
|
||||
@ApiParam({ name: 'codigo', description: 'Código ISO alpha-2 (PE) o alpha-3 (PER)', example: 'PE' })
|
||||
getPais(@Param('codigo') codigo: string) {
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import type { Cache } from 'cache-manager';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class PaisesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(CACHE_MANAGER) private cache: Cache,
|
||||
) {}
|
||||
|
||||
private async cached<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
||||
const hit = await this.cache.get<T>(key);
|
||||
if (hit !== undefined && hit !== null) return hit;
|
||||
const result = await fn();
|
||||
await this.cache.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async getPaises(search?: string, region?: string) {
|
||||
const key = `paises:list:${search ?? ''}:${region ?? ''}`;
|
||||
return this.cached(key, () => {
|
||||
const where: any = { activo: true };
|
||||
if (search) {
|
||||
where.OR = [
|
||||
@@ -34,9 +49,11 @@ export class PaisesService {
|
||||
longitud: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getPais(codigo: string) {
|
||||
return this.cached(`pais:${codigo.toUpperCase()}`, async () => {
|
||||
const where = codigo.length === 2
|
||||
? { codigoAlpha2: codigo.toUpperCase() }
|
||||
: { codigo: codigo.toUpperCase() };
|
||||
@@ -44,9 +61,11 @@ export class PaisesService {
|
||||
const pais = await this.prisma.pais.findFirst({ where });
|
||||
if (!pais) throw new NotFoundException(`País '${codigo}' no encontrado`);
|
||||
return pais;
|
||||
});
|
||||
}
|
||||
|
||||
async getRegiones() {
|
||||
return this.cached('paises:regiones', async () => {
|
||||
const result = await this.prisma.pais.findMany({
|
||||
where: { activo: true },
|
||||
distinct: ['region'],
|
||||
@@ -54,9 +73,11 @@ export class PaisesService {
|
||||
orderBy: { region: 'asc' },
|
||||
});
|
||||
return result.map((r) => r.region);
|
||||
});
|
||||
}
|
||||
|
||||
async getStats() {
|
||||
return this.cached('stats:paises', async () => {
|
||||
const [total, regiones] = await Promise.all([
|
||||
this.prisma.pais.count({ where: { activo: true } }),
|
||||
this.prisma.pais.findMany({
|
||||
@@ -66,5 +87,6 @@ export class PaisesService {
|
||||
}),
|
||||
]);
|
||||
return { total, regiones: regiones.length };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { Controller, Get, Param, Query, Header } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiParam, ApiOkResponse } from '@nestjs/swagger';
|
||||
import { UbigeoService } from './ubigeo.service';
|
||||
import { SearchDto } from './dto/ubigeo.dto';
|
||||
|
||||
const CACHE_1DAY = 'public, max-age=86400, stale-while-revalidate=3600';
|
||||
|
||||
@ApiTags('ubigeo')
|
||||
@Controller('ubigeo')
|
||||
export class UbigeoController {
|
||||
@@ -10,6 +12,7 @@ export class UbigeoController {
|
||||
|
||||
// ─── STATS ─────────────────────────────────────────────────────────────────
|
||||
@Get('stats')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Estadísticas generales del ubigeo' })
|
||||
getStats() {
|
||||
return this.svc.getStats();
|
||||
@@ -17,6 +20,7 @@ export class UbigeoController {
|
||||
|
||||
// ─── LOOKUP ─────────────────────────────────────────────────────────────────
|
||||
@Get('lookup/:codigo')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Lookup de cualquier código ubigeo (2, 4 o 6 dígitos)' })
|
||||
@ApiParam({ name: 'codigo', description: 'Código ubigeo: 15 | 1501 | 150101', example: '150101' })
|
||||
lookup(@Param('codigo') codigo: string) {
|
||||
@@ -25,6 +29,7 @@ export class UbigeoController {
|
||||
|
||||
// ─── DEPARTAMENTOS ──────────────────────────────────────────────────────────
|
||||
@Get('departamentos')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Listar todos los departamentos del Perú' })
|
||||
getDepartamentos(@Query() query: SearchDto) {
|
||||
if (query.q) return this.svc.searchDepartamentos(query);
|
||||
@@ -32,6 +37,7 @@ export class UbigeoController {
|
||||
}
|
||||
|
||||
@Get('departamentos/:codigo')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Obtener departamento con sus provincias' })
|
||||
@ApiParam({ name: 'codigo', description: 'Código de 2 dígitos', example: '15' })
|
||||
getDepartamento(@Param('codigo') codigo: string) {
|
||||
@@ -40,6 +46,7 @@ export class UbigeoController {
|
||||
|
||||
// ─── PROVINCIAS ─────────────────────────────────────────────────────────────
|
||||
@Get('provincias')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Listar provincias (filtrar por departamento con ?dep=15)' })
|
||||
getProvincias(@Query() query: SearchDto, @Query('dep') dep?: string) {
|
||||
if (query.q) return this.svc.searchProvincias(query);
|
||||
@@ -47,6 +54,7 @@ export class UbigeoController {
|
||||
}
|
||||
|
||||
@Get('provincias/:codigo')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Obtener provincia con sus distritos' })
|
||||
@ApiParam({ name: 'codigo', description: 'Código de 4 dígitos', example: '1501' })
|
||||
getProvincia(@Param('codigo') codigo: string) {
|
||||
@@ -55,6 +63,7 @@ export class UbigeoController {
|
||||
|
||||
// ─── DISTRITOS ──────────────────────────────────────────────────────────────
|
||||
@Get('distritos')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Buscar distritos (requiere ?q= o ?prov=)' })
|
||||
getDistritos(@Query() query: SearchDto, @Query('prov') prov?: string) {
|
||||
if (query.q) return this.svc.searchDistritos(query);
|
||||
@@ -62,6 +71,7 @@ export class UbigeoController {
|
||||
}
|
||||
|
||||
@Get('distritos/:codigo')
|
||||
@Header('Cache-Control', CACHE_1DAY)
|
||||
@ApiOperation({ summary: 'Obtener distrito por código de 6 dígitos' })
|
||||
@ApiParam({ name: 'codigo', description: 'Código de 6 dígitos', example: '150101' })
|
||||
getDistrito(@Param('codigo') codigo: string) {
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import type { Cache } from 'cache-manager';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { SearchDto } from './dto/ubigeo.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UbigeoService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(CACHE_MANAGER) private cache: Cache,
|
||||
) {}
|
||||
|
||||
private async cached<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
||||
const hit = await this.cache.get<T>(key);
|
||||
if (hit !== undefined && hit !== null) return hit;
|
||||
const result = await fn();
|
||||
await this.cache.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── DEPARTAMENTOS ────────────────────────────────────────────────────────────
|
||||
|
||||
async getDepartamentos() {
|
||||
return this.prisma.departamento.findMany({
|
||||
return this.cached('dep:all', () =>
|
||||
this.prisma.departamento.findMany({
|
||||
orderBy: { nombre: 'asc' },
|
||||
select: { codigo: true, nombre: true, latitud: true, longitud: true },
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getDepartamento(codigo: string) {
|
||||
return this.cached(`dep:${codigo}`, async () => {
|
||||
const dep = await this.prisma.departamento.findUnique({
|
||||
where: { codigo },
|
||||
include: {
|
||||
@@ -27,9 +43,12 @@ export class UbigeoService {
|
||||
});
|
||||
if (!dep) throw new NotFoundException(`Departamento '${codigo}' no encontrado`);
|
||||
return dep;
|
||||
});
|
||||
}
|
||||
|
||||
async searchDepartamentos(dto: SearchDto) {
|
||||
const key = `dep:search:${dto.q ?? ''}:${dto.page}:${dto.limit}`;
|
||||
return this.cached(key, () => {
|
||||
const where = dto.q
|
||||
? { nombre: { contains: dto.q, mode: 'insensitive' as const } }
|
||||
: {};
|
||||
@@ -39,11 +58,13 @@ export class UbigeoService {
|
||||
take: dto.limit,
|
||||
skip: (dto.page! - 1) * dto.limit!,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── PROVINCIAS ───────────────────────────────────────────────────────────────
|
||||
|
||||
async getProvincias(codigoDep?: string) {
|
||||
return this.cached(`prov:all:${codigoDep ?? ''}`, () => {
|
||||
const where = codigoDep ? { codigoDep } : {};
|
||||
return this.prisma.provincia.findMany({
|
||||
where,
|
||||
@@ -52,9 +73,11 @@ export class UbigeoService {
|
||||
departamento: { select: { codigo: true, nombre: true } },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getProvincia(codigo: string) {
|
||||
return this.cached(`prov:${codigo}`, async () => {
|
||||
const prov = await this.prisma.provincia.findUnique({
|
||||
where: { codigo },
|
||||
include: {
|
||||
@@ -67,9 +90,12 @@ export class UbigeoService {
|
||||
});
|
||||
if (!prov) throw new NotFoundException(`Provincia '${codigo}' no encontrada`);
|
||||
return prov;
|
||||
});
|
||||
}
|
||||
|
||||
async searchProvincias(dto: SearchDto) {
|
||||
const key = `prov:search:${dto.q ?? ''}:${dto.page}:${dto.limit}`;
|
||||
return this.cached(key, () => {
|
||||
const where = dto.q
|
||||
? { nombre: { contains: dto.q, mode: 'insensitive' as const } }
|
||||
: {};
|
||||
@@ -80,11 +106,13 @@ export class UbigeoService {
|
||||
skip: (dto.page! - 1) * dto.limit!,
|
||||
include: { departamento: { select: { codigo: true, nombre: true } } },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── DISTRITOS ────────────────────────────────────────────────────────────────
|
||||
|
||||
async getDistritos(codigoProv?: string) {
|
||||
return this.cached(`dist:all:${codigoProv ?? ''}`, () => {
|
||||
const where = codigoProv ? { codigoProv } : {};
|
||||
return this.prisma.distrito.findMany({
|
||||
where,
|
||||
@@ -93,15 +121,15 @@ export class UbigeoService {
|
||||
provincia: { select: { codigo: true, nombre: true } },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getDistrito(codigo: string) {
|
||||
return this.cached(`dist:${codigo}`, async () => {
|
||||
const dist = await this.prisma.distrito.findUnique({
|
||||
where: { codigo },
|
||||
include: {
|
||||
provincia: {
|
||||
select: { codigo: true, nombre: true },
|
||||
},
|
||||
provincia: { select: { codigo: true, nombre: true } },
|
||||
},
|
||||
});
|
||||
if (!dist) throw new NotFoundException(`Distrito '${codigo}' no encontrado`);
|
||||
@@ -112,9 +140,12 @@ export class UbigeoService {
|
||||
});
|
||||
|
||||
return { ...dist, departamento: dep };
|
||||
});
|
||||
}
|
||||
|
||||
async searchDistritos(dto: SearchDto) {
|
||||
const key = `dist:search:${dto.q ?? ''}:${dto.page}:${dto.limit}`;
|
||||
return this.cached(key, async () => {
|
||||
const where = dto.q
|
||||
? { nombre: { contains: dto.q, mode: 'insensitive' as const } }
|
||||
: {};
|
||||
@@ -131,7 +162,6 @@ export class UbigeoService {
|
||||
this.prisma.distrito.count({ where }),
|
||||
]);
|
||||
|
||||
// Enriquecer con departamento
|
||||
const depCodigos = [...new Set(data.map((d) => d.codigoDep))];
|
||||
const deps = await this.prisma.departamento.findMany({
|
||||
where: { codigo: { in: depCodigos } },
|
||||
@@ -143,11 +173,13 @@ export class UbigeoService {
|
||||
data: data.map((d) => ({ ...d, departamento: depMap[d.codigoDep] })),
|
||||
meta: { total, page: dto.page, limit: dto.limit, totalPages: Math.ceil(total / dto.limit!) },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ─── LOOKUP POR CÓDIGO ────────────────────────────────────────────────────────
|
||||
|
||||
async lookup(codigo: string) {
|
||||
return this.cached(`lookup:${codigo}`, async () => {
|
||||
const len = codigo.length;
|
||||
|
||||
if (len === 2) {
|
||||
@@ -188,16 +220,19 @@ export class UbigeoService {
|
||||
}
|
||||
|
||||
throw new NotFoundException(`Código '${codigo}' inválido (debe tener 2, 4 o 6 dígitos)`);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── STATS ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async getStats() {
|
||||
return this.cached('stats:ubigeo', async () => {
|
||||
const [departamentos, provincias, distritos] = await Promise.all([
|
||||
this.prisma.departamento.count(),
|
||||
this.prisma.provincia.count(),
|
||||
this.prisma.distrito.count(),
|
||||
]);
|
||||
return { departamentos, provincias, distritos, fuente: 'INEI 2025' };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user