base de datos de orders con repositorio y test

This commit is contained in:
2026-02-16 17:31:20 +01:00
parent 0a42e4776d
commit 39a2622cb1
12 changed files with 561 additions and 22 deletions

View File

@@ -0,0 +1,27 @@
/**
* !Importate
* Configuración unicamente para lanzar los test, este código no debe de ejecutarse
* en produccion
*/
import { env, loadEnvFile } from "node:process";
import { Pool } from "pg";
import { PgClient } from "../infrastructure/PgClient.js";
console.warn("[i!] Se está corriendo codigo de test")
loadEnvFile("../../.env") // Global
// se hace una por servicio.
export const pgPool = new Pool({
user: env.POSTGRES_USER,
host: env.POSTGRES_HOST,
database: env.POSTGRES_DATABASE,
password: env.POSTGRES_PASSWORD,
port: Number(env.POSTGRES_PORT) || 5432,
});
export const postgresClient = new PgClient({
pool: pgPool
})
console.warn(`[T] TEST DB : ${env.POSTGRES_DATABASE}@${env.POSTGRES_HOST}`)

View File

@@ -0,0 +1,50 @@
// Reemplaza al enum OrderStatus
export type OrderStatus =
| 'pending'
| 'running'
| 'finished'
| 'failed'
| 'dlx';
// Reemplaza al enum OrderTypes
export type OrderType =
| 'activate'
| 'preactivate'
| 'cancel'
| 'pause'
| 'reactivate'
| 'unknown';
// Interfaz para la tabla order_tracking
export interface OrderTracking<T> {
id: number;
correlation_id: string;
exchange?: string | null;
routing_key?: string | null;
order_type: OrderType;
payload?: Record<string, T> | null; // Por no especificar el tipo del json hasta que no se cree
status: OrderStatus;
retry_count: number;
error_message?: string | null;
error_stacktrace?: string | null;
/* TODO: Importante decidir si trabajar con fecha y tener que crear los objetos o seguir como string */
start_date: string | Date;
update_date: string | Date;
finish_date?: string | Date | null;
}
// Interfaz para la tabla order_history
export interface OrderHistory {
id: number;
order_id: number;
previous_status: OrderStatus;
new_status: OrderStatus;
change_reason?: string | null;
change_date: Date;
}
// Tipo útil para la creación (Omitiendo campos generados por la DB)
export type CreateOrderDTO = Pick<
OrderTracking<any>, // Aqui realmente no importan los campos
'correlation_id' | 'exchange' | 'routing_key' | 'order_type' | 'payload'
>;

View File

@@ -1,7 +1,14 @@
/**
* Result<Error,Data>
*/
export type Result<E, D> = {
error: E | undefined,
data: D | undefined
}
export type Result<E, D> =
{
error: E,
data: undefined
}
|
{
error: undefined,
data: D
}

View File

@@ -45,8 +45,8 @@ export class OperationsRepository implements IOperationsRepository {
error = COALESCE($3,error),
request_id = COALESCE($4, request_id),
mass_action_id = COALESCE($5, mass_action_id),
last_change_date = now(),
end_date = CASE WHEN $2 IN ('finished') THEN now() ELSE end_date END,
last_change_date = now() at time zone 'utc',
end_date = CASE WHEN $2 IN ('finished') THEN now() at time zone 'utc' ELSE end_date END,
objenious_status = $6
WHERE id = $1`;

View File

@@ -0,0 +1,35 @@
import { describe, it } from "node:test";
import { OrderRepository } from "./OrderRepository.js";
import { CreateOrderDTO } from "../domain/Order.js";
import { postgresClient } from "../config/config.test.js";
import assert from "node:assert";
const order1 = <CreateOrderDTO>{
correlation_id: "fakeRMQid-1234",
exchange: "fake.ex",
routing_key: "test.order.idk",
order_type: "activate",
payload: { iccid: "1234", action: "activate" }
}
describe("Test OrderRepository", {}, () => {
const orderRepo = new OrderRepository(postgresClient)
it("Insert new Order", async () => {
const newOrder = order1
const result = await orderRepo.createOrder(newOrder)
assert(result.error == undefined)
assert(result.data != undefined)
const order = result.data!
assert(order.id != undefined)
assert(order.correlation_id == newOrder.correlation_id)
assert(order.status == 'pending')
console.log("[T] Creada Order", typeof (result.data.start_date))
})
})

View File

@@ -0,0 +1,381 @@
/**
* TODO: Usar
*/
import { PoolClient, QueryResult, QueryResultRow } from "pg";
import { CreateOrderDTO, OrderTracking } from "../domain/Order.js";
import { Result } from "../domain/Result.js";
import { PgClient } from "./PgClient.js";
export class OrderRepository {
constructor(
private readonly pgClient: PgClient
) {
}
/**
* Comprobacion de la query y devolucion del primer resulado
* Garantiza la gestion de errores
*/
private async getFirst<T extends QueryResultRow>(queryPromise: Promise<QueryResult<T>>) {
try {
const queryResult = await queryPromise
return <Result<string, T>>{
data: queryResult.rows[0]
}
} catch (e) {
return <Result<string, T>>{
error: e as string
}
}
}
/**
* Se asume que se va a devolver una lista del tipo T
*/
private async getAll<T extends QueryResultRow>(queryPromise: Promise<QueryResult<T>>) {
try {
const queryResult = await queryPromise
return <Result<string, T[]>>{
data: queryResult.rows
}
} catch (e) {
return <Result<string, T>>{
error: e as string
}
}
}
/**
* TODO: OrderTracking necestia un tipo para la estructura del mensaje almacenado
*/
public async getOrderById(data: { id: number }) {
const query = `
SELECT * FROM order_tracking
WHERE id = $1
`
const values = [data.id]
const queryPromise = this.pgClient.query<OrderTracking<any>>(query, values)
const result = this.getFirst(queryPromise);
return result
}
/**
* Busqueda según la id de RabbitMq
*/
public async getOrderByQueueId(data: { correlation_id: string }, pool?: PoolClient) {
const query = `
SELECT * FROM order_tracking
WHERE correlation_id = $1
`
const values = [data.correlation_id]
const queryPromise = this.pgClient.query<OrderTracking<any>>(query, values)
const result = this.getFirst(queryPromise);
return result
}
/**
* TODO:
* - variable para el limit
*/
public async getPendingOrders() {
const client = await this.pgClient.connect();
const query = `
SELECT * FROM order_tracking
WHERE finish_date IS NULL
ORDER BY start_date ASC
`
const values: string[] = []
const queryPromise = client.query(query, values)
const result = await this.getAll(queryPromise)
client.release()
return result
}
public async createOrder(data: CreateOrderDTO) {
const client = await this.pgClient.connect();
await client.query("BEGIN")
const query = `
INSERT INTO order_tracking (
correlation_id,
exchange,
routing_key,
order_type,
payload,
status
)
VALUES (
$1, -- correlation_id
$2, -- exchange
$3, -- routing_key
$4, -- order_type (ej: 'activate')
$5, -- payload (json object)
'pending'
)
RETURNING id, correlation_id, status, start_date;
`
const values = [data.correlation_id, data.exchange, data.routing_key, data.order_type, data.payload]
const queryPromise = client.query(query, values)
const result = await this.getFirst(queryPromise)
if (result.error == undefined) {
await client.query("COMMIT")
} else {
await client.query("ROLLBACK")
}
client.release()
return result
}
/**
* Actualizacion "correcta" del estado de un order
*/
public async updateOrder(args: {
id: number,
new_status: string,
reason: string
}) {
const client = await this.pgClient.connect();
await client.query('BEGIN');
// 1. Se consulta la order de base
const qCurrentOrder = `
SELECT * FROM order_tracking
WHERE id = $1
`
const vCurrentOrder = [args.id]
const currentOrderResult = await this.getFirst(client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder))
if (currentOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return currentOrderResult
}
const currentOrder = currentOrderResult.data!
// 2. Si todo ok se actualiza el order
const uOrderTracking = `
UPDATE order_tracking
SET
status = $2,
update_date = (now() at time zone 'utc')
WHERE id = $1
RETURNING id, status, update_date;
`
const vOrderTracking = [args.id, args.new_status]
const updatedOrderResult = await this.getFirst(
client.query<{ id: number, status: string, update_date: string }>(uOrderTracking, vOrderTracking)
)
if (updatedOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return updatedOrderResult
}
// 3. Si todo ok se añade una entradad de order_history con los datos modificados
const iOrderHistory = `
INSERT INTO order_history (
order_id,
previous_status,
new_status,
change_reason
)
VALUES (
$1, -- ID de la orden
$2, -- Estado anterior
$3, -- Nuevo estado
$4 -- Razón (ej: "Consumer processed successfully" o "RabbitMQ NACK")
);
`
const vOrderHistory = [args.id, currentOrder.status, args.new_status, args.reason]
const newOrderHistory = await this.getFirst(
client.query<{ id: number, status: string, update_date: string }>(iOrderHistory, vOrderHistory)
)
if (newOrderHistory.error != undefined) {
await client.query("ROLLBACK")
client.release()
return updatedOrderResult
}
await client.query("COMMIT")
const updatedOrder = await this.getFirst(
client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder)
)
return updatedOrder
}
public async finishOrder(args: { id: number, reason?: string }) {
const client = await this.pgClient.connect();
await client.query('BEGIN');
// 1. Se consulta la order de base
const qCurrentOrder = `
SELECT * FROM order_tracking
WHERE id = $1
`
const vCurrentOrder = [args.id]
const currentOrderResult = await this.getFirst(client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder))
if (currentOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return currentOrderResult
}
const currentOrder = currentOrderResult.data!
// 2. Si todo ok se actualiza el order
const uOrderTracking = `
UPDATE order_tracking
SET
status = 'finished',
update_date = (now() at time zone 'utc'),
finish_date = (now() at time zone 'utc')
WHERE id = $1
RETURNING id, status, update_date;
`
const vOrderTracking = [args.id]
const updatedOrderResult = await this.getFirst(
client.query<{ id: number, status: string, update_date: string }>(uOrderTracking, vOrderTracking)
)
if (updatedOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return updatedOrderResult
}
// 3. Si todo ok se guardo un nuevo registro de history
const iOrderHistory = `
INSERT INTO order_history (
order_id,
previous_status,
new_status,
change_reason
)
VALUES (
$1, -- ID de la orden
$2, -- Estado anterior
'finished',
$3 -- Siempre "finished successfully" a no ser que se especifique otra razón
);
`
const vOrderHistory = [args.id, currentOrder.status, args.reason ?? "finished successfully"]
const newOrderHistory = await this.getFirst(
client.query<{ id: number, status: string, update_date: string }>(iOrderHistory, vOrderHistory)
)
if (newOrderHistory.error != undefined) {
await client.query("ROLLBACK")
client.release()
return updatedOrderResult
}
await client.query("COMMIT")
const updatedOrder = await this.getFirst(
client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder)
)
return updatedOrder
}
public async errorOrder(args: {
id: number,
status: "failed" | "dlx",
reason: string,
error?: string,
stackTrace?: string
}) {
const client = await this.pgClient.connect();
await client.query('BEGIN');
// 1. Se consulta la order de base
const qCurrentOrder = `
SELECT * FROM order_tracking
WHERE id = $1
`
const vCurrentOrder = [args.id]
const currentOrderResult = await this.getFirst(client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder))
if (currentOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return currentOrderResult
}
const currentOrder = currentOrderResult.data!
// 3. Si todo ok se actualiza el order
// Si el status es dlx se asume que ha terminado y no va a reintentarse
// Si es failed se asume que se ha movido a la cola de delay y en algún momento se va a reintentar
const uOrderTracking = `
UPDATE order_tracking
SET
status = $2,
update_date = (now() at time zone 'utc'),
finish_date = CASE WHEN $2 = 'dlx' THEN (now() at time zone 'utc') ELSE null,
retry_count = retry_count + 1,
error_message = $3,
error_stacktrace = $4
WHERE id = $1
RETURNING id, status, update_date;
`
const vOrderTracking = [args.id, args.status, args.error, args.stackTrace]
const updatedOrderResult = await this.getFirst(
client.query<{ id: number, status: string, update_date: string }>(uOrderTracking, vOrderTracking)
)
if (updatedOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return updatedOrderResult
}
// 3. Si todo ok se guardo un nuevo registro de history
const iOrderHistory = `
INSERT INTO order_history (
order_id,
previous_status,
new_status,
change_reason
)
VALUES (
$1, -- ID de la orden
$2, -- Estado anterior
$3, -- En este caso particular 'dlx' o 'failed'
$4 -- En este caso el motivo de fallo completo
);
`
const vOrderHistory = [args.id, currentOrder.status, args.reason ?? "finished successfully", args.stackTrace]
const newOrderHistory = await this.getFirst(
client.query<{ id: number, status: string, update_date: string }>(iOrderHistory, vOrderHistory)
)
if (newOrderHistory.error != undefined) {
await client.query("ROLLBACK")
client.release()
return updatedOrderResult
}
await client.query("COMMIT")
const updatedOrder = await this.getFirst(
client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder)
)
return updatedOrder
}
}

View File

@@ -38,7 +38,7 @@
}
},
"scripts": {
"test": "echo \"Error: no test specified\" ",
"test": "node --import tsx --test ./**/*.test.ts",
"dev": "echo \" Shared no es un modulo ejecutable \" ",
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-shared/"
},