Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"jsonwebtoken": "^9.0.3",
"morgan": "^1.10.1",
"nodemon": "^3.1.14",
"pg": "^8.19.0"
"pg": "^8.19.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/express": "^5.0.6",
Expand All @@ -44,4 +45,4 @@
"typescript": "^5.9.3",
"vitest": "^4.1.0"
}
}
}
19 changes: 19 additions & 0 deletions prisma.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";

const directUrl = process.env["DIRECT_URL"];
if (!directUrl) {
throw new Error("La variable de entorno DIRECT_URL es requerida y no está definida");
}

export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: directUrl,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
2 changes: 1 addition & 1 deletion prisma.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ export default defineConfig({
path: "prisma/migrations",
},
datasource: {
url: process.env["DIRECT_URL"],
url: process.env["DIRECT_URL"] || "",
},
});
27 changes: 27 additions & 0 deletions src/middlewares/validate.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Request, Response, NextFunction } from "express";
import { ZodSchema, ZodError } from "zod";

type RequestSection = "body" | "query" | "params";

export const validate =
(schema: ZodSchema, section: RequestSection = "body") =>
(req: Request, res: Response, next: NextFunction): void => {
const result = schema.safeParse(req[section]);

if (!result.success) {
const errors = result.error.issues.map((e) => ({
field: e.path.join("."),
message: e.message
}));

res.status(400).json({
message: "Error de validación",
errors
});
return;
}

// Reemplaza el valor original con el dato ya parseado y transformado por Zod
req[section] = result.data as any;
next();
};
File renamed without changes.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export const getProductReviews = async (req, res) => {
const result = await getProductReviewsService(req.params.id, req.query);
return res.status(200).json(result);
} catch (error) {
console.error("Error obteniendo reseñas:", error);
return res.status(error.status || 500).json({
message: error.message || "Error interno del servidor"
const status = Number.isInteger(error?.status) && error.status >= 400 && error.status <= 599
? error.status
: 500;
return res.status(status).json({
message: status < 500 ? error.message : "Error interno del servidor"
});
}
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Router } from "express";
import { getProductReviews } from "./product-review.controller.js";
import {
getProductReviews,
} from "./product-review.controller.js";

const router = Router();

Expand Down
140 changes: 140 additions & 0 deletions src/modules/global/dtos/addresses/address.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { z } from "zod";
import { BaseResponseDTO } from "../base/base.response.dto.js";

// ─── REQUEST ─────────────────────────────────────────────────────
export const CreateAddressDTO = z.object({
fk_user: z
.number({ error: "fk_user es requerido" })
.int()
.positive("fk_user debe ser un ID válido"),

fk_store: z
.number()
.int()
.positive("fk_store debe ser un ID válido")
.nullable()
.optional(),

address: z
.string({ error: "address es requerido" })
.trim()
.min(1, "address no puede estar vacío")
.max(500, "address no puede superar 500 caracteres"),

city: z
.string({ error: "city es requerido" })
.trim()
.min(1, "city no puede estar vacío")
.max(100, "city no puede superar 100 caracteres"),

region: z
.string({ error: "region es requerida" })
.trim()
.min(1, "region no puede estar vacía")
.max(100, "region no puede superar 100 caracteres"),
Comment thread
coderabbitai[bot] marked this conversation as resolved.

postal_code: z
.string()
.trim()
.max(20, "postal_code no puede superar 20 caracteres")
.nullable()
.optional()
});

export type CreateAddressDTOType = z.infer<typeof CreateAddressDTO>;

export const UpdateAddressDTO = z
.object({
address: z
.string()
.trim()
.min(1, "address no puede estar vacío")
.max(500, "address no puede superar 500 caracteres").optional(),

city: z
.string()
.trim()
.min(1, "city no puede estar vacío")
.max(100, "city no puede superar 100 caracteres")
.optional(),

region: z
.string()
.trim()
.min(1, "region no puede estar vacía")
.max(100, "region no puede superar 100 caracteres")
.optional(),

postal_code: z
.string()
.trim()
.max(20, "postal_code no puede superar 20 caracteres")
.nullable()
.optional()
})
.refine((data) => Object.values(data).some((v) => v !== undefined), {
message: "Debe enviar al menos un campo para actualizar"
});

export type UpdateAddressDTOType = z.infer<typeof UpdateAddressDTO>;

export const FilterAddressDTO = z.object({
fk_user: z
.string()
.transform(Number)
.pipe(z.number().int().positive("fk_user debe ser un ID válido"))
.optional(),

city: z.string().trim().optional(),
region: z.string().trim().optional(),

page: z
.string()
.transform(Number)
.pipe(z.number().int().positive("page debe ser mayor a 0"))
.optional()
.default(1),

limit: z
.string()
.transform(Number)
.pipe(z.number().int().min(1).max(100, "limit no puede superar 100"))
.optional()
.default(10)
});

export type FilterAddressDTOType = z.infer<typeof FilterAddressDTO>;

// ─── RESPONSE ────────────────────────────────────────────────────
export class AddressResponseDTO extends BaseResponseDTO {
id_address: number;
fk_user: number;
fk_store: number | null;
address: string;
city: string;
region: string;
postal_code: string | null;

constructor(data: any) {
super({
id: data.id_address,
created_at: data.created_at,
updated_at: data.updated_at
});
this.id_address = data.id_address;
this.fk_user = data.fk_user;
this.fk_store = data.fk_store ?? null;
this.address = data.address;
this.city = data.city;
this.region = data.region;
this.postal_code = data.postal_code ?? null;
}

static map(data: any): AddressResponseDTO {
return new AddressResponseDTO(data);
}

static mapList(data: any[]): AddressResponseDTO[] {
return data.map(AddressResponseDTO.map);
}
}
48 changes: 48 additions & 0 deletions src/modules/global/dtos/base/base.response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

export class BaseResponseDTO {
id: number;
created_at: Date;
updated_at: Date;

constructor(data: { id: number; created_at: Date; updated_at: Date }) {
this.id = data.id;
this.created_at = data.created_at;
this.updated_at = data.updated_at;
}
}


export class PaginatedResponseDTO<T> {
content: T[];
total_elements: number;
total_pages: number;
size: number;
page: number;

constructor(data: {
content: T[];
total_elements: number;
size: number;
page: number;
}) {
this.content = data.content;
this.total_elements = data.total_elements;
this.size = data.size > 0 ? data.size : 0;
this.total_pages = data.size > 0 ? Math.ceil(data.total_elements / data.size) : 0;
this.page = data.page;
}

static from<T>(
items: T[],
totalCount: number,
page: number,
limit: number
): PaginatedResponseDTO<T> {
return new PaginatedResponseDTO({
content: items,
total_elements: totalCount,
size: limit,
page
});
}
}
Loading
Loading