Refactorizacion para permitir la inyeccion de dependencias en el

servicio de HTTP
This commit is contained in:
2026-02-10 13:20:39 +01:00
parent a9cde211eb
commit 70ef1131df
18 changed files with 104 additions and 56 deletions

View File

@@ -1,11 +1,45 @@
/**
* Herramientas para gestionar todos los trabajos con JWT, los tipos
* definidos aquí deben de ser sufcientemente generales para usarse
* en todos los proyectos.
*
* Las cabeceras de un token y la firma son standard para todos, pero
* el body puede variar con contenido nuevo.
*/
import { sign } from "node:crypto"
export type JWTHeader = {
alg: string,
typ: string,
kid: string
export interface IJWTService<T> {
/* Obtener un token de auth sin comprobar nada */
getNewAuthToken: () => Promise<JWTToken<T>>,
/* Obtener un token valido -> sino refrescar -> sin pillar un token nuevo */
getAccessToken: () => Promise<JWTToken<T>>,
/* Intenta refrescar el token actual */
tryRefreshToken: () => Promise<JWTToken<T>>,
}
export type JWTHeader = {
/* algoritmo de firma */
alg: string,
/* tipo de token */
typ: string,
/* key ID */
kid?: string
/* content type */
cty?: string
/**/
iss?: string,
sub?: string,
aud?: string,
jti?: string,
iat?: number,
exp?: number,
}
/*
* Define los campos basicos del payload de un token, en <T> se
* añaden los campos espeficos de la comunicacion
* */
export type JWTPayload<T> = {
/** (Issuer) Quién emitió el token */
iss?: string;
@@ -45,29 +79,33 @@ export type JWT<T> = {
}
// todo pasar a la clase JWT
function signJWT(args: {
algorythm: "sha256" | string,
data: string,
privateKey: string
}) {
function signJWT(args: SignatureOptions) {
const signature = sign(
args.algorythm,
Buffer.from(args.data),
args.privateKey
)
return signature
return signature.toString("base64url")
}
export type SignatureOptions = {
algorythm: "sha256" | string,
data: string,
privateKey: string,
}
export class JWTToken<T> {
public rawToken: string
private decodedPayload: JWTPayload<T> | undefined
private signatureFunc = signJWT
constructor(
token: string
token: string,
externalSignatureFunc?: (args: SignatureOptions) => string
) {
this.rawToken = token
this.decodedPayload = this.decodePayload()
if (externalSignatureFunc != undefined) this.signatureFunc = externalSignatureFunc
}
public static fromParts<T>(args: {
@@ -76,10 +114,13 @@ export class JWTToken<T> {
sigantureData?: {
privateKey: string,
algorythm: string
}
},
signatureFunc?: (args: SignatureOptions) => string
}) {
const strHeader = JSON.stringify(args.header)
const base64Header = Buffer.from(strHeader).toString("base64url")
const signatureFunc = args.signatureFunc ?? signJWT
let token = base64Header
if (args.payload != undefined) {
@@ -89,11 +130,11 @@ export class JWTToken<T> {
}
if (args.sigantureData != undefined) {
const base64signature = signJWT({
const base64signature = signatureFunc({
algorythm: args.sigantureData.algorythm,
privateKey: args.sigantureData.privateKey,
data: token
}).toString("base64url")
data: token,
})
token += ("." + base64signature)
}
@@ -116,4 +157,14 @@ export class JWTToken<T> {
if (expirationDate * 1000 <= now.getTime()) return true
return false
}
public isValid() {
throw new Error("No implementado")
}
public verifySignature() {
throw new Error("No implementado")
}
}

View File

@@ -0,0 +1,96 @@
import { Result } from "sim-shared/domain/Result.js";
export type StatusEnum = 'error' | 'finished' | 'noRequestId' | 'running' | 'noMassID';
export interface IOperationsRepository {
createOperation(data: ObjeniousOperation): Promise<Result<string, ObjeniousOperation>>
updateOperation(data: ObjeniousOperationChange): Promise<Result<string, ObjeniousOperation>>
getPendingOperations(): Promise<Result<string, ObjeniousOperation[]>>
}
export type ObjeniousOperation = {
id?: number;
operation: string;
retry_count?: number;
max_retry?: number;
max_date_retry?: Date | null;
iccids: string[];
request_id?: string;
mass_action_id?: string;
end_date?: Date | null;
error?: string | null;
status: StatusEnum;
objenious_status?: string;
last_change_date?: string;
}
export type ObjeniousOperationChange = {
id?: number;
operation_id: number;
info?: string | null;
error?: string | null;
new_status: StatusEnum;
previous_status?: StatusEnum;
new_objenious_status?: string;
previous_objenious_status?: string;
new_request_id?: string;
new_mass_action_id?: string;
}
export namespace Objenious {
export type Status = "En Cours" | "Terminé";
export type Identifier = "IMSI" | "MSISDN" | "REFERENCE" | "ICCID" | "IMEI"
export type ResponseGetRequestById = {
created: string,
status: "NEW" | "RUNNING" | "OK" | "KO" | "REPLAYED" | "CANCELLED" | "CLOSED" | "DISABLED",
statusDate: string,
actionType: "PREACTIVATION_AND_ACTIVATION" | string, // todo: añadir el resto
massActionIds: number[]
}
export type ActionType = "PREACTIVATION" | "PREACTIVATION_ACTIVATION" | "ACTIVATION" |
"STATUS_CHANGE" | "ICCID_CHANGE" | "EUICC_NOTIFICATION"
| "EUICC_AUDIT" | "MSISDN_CHANGE" | "ALARM_SETTING"
| "ALARM_UNSETTING" | "CUSTOM_FIELDS_UPDATE" | "SERVICE_CHANGE"
| "SEND_SMS" | "CHANGE_OFFER" | "PORT_IN"
| "PORT_OUT" | "CHANGE_CUSTOMER_ACCOUNT" | "SIMCARD_TRANSFER"
| "GEO_LOCATION" | "UPDATE_COMMITMENT" | "COACH_M2M"
| "PP_RECHARGE" | "TERMINATION_DFE" | "DO_ORDER_PAYMENT"
| "DO_TOP_UP" | "RADIUS_READ" | "RADIUS_UPDATE"
| "RADIUS_SYNCHRONIZE" | "NETWORK_RESET" | "UPDATE_YORK_COMMUNITY"
| "SUSPENSION" | "REACTIVATION" | "RESILIATION"
| "SORTIE_TEST" | "VALIDATION_RESILIATION" | "REFUS_RESILIATION";
export type ParametersGetMassAction = {
massActionId?: string,
unitActionId?: string,
createDateMin?: string,
createDateMax?: string,
dueDateMin?: string,
dueDateMax?: string,
endDateMin?: string,
endDateMax?: string,
"identifier.identifiers"?: string[],
"identifier.identifierType"?: Identifier,
actionTypes?: ActionType,
errorCodes?: string[],
pageNumber?: number,
pageSize?: number
}
export type ResponseGetMassAction = {
id: number,
loginCreator?: string,
longinCancellor?: string,
actionType: ActionType,
dueDate?: string,
targetActionNumber?: number,
created: string,
started?: string,
ended?: string,
status: string,
info?: string
}
}

View File

@@ -1,5 +1,7 @@
import axios, { AxiosInstance } from "axios"
import { JWTToken } from "../domain/JWT.js"
import { IJWTService, JWTToken } from "../domain/JWT.js"
// Cambiar por IJWRGeneralService
export type JWTProvider<T> = {
/** El servidor está solicitando un token nuevo o refrescando el actual*/
@@ -13,11 +15,13 @@ export class HttpClient {
public client: AxiosInstance
private jwtManager: JWTProvider<{}>
private jwtService: IJWTService<any> | undefined;
constructor(args: {
baseURL: string,
headers: Object,
jwtManager: JWTProvider<{}> // todo: asociar el tipo de token
jwtManager: JWTProvider<{}> // todo: asociar el tipo de token,
jwtService?: IJWTService<any>
}) {
this.client = axios.create({
...args
@@ -25,6 +29,7 @@ export class HttpClient {
this.jwtManager = args.jwtManager
if (args.jwtService != undefined) this.jwtService = args.jwtService
this.client.interceptors.request.use(
async (config) => {

View File

@@ -0,0 +1,96 @@
import { IOperationsRepository, ObjeniousOperation, ObjeniousOperationChange } from "sim-shared/domain/operationsRepository.port.js";
import { Result } from "sim-shared/domain/Result.js";
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
export class OperationsRepository implements IOperationsRepository {
constructor(
private readonly pgClient: PgClient
) {
}
async createOperation(data: ObjeniousOperation): Promise<Result<string, ObjeniousOperation>> {
const query = `
INSERT INTO objenious_operation (operation, iccids, status, max_retry, request_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`;
const iccids = JSON.stringify(data.iccids)
const values = [data.operation, iccids, data.status, data.max_retry, data.request_id];
const { rows } = await this.pgClient.query(query, values);
return <Result<string, ObjeniousOperation>>{
data: rows[0]
}
}
async updateOperation(data: ObjeniousOperationChange): Promise<Result<string, ObjeniousOperation>> {
const client = await this.pgClient.connect();
const {
new_status,
previous_status,
error,
new_request_id,
new_mass_action_id,
operation_id,
new_objenious_status,
previous_objenious_status
} = data
try {
await client.query('BEGIN');
// 1. Actualizar objenious_operation (la operacion base)
const updateParams = [operation_id, new_status, error, new_request_id, new_mass_action_id, new_objenious_status]
const updateOpQuery = `
UPDATE objenious_operation
SET
status = $2::status_enum,
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,
objenious_status = $6
WHERE id = $1`;
const operation = await client.query<ObjeniousOperation>(
updateOpQuery, updateParams)
// 2. Nuevo registro en objenious_operation_change (indica un cambio de estado)
const insertChangeQuery = `
INSERT INTO objenious_operation_change (operation_id, new_status, previous_status, error, new_request_id, new_mass_action_id, new_objenious_status, previous_objenious_status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`;
await client.query(insertChangeQuery, [operation_id, new_status, previous_status, error, new_request_id, new_mass_action_id, new_objenious_status, previous_objenious_status]);
await client.query('COMMIT');
return <Result<string, ObjeniousOperation>>{
data: operation.rows[0]
}
} catch (e) {
console.error("Error añadiendo actualizacion de la operation", e)
console.error("datos errorneos", data)
await client.query('ROLLBACK');
return <Result<string, ObjeniousOperation>>{
data: undefined,
error: e
}
} finally {
client.release();
}
}
async getPendingOperations(): Promise<Result<string, ObjeniousOperation[]>> {
// Aprovecha el índice 'pending_operations'
const query = `SELECT * FROM objenious_operation WHERE end_date IS NULL ORDER BY start_date ASC`;
try {
const { rows } = await this.pgClient.query<ObjeniousOperation>(query);
return {
error: undefined,
data: rows
};
} catch (e) {
return {
error: String(e),
data: undefined
}
}
}
}

View File

@@ -57,4 +57,4 @@
"tsx": "*",
"vitest": "*"
}
}
}

View File

@@ -9,6 +9,6 @@
],
"include": [
"**/*.ts",
"../../packages/sim-shared/**/*.ts"
"../../packages/sim-shared/**/*.ts",
]
}