feat: add Redis cache, gzip, CI/CD via Gitea self-hosted runner
This commit is contained in:
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,70 +1,92 @@
|
||||
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 where: any = { activo: true };
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nombre: { contains: search, mode: 'insensitive' } },
|
||||
{ nombreEn: { contains: search, mode: 'insensitive' } },
|
||||
{ codigo: { equals: search.toUpperCase() } },
|
||||
{ codigoAlpha2: { equals: search.toUpperCase() } },
|
||||
];
|
||||
}
|
||||
if (region) {
|
||||
where.region = { contains: region, mode: 'insensitive' };
|
||||
}
|
||||
return this.prisma.pais.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
select: {
|
||||
codigo: true,
|
||||
codigoAlpha2: true,
|
||||
nombre: true,
|
||||
nombreEn: true,
|
||||
capital: true,
|
||||
region: true,
|
||||
subregion: true,
|
||||
emoji: true,
|
||||
latitud: true,
|
||||
longitud: true,
|
||||
},
|
||||
const key = `paises:list:${search ?? ''}:${region ?? ''}`;
|
||||
return this.cached(key, () => {
|
||||
const where: any = { activo: true };
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nombre: { contains: search, mode: 'insensitive' } },
|
||||
{ nombreEn: { contains: search, mode: 'insensitive' } },
|
||||
{ codigo: { equals: search.toUpperCase() } },
|
||||
{ codigoAlpha2: { equals: search.toUpperCase() } },
|
||||
];
|
||||
}
|
||||
if (region) {
|
||||
where.region = { contains: region, mode: 'insensitive' };
|
||||
}
|
||||
return this.prisma.pais.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
select: {
|
||||
codigo: true,
|
||||
codigoAlpha2: true,
|
||||
nombre: true,
|
||||
nombreEn: true,
|
||||
capital: true,
|
||||
region: true,
|
||||
subregion: true,
|
||||
emoji: true,
|
||||
latitud: true,
|
||||
longitud: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getPais(codigo: string) {
|
||||
const where = codigo.length === 2
|
||||
? { codigoAlpha2: codigo.toUpperCase() }
|
||||
: { codigo: codigo.toUpperCase() };
|
||||
return this.cached(`pais:${codigo.toUpperCase()}`, async () => {
|
||||
const where = codigo.length === 2
|
||||
? { codigoAlpha2: codigo.toUpperCase() }
|
||||
: { codigo: codigo.toUpperCase() };
|
||||
|
||||
const pais = await this.prisma.pais.findFirst({ where });
|
||||
if (!pais) throw new NotFoundException(`País '${codigo}' no encontrado`);
|
||||
return pais;
|
||||
const pais = await this.prisma.pais.findFirst({ where });
|
||||
if (!pais) throw new NotFoundException(`País '${codigo}' no encontrado`);
|
||||
return pais;
|
||||
});
|
||||
}
|
||||
|
||||
async getRegiones() {
|
||||
const result = await this.prisma.pais.findMany({
|
||||
where: { activo: true },
|
||||
distinct: ['region'],
|
||||
select: { region: true },
|
||||
orderBy: { region: 'asc' },
|
||||
});
|
||||
return result.map((r) => r.region);
|
||||
}
|
||||
|
||||
async getStats() {
|
||||
const [total, regiones] = await Promise.all([
|
||||
this.prisma.pais.count({ where: { activo: true } }),
|
||||
this.prisma.pais.findMany({
|
||||
return this.cached('paises:regiones', async () => {
|
||||
const result = await this.prisma.pais.findMany({
|
||||
where: { activo: true },
|
||||
distinct: ['region'],
|
||||
select: { region: true },
|
||||
}),
|
||||
]);
|
||||
return { total, regiones: regiones.length };
|
||||
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({
|
||||
where: { activo: true },
|
||||
distinct: ['region'],
|
||||
select: { region: true },
|
||||
}),
|
||||
]);
|
||||
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,203 +1,238 @@
|
||||
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({
|
||||
orderBy: { nombre: 'asc' },
|
||||
select: { codigo: true, nombre: true, latitud: true, longitud: true },
|
||||
});
|
||||
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) {
|
||||
const dep = await this.prisma.departamento.findUnique({
|
||||
where: { codigo },
|
||||
include: {
|
||||
provincias: {
|
||||
orderBy: { nombre: 'asc' },
|
||||
select: { codigo: true, nombre: true, latitud: true, longitud: true },
|
||||
return this.cached(`dep:${codigo}`, async () => {
|
||||
const dep = await this.prisma.departamento.findUnique({
|
||||
where: { codigo },
|
||||
include: {
|
||||
provincias: {
|
||||
orderBy: { nombre: 'asc' },
|
||||
select: { codigo: true, nombre: true, latitud: true, longitud: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!dep) throw new NotFoundException(`Departamento '${codigo}' no encontrado`);
|
||||
return dep;
|
||||
});
|
||||
if (!dep) throw new NotFoundException(`Departamento '${codigo}' no encontrado`);
|
||||
return dep;
|
||||
}
|
||||
|
||||
async searchDepartamentos(dto: SearchDto) {
|
||||
const where = dto.q
|
||||
? { nombre: { contains: dto.q, mode: 'insensitive' as const } }
|
||||
: {};
|
||||
return this.prisma.departamento.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
take: dto.limit,
|
||||
skip: (dto.page! - 1) * dto.limit!,
|
||||
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 } }
|
||||
: {};
|
||||
return this.prisma.departamento.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
take: dto.limit,
|
||||
skip: (dto.page! - 1) * dto.limit!,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── PROVINCIAS ───────────────────────────────────────────────────────────────
|
||||
|
||||
async getProvincias(codigoDep?: string) {
|
||||
const where = codigoDep ? { codigoDep } : {};
|
||||
return this.prisma.provincia.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
include: {
|
||||
departamento: { select: { codigo: true, nombre: true } },
|
||||
},
|
||||
return this.cached(`prov:all:${codigoDep ?? ''}`, () => {
|
||||
const where = codigoDep ? { codigoDep } : {};
|
||||
return this.prisma.provincia.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
include: {
|
||||
departamento: { select: { codigo: true, nombre: true } },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getProvincia(codigo: string) {
|
||||
const prov = await this.prisma.provincia.findUnique({
|
||||
where: { codigo },
|
||||
include: {
|
||||
departamento: { select: { codigo: true, nombre: true } },
|
||||
distritos: {
|
||||
orderBy: { nombre: 'asc' },
|
||||
select: { codigo: true, nombre: true, capital: true, categoria: true, poblacion: true, latitud: true, longitud: true },
|
||||
return this.cached(`prov:${codigo}`, async () => {
|
||||
const prov = await this.prisma.provincia.findUnique({
|
||||
where: { codigo },
|
||||
include: {
|
||||
departamento: { select: { codigo: true, nombre: true } },
|
||||
distritos: {
|
||||
orderBy: { nombre: 'asc' },
|
||||
select: { codigo: true, nombre: true, capital: true, categoria: true, poblacion: true, latitud: true, longitud: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!prov) throw new NotFoundException(`Provincia '${codigo}' no encontrada`);
|
||||
return prov;
|
||||
});
|
||||
if (!prov) throw new NotFoundException(`Provincia '${codigo}' no encontrada`);
|
||||
return prov;
|
||||
}
|
||||
|
||||
async searchProvincias(dto: SearchDto) {
|
||||
const where = dto.q
|
||||
? { nombre: { contains: dto.q, mode: 'insensitive' as const } }
|
||||
: {};
|
||||
return this.prisma.provincia.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
take: dto.limit,
|
||||
skip: (dto.page! - 1) * dto.limit!,
|
||||
include: { departamento: { select: { codigo: true, nombre: true } } },
|
||||
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 } }
|
||||
: {};
|
||||
return this.prisma.provincia.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
take: dto.limit,
|
||||
skip: (dto.page! - 1) * dto.limit!,
|
||||
include: { departamento: { select: { codigo: true, nombre: true } } },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── DISTRITOS ────────────────────────────────────────────────────────────────
|
||||
|
||||
async getDistritos(codigoProv?: string) {
|
||||
const where = codigoProv ? { codigoProv } : {};
|
||||
return this.prisma.distrito.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
include: {
|
||||
provincia: { select: { codigo: true, nombre: true } },
|
||||
},
|
||||
return this.cached(`dist:all:${codigoProv ?? ''}`, () => {
|
||||
const where = codigoProv ? { codigoProv } : {};
|
||||
return this.prisma.distrito.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
include: {
|
||||
provincia: { select: { codigo: true, nombre: true } },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getDistrito(codigo: string) {
|
||||
const dist = await this.prisma.distrito.findUnique({
|
||||
where: { codigo },
|
||||
include: {
|
||||
provincia: {
|
||||
select: { codigo: true, nombre: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!dist) throw new NotFoundException(`Distrito '${codigo}' no encontrado`);
|
||||
|
||||
const dep = await this.prisma.departamento.findUnique({
|
||||
where: { codigo: dist.codigoDep },
|
||||
select: { codigo: true, nombre: true },
|
||||
});
|
||||
|
||||
return { ...dist, departamento: dep };
|
||||
}
|
||||
|
||||
async searchDistritos(dto: SearchDto) {
|
||||
const where = dto.q
|
||||
? { nombre: { contains: dto.q, mode: 'insensitive' as const } }
|
||||
: {};
|
||||
const [data, total] = await Promise.all([
|
||||
this.prisma.distrito.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
take: dto.limit,
|
||||
skip: (dto.page! - 1) * dto.limit!,
|
||||
return this.cached(`dist:${codigo}`, async () => {
|
||||
const dist = await this.prisma.distrito.findUnique({
|
||||
where: { codigo },
|
||||
include: {
|
||||
provincia: { select: { codigo: true, nombre: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.distrito.count({ where }),
|
||||
]);
|
||||
});
|
||||
if (!dist) throw new NotFoundException(`Distrito '${codigo}' no encontrado`);
|
||||
|
||||
// Enriquecer con departamento
|
||||
const depCodigos = [...new Set(data.map((d) => d.codigoDep))];
|
||||
const deps = await this.prisma.departamento.findMany({
|
||||
where: { codigo: { in: depCodigos } },
|
||||
select: { codigo: true, nombre: true },
|
||||
const dep = await this.prisma.departamento.findUnique({
|
||||
where: { codigo: dist.codigoDep },
|
||||
select: { codigo: true, nombre: true },
|
||||
});
|
||||
|
||||
return { ...dist, departamento: dep };
|
||||
});
|
||||
const depMap = Object.fromEntries(deps.map((d) => [d.codigo, d]));
|
||||
}
|
||||
|
||||
return {
|
||||
data: data.map((d) => ({ ...d, departamento: depMap[d.codigoDep] })),
|
||||
meta: { total, page: dto.page, limit: dto.limit, totalPages: Math.ceil(total / dto.limit!) },
|
||||
};
|
||||
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 } }
|
||||
: {};
|
||||
const [data, total] = await Promise.all([
|
||||
this.prisma.distrito.findMany({
|
||||
where,
|
||||
orderBy: { nombre: 'asc' },
|
||||
take: dto.limit,
|
||||
skip: (dto.page! - 1) * dto.limit!,
|
||||
include: {
|
||||
provincia: { select: { codigo: true, nombre: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.distrito.count({ where }),
|
||||
]);
|
||||
|
||||
const depCodigos = [...new Set(data.map((d) => d.codigoDep))];
|
||||
const deps = await this.prisma.departamento.findMany({
|
||||
where: { codigo: { in: depCodigos } },
|
||||
select: { codigo: true, nombre: true },
|
||||
});
|
||||
const depMap = Object.fromEntries(deps.map((d) => [d.codigo, d]));
|
||||
|
||||
return {
|
||||
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) {
|
||||
const len = codigo.length;
|
||||
return this.cached(`lookup:${codigo}`, async () => {
|
||||
const len = codigo.length;
|
||||
|
||||
if (len === 2) {
|
||||
const dep = await this.prisma.departamento.findUnique({ where: { codigo } });
|
||||
if (!dep) throw new NotFoundException(`Código '${codigo}' no encontrado`);
|
||||
return { tipo: 'departamento', codigo, nombre: dep.nombre };
|
||||
}
|
||||
if (len === 2) {
|
||||
const dep = await this.prisma.departamento.findUnique({ where: { codigo } });
|
||||
if (!dep) throw new NotFoundException(`Código '${codigo}' no encontrado`);
|
||||
return { tipo: 'departamento', codigo, nombre: dep.nombre };
|
||||
}
|
||||
|
||||
if (len === 4) {
|
||||
const prov = await this.prisma.provincia.findUnique({
|
||||
where: { codigo },
|
||||
include: { departamento: { select: { codigo: true, nombre: true } } },
|
||||
});
|
||||
if (!prov) throw new NotFoundException(`Código '${codigo}' no encontrado`);
|
||||
return { tipo: 'provincia', codigo, departamento: prov.departamento.nombre, provincia: prov.nombre };
|
||||
}
|
||||
if (len === 4) {
|
||||
const prov = await this.prisma.provincia.findUnique({
|
||||
where: { codigo },
|
||||
include: { departamento: { select: { codigo: true, nombre: true } } },
|
||||
});
|
||||
if (!prov) throw new NotFoundException(`Código '${codigo}' no encontrado`);
|
||||
return { tipo: 'provincia', codigo, departamento: prov.departamento.nombre, provincia: prov.nombre };
|
||||
}
|
||||
|
||||
if (len === 6) {
|
||||
const dist = await this.prisma.distrito.findUnique({
|
||||
where: { codigo },
|
||||
include: { provincia: { select: { codigo: true, nombre: true } } },
|
||||
});
|
||||
if (!dist) throw new NotFoundException(`Código '${codigo}' no encontrado`);
|
||||
const dep = await this.prisma.departamento.findUnique({
|
||||
where: { codigo: dist.codigoDep },
|
||||
select: { nombre: true },
|
||||
});
|
||||
return {
|
||||
tipo: 'distrito',
|
||||
codigo,
|
||||
departamento: dep?.nombre,
|
||||
provincia: dist.provincia.nombre,
|
||||
distrito: dist.nombre,
|
||||
capital: dist.capital,
|
||||
latitud: dist.latitud,
|
||||
longitud: dist.longitud,
|
||||
};
|
||||
}
|
||||
if (len === 6) {
|
||||
const dist = await this.prisma.distrito.findUnique({
|
||||
where: { codigo },
|
||||
include: { provincia: { select: { codigo: true, nombre: true } } },
|
||||
});
|
||||
if (!dist) throw new NotFoundException(`Código '${codigo}' no encontrado`);
|
||||
const dep = await this.prisma.departamento.findUnique({
|
||||
where: { codigo: dist.codigoDep },
|
||||
select: { nombre: true },
|
||||
});
|
||||
return {
|
||||
tipo: 'distrito',
|
||||
codigo,
|
||||
departamento: dep?.nombre,
|
||||
provincia: dist.provincia.nombre,
|
||||
distrito: dist.nombre,
|
||||
capital: dist.capital,
|
||||
latitud: dist.latitud,
|
||||
longitud: dist.longitud,
|
||||
};
|
||||
}
|
||||
|
||||
throw new NotFoundException(`Código '${codigo}' inválido (debe tener 2, 4 o 6 dígitos)`);
|
||||
throw new NotFoundException(`Código '${codigo}' inválido (debe tener 2, 4 o 6 dígitos)`);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── STATS ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async getStats() {
|
||||
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' };
|
||||
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