primera instancia de validador y carga del excel de usuarios, falta agregar las relaciones con las carreras y verificar los validadores
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
API_PORT=3051
|
||||
|
||||
|
||||
DB_HOST=database
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASS=root
|
||||
DB_NAME=carga_test
|
||||
DB_SYNC=true
|
||||
DB_LOGGING=true
|
||||
Generated
+1385
-87
File diff suppressed because it is too large
Load Diff
+14
-3
@@ -21,10 +21,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.1.3",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"dotenv": "^16.5.0",
|
||||
"exceljs": "^4.4.0",
|
||||
"multer": "^2.0.1",
|
||||
"mysql2": "^3.14.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
@@ -36,7 +46,8 @@
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/multer": "^1.4.13",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
|
||||
+44
-2
@@ -1,9 +1,51 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Logger, Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import * as entities from './entities/entities';
|
||||
import { ExcelModule } from './excel/excel.module';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (config: ConfigService) => {
|
||||
const dbConfig = {
|
||||
type: 'mysql' as const,
|
||||
host: config.get('DB_HOST', 'localhost'),
|
||||
port: config.get<number>('DB_PORT', 3306),
|
||||
username: config.get('DB_USER', 'root'),
|
||||
// no prints en claro en prod:
|
||||
password: config.get('DB_PASS', ''),
|
||||
database: config.get('DB_NAME', 'test'),
|
||||
};
|
||||
Logger.log('[DB CONFIG] ' + JSON.stringify({
|
||||
...dbConfig,
|
||||
password: '***'
|
||||
}), 'TypeORM');
|
||||
return {
|
||||
...dbConfig,
|
||||
entities: Object.values(entities),
|
||||
synchronize: config.get<boolean>('DB_SYNC', true),
|
||||
logging: ['error', 'warn', 'info', 'query', 'schema'],
|
||||
logger: 'advanced-console',
|
||||
retryAttempts: 5,
|
||||
retryDelay: 2000,
|
||||
};
|
||||
},
|
||||
}),
|
||||
TypeOrmModule.forFeature(Object.values(entities)),
|
||||
|
||||
|
||||
ExcelModule,
|
||||
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'origen' })
|
||||
export class Origen {
|
||||
@PrimaryGeneratedColumn({ name: 'id_origen', type: 'int' })
|
||||
id_origen: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 30 })
|
||||
fuente: string;
|
||||
|
||||
@OneToMany(() => Movimiento, movimiento => movimiento.origen)
|
||||
movimientos: Movimiento[];
|
||||
|
||||
@OneToMany(() => UsuariosDelSistema, uds => uds.origen)
|
||||
usuariosDelSistema: UsuariosDelSistema[];
|
||||
}
|
||||
|
||||
@Entity({ name: 'genero' })
|
||||
export class Genero {
|
||||
@PrimaryGeneratedColumn({ name: 'id_genero', type: 'int', })
|
||||
id_genero: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, nullable: true })
|
||||
genero: string;
|
||||
|
||||
@OneToMany(() => Usuario, usuario => usuario.genero)
|
||||
usuarios: Usuario[];
|
||||
}
|
||||
|
||||
@Entity({ name: 'carrera' })
|
||||
export class Carrera {
|
||||
@PrimaryGeneratedColumn({ name: 'id_carrera', type: 'int' })
|
||||
id_carrera: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
carrera: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 6 })
|
||||
clave: string;
|
||||
|
||||
@OneToMany(() => CarreraUsuario, cu => cu.carrera)
|
||||
carreraUsuarios: CarreraUsuario[];
|
||||
}
|
||||
|
||||
@Entity({ name: 'usuario' })
|
||||
export class Usuario {
|
||||
@PrimaryGeneratedColumn({ name: 'id_usuario', type: 'int' })
|
||||
id_usuario: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 9 })
|
||||
num_cuenta: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 60 })
|
||||
nombre: string;
|
||||
|
||||
@Column({ name: 'a_paterno', type: 'varchar', length: 60 })
|
||||
a_paterno: string;
|
||||
|
||||
@Column({ name: 'a_materno', type: 'varchar', length: 60 })
|
||||
a_materno: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 15 })
|
||||
rfc: string;
|
||||
|
||||
@Column({ name: 'fecha_nacimiento', type: 'varchar', length: 8 })
|
||||
fecha_nacimiento: string;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
generacion: number;
|
||||
|
||||
@ManyToOne(() => Genero, genero => genero.usuarios)
|
||||
@JoinColumn({ name: 'id_genero' })
|
||||
genero: Genero;
|
||||
|
||||
@OneToMany(() => Movimiento, mov => mov.usuario)
|
||||
movimientos: Movimiento[];
|
||||
|
||||
@OneToMany(() => CarreraUsuario, cu => cu.usuario)
|
||||
carreraUsuarios: CarreraUsuario[];
|
||||
|
||||
@OneToMany(() => UsuarioTipoUsuario, utu => utu.usuario)
|
||||
usuarioTipos: UsuarioTipoUsuario[];
|
||||
}
|
||||
|
||||
@Entity({ name: 'usuariosDelSistema' })
|
||||
export class UsuariosDelSistema {
|
||||
@PrimaryColumn({ type: 'varchar', length: 60 })
|
||||
usuario: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 60 })
|
||||
contraseña: string;
|
||||
|
||||
@ManyToOne(() => Origen, origen => origen.usuariosDelSistema)
|
||||
@JoinColumn({ name: 'id_origen' })
|
||||
origen: Origen;
|
||||
}
|
||||
|
||||
@Entity({ name: 'carrera_usuario' })
|
||||
export class CarreraUsuario {
|
||||
@PrimaryGeneratedColumn({ name: 'id_carrera_usuario', type: 'int' })
|
||||
id_carrera_usuario: number;
|
||||
|
||||
@ManyToOne(() => Carrera, carrera => carrera.carreraUsuarios)
|
||||
@JoinColumn({ name: 'id_carrera' })
|
||||
carrera: Carrera;
|
||||
|
||||
@ManyToOne(() => Usuario, usuario => usuario.carreraUsuarios)
|
||||
@JoinColumn({ name: 'id_usuario' })
|
||||
usuario: Usuario;
|
||||
}
|
||||
|
||||
@Entity({ name: 'equipo' })
|
||||
export class Equipo {
|
||||
@PrimaryGeneratedColumn({ name: 'id_equipo', type: 'int' })
|
||||
id_equipo: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10 })
|
||||
equipo: string;
|
||||
|
||||
@Column({ name: 'numero_serie', type: 'varchar', length: 30 })
|
||||
numero_serie: string;
|
||||
}
|
||||
|
||||
@Entity({ name: 'tipo_usuario' })
|
||||
export class TipoUsuario {
|
||||
@PrimaryGeneratedColumn({ name: 'id_tipo_usuario', type: 'int' })
|
||||
id_tipo_usuario: number;
|
||||
|
||||
@Column({ name: 'tipo_usuario', type: 'varchar', length: 20 })
|
||||
tipo_usuario: string;
|
||||
|
||||
@OneToMany(() => UsuarioTipoUsuario, utu => utu.tipoUsuario)
|
||||
usuariosTipos: UsuarioTipoUsuario[];
|
||||
}
|
||||
|
||||
@Entity({ name: 'usuario_tipo_usuario' })
|
||||
export class UsuarioTipoUsuario {
|
||||
@PrimaryGeneratedColumn({ name: 'id_user_tipo_usuario', type: 'int' })
|
||||
id_user_tipo_usuario: number;
|
||||
|
||||
@ManyToOne(() => TipoUsuario, tipo => tipo.usuariosTipos)
|
||||
@JoinColumn({ name: 'id_tipo_usuario' })
|
||||
tipoUsuario: TipoUsuario;
|
||||
|
||||
@ManyToOne(() => Usuario, usuario => usuario.usuarioTipos)
|
||||
@JoinColumn({ name: 'id_usuario' })
|
||||
usuario: Usuario;
|
||||
}
|
||||
|
||||
@Entity({ name: 'movimiento' })
|
||||
export class Movimiento {
|
||||
@PrimaryGeneratedColumn({ name: 'id_mov', type: 'int' })
|
||||
id_mov: number;
|
||||
|
||||
@Column({ name: 'fecha_mov', type: 'datetime' })
|
||||
fecha_mov: Date;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
status: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 200 })
|
||||
observaciones: string;
|
||||
|
||||
@Column({ type: 'bit' })
|
||||
flag: boolean;
|
||||
|
||||
@Column({ type: 'varchar', length: 200 })
|
||||
reporte: string;
|
||||
|
||||
@ManyToOne(() => Usuario, usuario => usuario.movimientos)
|
||||
@JoinColumn({ name: 'id_usuario' })
|
||||
usuario: Usuario;
|
||||
|
||||
@ManyToOne(() => Origen, origen => origen.movimientos)
|
||||
@JoinColumn({ name: 'id_origen' })
|
||||
origen: Origen;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// src/excel/excel.controller.ts
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ExcelService } from './excel.service';
|
||||
import { ExcelDocumentation } from './excel.documentation';
|
||||
|
||||
@Controller('excel')
|
||||
export class ExcelController {
|
||||
constructor(private readonly excelService: ExcelService) {}
|
||||
|
||||
@Post('verify')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ExcelDocumentation.verifyExcel()
|
||||
async verifyExcel(@UploadedFile() file: Express.Multer.File) {
|
||||
if (!file) throw new BadRequestException('Se requiere un archivo .xlsx');
|
||||
const errors = await this.excelService.validateFile(file.buffer);
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
@Post('load')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ExcelDocumentation.loadExcel()
|
||||
async loadExcel(@UploadedFile() file: Express.Multer.File) {
|
||||
if (!file) throw new BadRequestException('Se requiere un archivo .xlsx');
|
||||
const result = await this.excelService.loadFile(file.buffer);
|
||||
return result; // { inserted: X }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiConsumes, ApiBody, ApiResponse } from '@nestjs/swagger';
|
||||
|
||||
export class ExcelDocumentation {
|
||||
/**
|
||||
* Decorators Swagger para el endpoint POST /excel/verify
|
||||
*/
|
||||
static verifyExcel() {
|
||||
return applyDecorators(
|
||||
ApiTags('Excel'),
|
||||
ApiOperation({
|
||||
summary: 'Verificar archivo Excel',
|
||||
description: 'Valida duplicados, filas en blanco y formato básico de los datos sin realizar carga en la base.'
|
||||
}),
|
||||
ApiConsumes('multipart/form-data'),
|
||||
ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
description: 'Archivo Excel (.xlsx) con usuarios a validar'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Resultado de la validación',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
valid: { type: 'boolean', example: false },
|
||||
errors: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: ['Fila 2: cuenta duplicada "42515101".', 'Fila 3: fecha de nacimiento inválida "1988051".']
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 400, description: 'No se recibió archivo o formato inválido.' })
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorators Swagger para el endpoint POST /excel/load
|
||||
*/
|
||||
static loadExcel() {
|
||||
return applyDecorators(
|
||||
ApiTags('Excel'),
|
||||
ApiOperation({
|
||||
summary: 'Cargar archivo Excel',
|
||||
description: 'Valida y persiste los datos en la base de datos. Retorna el número de registros insertados.'
|
||||
}),
|
||||
ApiConsumes('multipart/form-data'),
|
||||
ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
description: 'Archivo Excel (.xlsx) con usuarios a cargar'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 201,
|
||||
description: 'Usuarios insertados correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
inserted: { type: 'number', example: 3 }
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 400,
|
||||
description: 'Errores de validación o carga',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
errors: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: ['Fila 5: rfc contiene caracteres inválidos.']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ExcelService } from './excel.service';
|
||||
import { ExcelController } from './excel.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Usuario, Genero, Carrera } from '../entities/entities';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Usuario, Genero, Carrera]),
|
||||
],
|
||||
providers: [ExcelService],
|
||||
controllers: [ExcelController],
|
||||
})
|
||||
export class ExcelModule {}
|
||||
@@ -0,0 +1,207 @@
|
||||
// src/excel/excel.service.ts
|
||||
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||
import { Workbook } from 'exceljs';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Usuario } from '../entities/entities';
|
||||
import { Genero } from '../entities/entities';
|
||||
import { Carrera } from '../entities/entities';
|
||||
|
||||
export interface UsuarioRow {
|
||||
cuenta: string;
|
||||
nombreCompleto: string;
|
||||
clave: string;
|
||||
nomCarr: string;
|
||||
gen: string; // generación
|
||||
fechnac: string; // YYYYMMDD
|
||||
apellidopa: string;
|
||||
apellidoma: string;
|
||||
nombres: string;
|
||||
sexo: string; // m/f o texto
|
||||
tipo: string;
|
||||
correo: string;
|
||||
rfc: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ExcelService {
|
||||
private readonly logger = new Logger(ExcelService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Usuario)
|
||||
private readonly usuarioRepo: Repository<Usuario>,
|
||||
@InjectRepository(Genero)
|
||||
private readonly generoRepo: Repository<Genero>,
|
||||
@InjectRepository(Carrera)
|
||||
private readonly carreraRepo: Repository<Carrera>,
|
||||
// … inyecta otros repositorios si los usarás
|
||||
) {}
|
||||
|
||||
/** Lee el buffer del Excel y devuelve un arreglo de filas tipadas */
|
||||
private async parseFile(buffer: Buffer): Promise<UsuarioRow[]> {
|
||||
const wb = new Workbook();
|
||||
await wb.xlsx.load(buffer);
|
||||
const sheet = wb.worksheets[0];
|
||||
const rows: UsuarioRow[] = [];
|
||||
|
||||
// Asume que la primera fila es encabezados
|
||||
sheet.eachRow((row, idx) => {
|
||||
if (idx === 1) return; // salto encabezados
|
||||
|
||||
// 1) Asegurarnos de que row.values no sea null/undefined
|
||||
if (!row.values) {
|
||||
this.logger.warn(`Fila ${idx + 1}: row.values vacío, se omite.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) row.values[0] es null, así que slice(1) sí existe
|
||||
const [
|
||||
cuenta,
|
||||
nombreCompleto,
|
||||
clave,
|
||||
nomCarr,
|
||||
gen,
|
||||
fechnac,
|
||||
apellidopa,
|
||||
apellidoma,
|
||||
nombres,
|
||||
sexo,
|
||||
tipo,
|
||||
correo,
|
||||
rfc,
|
||||
] = (row.values as any[]).slice(1);
|
||||
|
||||
rows.push({
|
||||
cuenta,
|
||||
nombreCompleto,
|
||||
clave,
|
||||
nomCarr,
|
||||
gen,
|
||||
fechnac,
|
||||
apellidopa,
|
||||
apellidoma,
|
||||
nombres,
|
||||
sexo,
|
||||
tipo,
|
||||
correo,
|
||||
rfc,
|
||||
});
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Valida duplicados, celdas vacías y patrones básicos */
|
||||
validateRows(rows: UsuarioRow[]) {
|
||||
const errors: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
rows.forEach((r, i) => {
|
||||
const rowNum = i + 2; // por el encabezado
|
||||
|
||||
// Campos que no pueden quedar vacíos
|
||||
['cuenta','nombreCompleto','clave','fechnac'].forEach(field => {
|
||||
const raw = (r as any)[field];
|
||||
const str = raw != null ? raw.toString().trim() : '';
|
||||
if (!str) {
|
||||
errors.push(`Fila ${rowNum}: campo "${field}" está vacío.`);
|
||||
}
|
||||
});
|
||||
|
||||
// Duplicados de "cuenta"
|
||||
const cuentaStr = r.cuenta != null ? r.cuenta.toString().trim() : '';
|
||||
if (seen.has(cuentaStr)) {
|
||||
errors.push(`Fila ${rowNum}: cuenta duplicada "${cuentaStr}".`);
|
||||
} else {
|
||||
seen.add(cuentaStr);
|
||||
}
|
||||
|
||||
// Numérico en cuenta y generación
|
||||
if (!/^[0-9]+$/.test(cuentaStr)) {
|
||||
errors.push(`Fila ${rowNum}: cuenta debe ser numérica.`);
|
||||
}
|
||||
const genStr = r.gen != null ? r.gen.toString().trim() : '';
|
||||
if (!/^[0-9]{4}$/.test(genStr)) {
|
||||
errors.push(`Fila ${rowNum}: generación inválida "${genStr}".`);
|
||||
}
|
||||
|
||||
// Fecha en formato YYYYMMDD
|
||||
const fechaStr = r.fechnac != null ? r.fechnac.toString().trim() : '';
|
||||
if (!/^[0-9]{8}$/.test(fechaStr)) {
|
||||
errors.push(`Fila ${rowNum}: fecha de nacimiento inválida "${fechaStr}".`);
|
||||
}
|
||||
|
||||
// RFC alfanumérico
|
||||
const rfcStr = r.rfc != null ? r.rfc.toString().trim() : '';
|
||||
if (rfcStr && !/^[A-Z0-9]+$/.test(rfcStr)) {
|
||||
errors.push(`Fila ${rowNum}: RFC contiene caracteres no válidos.`);
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Solo valida: retorna lista de errores (vacío = ok)
|
||||
*/
|
||||
async validateFile(buffer: Buffer) {
|
||||
const rows = await this.parseFile(buffer);
|
||||
const errs = this.validateRows(rows);
|
||||
return errs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida y luego persiste usuarios y relaciones básicas
|
||||
*/
|
||||
async loadFile(buffer: Buffer) {
|
||||
const rows = await this.parseFile(buffer);
|
||||
const errs = this.validateRows(rows);
|
||||
if (errs.length) {
|
||||
throw new BadRequestException({ errors: errs });
|
||||
}
|
||||
|
||||
for (const r of rows) {
|
||||
// 1) Género (crea o reutiliza)
|
||||
let genero = await this.generoRepo.findOne({ where: { genero: r.sexo } });
|
||||
if (!genero) {
|
||||
genero = this.generoRepo.create({ genero: r.sexo });
|
||||
await this.generoRepo.save(genero);
|
||||
}
|
||||
|
||||
// 2) Carrera (por clave)
|
||||
let carrera = await this.carreraRepo.findOne({
|
||||
where: { clave: r.clave },
|
||||
});
|
||||
if (!carrera) {
|
||||
carrera = this.carreraRepo.create({
|
||||
clave: r.clave,
|
||||
carrera: r.nomCarr.slice(0, 100),
|
||||
});
|
||||
await this.carreraRepo.save(carrera);
|
||||
}
|
||||
|
||||
// 3) Usuario
|
||||
const usuario = this.usuarioRepo.create({
|
||||
num_cuenta: r.cuenta,
|
||||
nombre: r.nombres.trim().split(' ')[0], // ajusta si quieres
|
||||
a_paterno: r.apellidopa,
|
||||
a_materno: r.apellidoma,
|
||||
rfc: r.rfc,
|
||||
fecha_nacimiento: r.fechnac,
|
||||
generacion: parseInt(r.gen, 10),
|
||||
genero,
|
||||
});
|
||||
await this.usuarioRepo.save(usuario);
|
||||
|
||||
// 4) Relacion usuario–carrera (tabla intermedia)
|
||||
await this.carreraRepo
|
||||
.createQueryBuilder()
|
||||
.relation('carreraUsuarios')
|
||||
.of(carrera)
|
||||
.add(usuario);
|
||||
}
|
||||
|
||||
return { inserted: rows.length };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Catch()
|
||||
export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: any, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
|
||||
let status = 500; // Valor por defecto para errores no controlados
|
||||
let message = 'Internal server error';
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
const responseBody = exception.getResponse();
|
||||
|
||||
// Evitar el problema de circular structure usando JSON-safe conversion
|
||||
message =
|
||||
typeof responseBody === 'object'
|
||||
? this.safeStringify(responseBody)
|
||||
: (responseBody as string);
|
||||
} else {
|
||||
// Puedes mapear el error a un código HTTP y mensaje adecuados
|
||||
status = 400;
|
||||
message = (exception as any).sqlMessage || exception.message;
|
||||
|
||||
console.error('Unhandled Exception:', exception); // Log de errores no controlados
|
||||
}
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: ctx.getRequest().url,
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Método para manejar objetos circulares usando WeakSet
|
||||
private safeStringify(obj: any): string {
|
||||
const seen = new WeakSet();
|
||||
return JSON.stringify(obj, (key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) return '[Circular]';
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
}
|
||||
+48
-1
@@ -1,8 +1,55 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { GlobalExceptionFilter } from './helpers/exception.filter';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
app.enableCors();
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
|
||||
app.useGlobalFilters(new GlobalExceptionFilter());
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Documentacion de la api de carga masiva de CEDETEC')
|
||||
.setDescription(
|
||||
'El objetivo es mejorar la gestion interna y la integridad de los datos',
|
||||
)
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
// .addServer(process.env.SWAGGER_SERVER_URL) // descomentar parael servidor de acatlan
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
|
||||
SwaggerModule.setup('documentation', app, document);
|
||||
|
||||
const port = process.env.API_PORT || 4000;
|
||||
await app.listen(port, '0.0.0.0');
|
||||
|
||||
console.log(
|
||||
`[main] Aplicación iniciada en el puerto ${port} a las ${new Date().toISOString()}`,
|
||||
);
|
||||
console.log(
|
||||
`\n\x1b[36m=========================================================\x1b[0m`,
|
||||
);
|
||||
console.log(`\x1b[36m SISTEMA DE CARGA MASIVA - API\x1b[0m`);
|
||||
console.log(
|
||||
`\x1b[36m=========================================================\x1b[0m`,
|
||||
);
|
||||
console.log(`\x1b[32m✅ API corriendo en:\x1b[0m http://localhost:${port}`);
|
||||
console.log(
|
||||
`\x1b[32m✅ Documentación disponible en:\x1b[0m http://localhost:${port}/documentation`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\x1b[36m=========================================================\x1b[0m\n`,
|
||||
);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
Reference in New Issue
Block a user