19 Commits

Author SHA1 Message Date
30f1819a4e Merge branch 'main' of git.savefamilygps.net:SaveFamily/sf-sim into WEBINT-175_visualaizacion_tareas_sim 2026-04-21 08:57:17 +02:00
964ea6add9 Inicio migracion NOS 2026-04-16 12:44:31 +02:00
602878acf4 console log de debug 2026-04-16 12:01:31 +02:00
0aa52feaac Proxy 2026-04-16 11:51:30 +02:00
15b70309da Fix networkStatus 2026-04-15 15:11:13 +02:00
7001fccbf7 Problema de creacion de operacion de pausa/cancelacion 2026-04-15 13:50:20 +02:00
cffee785b2 logs 2026-04-15 12:48:08 +02:00
33d260310c Reactivate cuando la linea esté suspendida 2026-04-15 12:47:26 +02:00
e359acc1d5 Validaciones pausa instantaneas 2026-04-15 11:47:33 +02:00
bb4bce4a6d fix 2026-04-15 11:11:55 +02:00
eac74ef0cd Error key evento 2026-04-15 10:31:21 +02:00
1dc4eb5648 validador 2026-04-15 10:28:52 +02:00
a35a6c2b60 Error endpoint 2026-04-15 10:27:58 +02:00
1f78f4a3e1 Periodo pruebas reducidio 2026-04-15 10:18:49 +02:00
1e98559f3a Fix 2026-04-15 10:17:36 +02:00
ef0f860b9d Erro handler 2026-04-14 16:20:15 +02:00
0bff55379f tab 2026-04-14 16:15:01 +02:00
4d34308a13 Bug de logueo de operacion 2026-04-14 16:07:17 +02:00
cbbc0f6edb comienzo de la visualización de tareas + cola de rabbit 2026-03-26 12:35:44 +01:00
50 changed files with 934 additions and 53 deletions

3
.env
View File

@@ -27,3 +27,6 @@ PGHOST=localhost
PGUSER=alvar PGUSER=alvar
PGPASSWORD=alvar PGPASSWORD=alvar
PGPORT=5433 PGPORT=5433
# Proxy
CONNECTIONS_URL=https://sim-connections.savefamilygps.net

View File

@@ -11,7 +11,7 @@ post {
} }
body:form-urlencoded { body:form-urlencoded {
iccid: 8933201125068890694 iccid: 8933201125065160380
offer: SAVEFAMILY1 offer: SAVEFAMILY1
} }

View File

@@ -11,7 +11,7 @@ post {
} }
body:form-urlencoded { body:form-urlencoded {
iccid: 8933201125068886692 iccid: 8933201125068890074
} }
settings { settings {

View File

@@ -15,7 +15,7 @@ params:query {
} }
body:form-urlencoded { body:form-urlencoded {
iccid: 8933201125068886692 iccid: 8933201125068886700
} }
settings { settings {

View File

@@ -0,0 +1,21 @@
meta {
name: ReActivate
type: http
seq: 13
}
post {
url: {{baseurl}}/sim/reActivate
body: formUrlEncoded
auth: inherit
}
body:form-urlencoded {
iccid: 8933201125065160380
~offer: SAVEFAMILY1
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,4 @@
vars {
baseurl: http://sim-connections.savefamilygps.net
}
color: #C77A0F

View File

@@ -0,0 +1,20 @@
meta {
name: test proxy
type: http
seq: 14
}
get {
url: {{baseurl}}/simconnections/alai/select?iccid=1111111111111111111
body: none
auth: inherit
}
params:query {
iccid: 1111111111111111111
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -5,13 +5,13 @@ meta {
} }
get { get {
url: {{actionsUrl}}/massActions?massActionId=5192767 url: {{actionsUrl}}/massActions?massActionId=5363116
body: formUrlEncoded body: formUrlEncoded
auth: bearer auth: bearer
} }
params:query { params:query {
massActionId: 5192767 massActionId: 5363116
~identifier.identifierType: ICCID ~identifier.identifierType: ICCID
~identifier.identifiers: 8933201125065160463,8933201125065160422 ~identifier.identifiers: 8933201125065160463,8933201125065160422
} }

View File

@@ -1,5 +1,3 @@
PORT=3000 NOS_BASE_URL=localhost
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
ENVIORMENT=development ENVIORMENT=development

View File

@@ -1,5 +1,16 @@
import { loadEnvFile } from "node:process"; import { loadEnvFile } from "node:process";
loadEnvFile("../../.env") import path from "node:path";
try {
loadEnvFile(path.join("./.env")) // base
} catch (e) {
console.error("Error cargando el .env desde ./.env")
}
try {
loadEnvFile(path.join("../../.env")) // Global
} catch (e) {
console.error("Error cargando el .env desde ../../.env")
}
export const env = { export const env = {
ENVIRONMENT: process.env.ENVIORMENT, ENVIRONMENT: process.env.ENVIORMENT,
@@ -18,5 +29,8 @@ export const env = {
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE, RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL, RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST), RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
// ESPECIFICO NOS
NOS_BASE_URL: String(process.env.NOS_BASE_URL)
}; };

View File

@@ -0,0 +1,69 @@
import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js"
import { Channel } from "amqp-connection-manager"
import { env } from "./env/index.js"
const rmqUser = env.RABBITMQ_USER
const rmqPass = env.RABBITMQ_PASSWORD
const rmqHost = env.RABBITMQ_HOST
const rmqPort = Number(env.RABBITMQ_PORT)
const rmqSecure = false
const rmqVhost = env.RABBITMQ_VHOST
export const rmqConnOptions = <RMQConnectionParams>{
username: rmqUser,
password: rmqPass,
vhost: rmqVhost,
hostname: rmqHost,
port: rmqPort,
secure: rmqSecure,
}
export const rabbitmqEventBus = new RabbitMQEventBus({
connectionParams: rmqConnOptions,
buildStructure: buildQueues,
maxRetry: 5
})
async function buildQueues(channel: Channel) {
const QUEUES = {
NOS: "sim.nos",
NOSDLX: "sim.nos.dlx",
NOSDEL: "sim.nos.delayed",
}
const EXCHANGES = {
MAIN: "sim.exchange",
DLX: "sim.ex.nos.dlx",
DEL: "sim.ex.nos.delayed"
}
const DELAY = 10 * 1000
const BASE_NOS_KEY = "sim.nos.#"
await channel.assertExchange(EXCHANGES.DEL, "topic")
await channel.assertExchange(EXCHANGES.DLX, "topic")
await channel.assertExchange(EXCHANGES.MAIN, "topic")
await channel.assertQueue(QUEUES.NOS)
await channel.assertQueue(QUEUES.NOSDLX)
await channel.assertQueue(QUEUES.NOSDEL, {
durable: true,
arguments: {
'x-message-ttl': DELAY,
'x-dead-letter-exchange': EXCHANGES.MAIN,
}
})
// Cola dead-letter
await channel.bindQueue(QUEUES.NOSDLX, EXCHANGES.DLX, "sim.nos.#")
// Cola delay
await channel.bindQueue(QUEUES.NOSDEL, EXCHANGES.DEL, BASE_NOS_KEY)
// Cola nos -> main exchange
await channel.bindQueue(QUEUES.NOS, EXCHANGES.MAIN, BASE_NOS_KEY)
}
export async function startRMQClient() {
await rabbitmqEventBus.connect()
return rabbitmqEventBus
}

View File

@@ -1,5 +1,5 @@
import { startRMQClient } from "#config/eventBusConfig" import { startRMQClient } from "#config/eventBus.config.js"
import { SimNosController } from "./aplication/SimNOS.controller.js" import { SimNosController } from "./aplication/SimNOS.controller.js"
async function startWorker() { async function startWorker() {

View File

@@ -7,7 +7,8 @@
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build": "yarn tsc --project tsconfig.json && yarn tsc-alias && cp package.json ../../dist/packages/sim-consumidor-nos/", "build": "yarn tsc --project tsconfig.json && yarn tsc-alias && cp package.json ../../dist/packages/sim-consumidor-nos/",
"esbuild": "esbuild index.ts --platform=node", "esbuild": "esbuild index.ts --platform=node",
"start": "node ../../dist/packages/sim-consumidor-nos/index.js" "start": "node ../../dist/packages/sim-consumidor-nos/index.js",
"dev": "tsx watch index.ts"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",

View File

@@ -19,7 +19,7 @@ export class SimRouter {
["activate", this.simController.activate()], ["activate", this.simController.activate()],
["pause", this.simController.suspend()], ["pause", this.simController.suspend()],
["cancel", this.simController.terminate()], ["cancel", this.simController.terminate()],
["reActivate", this.simController.reActivate()], ["reactivate", this.simController.reActivate()],
["preActivate", this.simController.preActivate()] ["preActivate", this.simController.preActivate()]
]); ]);
} }

View File

@@ -75,13 +75,14 @@ export class SimUseCases {
operation: args.operation, operation: args.operation,
iccids: String(args.iccid), iccids: String(args.iccid),
status: "noMassID", status: "noMassID",
request_id: response.data.requestId request_id: response.data.requestId,
correlation_id: args.correlation_id
} }
// TODO: Esto tiene poco sentido si la operacion ya se // TODO: Esto tiene poco sentido si la operacion ya se
// tenia que haber creado en el generador // tenia que haber creado en el generador
this.logOperation(operation) this.logOperation(operation)
.then().catch(e => console.error(e)) .then().catch(e => console.error("Error login operation", e))
if (args.correlation_id != undefined) { if (args.correlation_id != undefined) {
this.orderRepository.updateOrder({ this.orderRepository.updateOrder({
@@ -115,6 +116,15 @@ export class SimUseCases {
public activate(activationData: ActivationData): () => Promise<Result<string, boolean>> { public activate(activationData: ActivationData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/activateLine" const OPERATION_URL = "/actions/activateLine"
return async () => { return async () => {
const iccid = activationData.identifier.identifiers
// Comporbación excepcional para saber si la linea está suspendida
const statusLinea = await this.objeniousRepository.getLinesAPI("ICCID", [String(iccid)])
console.log("statusLinea, ", iccid, statusLinea)
if (statusLinea.data != undefined && statusLinea.data[0].status.networkStatus == "SUSPENDED") {
const res = await this.reActivate(activationData)()
return res;
}
const req = this.httpClient.client.post(OPERATION_URL, { const req = this.httpClient.client.post(OPERATION_URL, {
dueDate: activationData.dueDate, dueDate: activationData.dueDate,
identifier: activationData.identifier, identifier: activationData.identifier,
@@ -198,16 +208,29 @@ export class SimUseCases {
} }
} }
public reActivate(pauseData: ActionData): () => Promise<Result<string, boolean>> { public reActivate(reactivateData: ActionData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/reactivateLine" const OPERATION_URL = "/actions/reactivateLine"
return async () => { return async () => {
const req = this.httpClient.client.post(OPERATION_URL, { const req = this.httpClient.client.post(OPERATION_URL, {
...pauseData ...reactivateData
}) })
try { try {
const response = await req const response = await req
// Creacion de la operacion inicial, antes de tener los datos
const operation: ObjeniousOperation = {
operation: "reactivate",
iccids: reactivateData.identifier.identifiers[0],
status: "noMassID",
request_id: response.data.requestId,
correlation_id: reactivateData.correlation_id
}
// TODO: Esto tiene poco sentido si la operacion ya se
// tenia que haber creado en el generador
this.logOperation(operation)
.then().catch(e => console.error("Error login operation", e))
if (response.status == 200) { if (response.status == 200) {
console.log("[o] Sim solicitud de reactivacion ", response.data) console.log("[o] Sim solicitud de reactivacion ", response.data)
return <Result<string, boolean>>{ return <Result<string, boolean>>{
@@ -223,7 +246,7 @@ export class SimUseCases {
} catch (error) { } catch (error) {
console.error("[x] Error reactivacion", (error as AxiosError).response?.status) console.error("[x] Error reactivacion", (error as AxiosError).response?.status)
return <Result<string, boolean>>{ return <Result<string, boolean>>{
error: "Error reactivando la sim" + pauseData.identifier, error: "Error reactivando la sim" + reactivateData.identifier,
data: undefined data: undefined
} }
} }
@@ -276,6 +299,17 @@ export class SimUseCases {
const correlation_id = suspendData.correlation_id const correlation_id = suspendData.correlation_id
const iccid = suspendData.identifier.identifiers const iccid = suspendData.identifier.identifiers
const operation: ObjeniousOperation = {
operation: "suspend",
iccids: iccid[0],
status: "running",
correlation_id: correlation_id
}
// No se registra hasta que no pase por la tabla de pausas
// this.logOperation(operation)
// .then().catch(e => console.error("Error login operation", e))
const fail = (error: string) => { const fail = (error: string) => {
console.error("[Sim.usecases]", error) console.error("[Sim.usecases]", error)
if (correlation_id != undefined) { if (correlation_id != undefined) {
@@ -286,6 +320,14 @@ export class SimUseCases {
} }
} }
// TODO REGISTRAR EL ORDER
if (correlation_id != undefined) {
await this.orderRepository.createOrder({
correlation_id: correlation_id,
order_type: "pause"
})
}
let activationDate; let activationDate;
try { try {
activationDate = await this.findActivationDate(suspendData) activationDate = await this.findActivationDate(suspendData)
@@ -332,10 +374,11 @@ export class SimUseCases {
public stage_terminate(terminateData: ActionData): () => Promise<Result<string, boolean>> { public stage_terminate(terminateData: ActionData): () => Promise<Result<string, boolean>> {
return async (): Promise<Result<string, boolean>> => { return async (): Promise<Result<string, boolean>> => {
const correlation_id = terminateData.correlation_id const correlation_id = terminateData.correlation_id
const iccid = terminateData.identifier.identifiers[0]
const activationDate = await this.findActivationDate(terminateData) const activationDate = await this.findActivationDate(terminateData)
const newTask: CreatePauseCancelTaskDTO = { const newTask: CreatePauseCancelTaskDTO = {
iccid: terminateData.identifier.identifiers[0], iccid: iccid,
activation_date: activationDate, activation_date: activationDate,
next_check: undefined, // Que se haga instantaneamente al ser la primera next_check: undefined, // Que se haga instantaneamente al ser la primera
operation_type: "terminate", operation_type: "terminate",
@@ -344,6 +387,17 @@ export class SimUseCases {
const taskCreated = await this.pauseRepository.addTask(newTask) const taskCreated = await this.pauseRepository.addTask(newTask)
const operation: ObjeniousOperation = {
operation: "terminate",
iccids: iccid,
status: "running",
correlation_id: correlation_id
}
/**
this.logOperation(operation)
.then().catch(e => console.error("Error login operation", e))
*/
// Caso que la task no se pueda crear en la BDD // Caso que la task no se pueda crear en la BDD
if (taskCreated.error != undefined) { if (taskCreated.error != undefined) {
console.error("[Sim.usecases]", taskCreated.error) console.error("[Sim.usecases]", taskCreated.error)

View File

@@ -27,8 +27,8 @@ export const rabbitmqEventBus = new RabbitMQEventBus({
async function buildQueues(channel: Channel) { async function buildQueues(channel: Channel) {
const QUEUES = { const QUEUES = {
OBJ: "sim.objenious", OBJ: "sim.objenious",
DLX: "sim.objenious.dlx", OBJDLX: "sim.objenious.dlx",
DEL: "sim.objenious.delayed" OBJDEL: "sim.objenious.delayed",
} }
const EXCHANGES = { const EXCHANGES = {
@@ -45,8 +45,8 @@ async function buildQueues(channel: Channel) {
await channel.assertExchange(EXCHANGES.MAIN, "topic") await channel.assertExchange(EXCHANGES.MAIN, "topic")
await channel.assertQueue(QUEUES.OBJ) await channel.assertQueue(QUEUES.OBJ)
await channel.assertQueue(QUEUES.DLX) await channel.assertQueue(QUEUES.OBJDLX)
await channel.assertQueue(QUEUES.DEL, { await channel.assertQueue(QUEUES.OBJDEL, {
durable: true, durable: true,
arguments: { arguments: {
'x-message-ttl': DELAY, 'x-message-ttl': DELAY,

View File

@@ -131,6 +131,21 @@ export class SimController {
}) })
} }
public reActivation() {
return this.controllerGenerator<{ iccid: string, offer: string }, { iccid: string, offer: string, compañia: string }>({
validator: iccidValidator,
mapBody: (b) => {
const { iccid, offer } = b
const compañia = companyFromIccid(iccid)
return { iccid, compañia, offer }
},
useCase: (args) => this.simUseCases.reActivation(args),
onError: (d, e) => console.error("[x] Error reactivacion: ", d, e),
onSuccess: console.log
})
}
public cancelation() { public cancelation() {
return this.controllerGenerator<{ iccid: string }, { iccid: string, compañia: string }>({ return this.controllerGenerator<{ iccid: string }, { iccid: string, compañia: string }>({
validator: iccidValidator, validator: iccidValidator,

View File

@@ -130,6 +130,36 @@ export class SimUsecases {
} }
} }
async reActivation(args: { iccid: string, compañia: string, offer: string }):
Promise<Result<string, { iccid: string, message_id: string, operation: "reactivate" }>> {
const activationEvent = <SimEvents.activation>{
key: `sim.${args.compañia}.reactivate`,
payload: {
iccid: args.iccid,
offer: args.offer
}
}
const activationWithId = this.addMessage_id(activationEvent)
console.log("[d] Reactivation ", activationWithId)
await this.eventBus.publish([activationWithId])
const createdOrder = await this.saveOrder<SimEvents.reActivation>(activationWithId)
if (createdOrder.error != undefined) {
console.error(createdOrder.error)
return {
error: createdOrder.error
}
}
return {
data: {
iccid: args.iccid,
operation: "reactivate",
message_id: createdOrder.data?.correlation_id
}
}
}
async preActivation(args: { iccid: string, compañia: string }): async preActivation(args: { iccid: string, compañia: string }):
Promise<Result<string, { iccid: string, message_id: string, operation: "preactivation" }>> { Promise<Result<string, { iccid: string, message_id: string, operation: "preactivation" }>> {
@@ -174,8 +204,10 @@ export class SimUsecases {
const cancelationWithId = this.addMessage_id(cancelationEvent) const cancelationWithId = this.addMessage_id(cancelationEvent)
console.log("[d] Cancelation ", cancelationWithId) console.log("[d] Cancelation ", cancelationWithId)
await this.eventBus.publish([cancelationWithId]) await this.eventBus.publish([cancelationWithId])
const savedOrder = await this.saveOrder(cancelationWithId) const savedOrder = await this.saveOrder(cancelationWithId)
if (savedOrder.error != undefined) { if (savedOrder.error != undefined) {
console.error(savedOrder.error) console.error(savedOrder.error)
return { return {
@@ -205,11 +237,12 @@ export class SimUsecases {
iccid: args.iccid iccid: args.iccid
} }
} }
const pauseWithId = this.addMessage_id(pauseEvent) const pauseWithId = this.addMessage_id(pauseEvent)
console.log("[d] Pause", pauseWithId) console.log("[d] Pause", pauseWithId)
await this.eventBus.publish([pauseWithId]) await this.eventBus.publish([pauseWithId])
await this.saveOrder(pauseWithId) //await this.saveOrder(pauseWithId)
const savedOrder = await this.saveOrder(pauseWithId) const savedOrder = await this.saveOrder<SimEvents.pause>(pauseWithId)
if (savedOrder.error != undefined) { if (savedOrder.error != undefined) {
console.error(savedOrder.error) console.error(savedOrder.error)

View File

@@ -22,4 +22,5 @@ export const env = {
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE, RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL, RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST), RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
CONNECTIONS_URL: String(process.env.CONNECTIONS_URL)
}; };

View File

@@ -5,6 +5,7 @@ import { simRoutes } from "./infrastructure/simRoutes.http.js"
import { rabbitmqEventBus } from '#config/eventBusConfig.js'; import { rabbitmqEventBus } from '#config/eventBusConfig.js';
import { env } from "#config/env/index.js" import { env } from "#config/env/index.js"
import { orderRoutes } from "#adapters/orderRoutes.http.js"; import { orderRoutes } from "#adapters/orderRoutes.http.js";
import { connectionsRoutes } from "#adapters/simconnectionsRoutes.js";
const PORT = env.API_PORT const PORT = env.API_PORT
const HOSTNAME = "0.0.0.0" const HOSTNAME = "0.0.0.0"
@@ -26,6 +27,7 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use("/sim", simRoutes) app.use("/sim", simRoutes)
app.use("/simconnections", connectionsRoutes)
app.use("/orders", orderRoutes) app.use("/orders", orderRoutes)
app.use("/docs", express.static(path.join(process.cwd(), '../../docs'))) app.use("/docs", express.static(path.join(process.cwd(), '../../docs')))

View File

@@ -23,6 +23,7 @@ simRoutes.get("/status", () => { })
simRoutes.post("/save", simController.save()) simRoutes.post("/save", simController.save())
simRoutes.post("/activate", simController.activation()) simRoutes.post("/activate", simController.activation())
simRoutes.post("/reActivate", simController.reActivation())
simRoutes.post("/preActivate", simController.preactivation()) simRoutes.post("/preActivate", simController.preactivation())
@@ -35,4 +36,5 @@ simRoutes.post("/test", simController.test())
// Proceso especifico de ALAI para liberar sims canceladas // Proceso especifico de ALAI para liberar sims canceladas
simRoutes.post("/free", simController.free()) simRoutes.post("/free", simController.free())
export { simRoutes } export { simRoutes }

View File

@@ -0,0 +1,87 @@
import { env } from "#config/env/index.js"
import { Router } from "express"
import { ClientRequest, IncomingMessage } from "http"
import { createProxyMiddleware } from "http-proxy-middleware"
import { Request } from "express"
export const connectionsRoutes = Router()
const CONNECTIONS_URL = env.CONNECTIONS_URL// TODO: Meter al ENV
//const CONNECTIONS_URL = "http://sf-nfc-server.savefamilygps.net"
console.log("CONNURL: ", CONNECTIONS_URL)
connectionsRoutes.use("", createProxyMiddleware({
target: CONNECTIONS_URL,
changeOrigin: true,
pathRewrite: {
'^/': "/simconnections/"
},
on: {
proxyReq: (proxyReq: ClientRequest, req: Request) => {
const protocol = req.protocol;
const host = req.get('host');
const originalFullUrl = `${protocol}://${host}${req.originalUrl}`;
const destinationFullUrl = `${CONNECTIONS_URL}${proxyReq.path}`;
/*
constnsole.log('──────────────────────────────────────────────────');
console.log(`[PROXY_DEBUG]`);
console.log(` ENTRADA: ${originalFullUrl}`);
console.log(` MÉTODO : ${req.method}`);
console.log(` DESTINO: ${destinationFullUrl}`);
console.log('──────────────────────────────────────────────────');
*/
console.log(`[Proxy Req]: ${req.method} ${req.url} -> ${proxyReq.path}`);
},
proxyRes: (proxyRes, req, res) => {
console.log(`[Proxy Res] Status: ${proxyRes.statusCode} desde ${req.url}`);
},
error: (err, req, res) => {
console.error('[Proxy Error]:', err);
// Validamos que 'res' tenga el método 'status' (típico de Express Response)
if ('status' in res) {
//@ts-ignore
res.status(500).json({ message: 'Error interno en el Gateway' });
}
},
}
}))
// Rutas
/**
connectionsRoutes.post('/simconnections/alai/preactivate',);
connectionsRoutes.get('/simconnections/alai/pause',);
connectionsRoutes.post('/simconnections/alai/terminate',);
connectionsRoutes.get('/simconnections/alai/pauseByPhone',);
connectionsRoutes.get('/simconnections/alai/active',);
connectionsRoutes.get('/simconnections/alai/change_orderid',);
connectionsRoutes.get('/simconnections/alai/select',);
connectionsRoutes.get('/simconnections/alai/select-iccid',);
connectionsRoutes.get('/simconnections/alai/selectFromDb',);
connectionsRoutes.get('/simconnections/alai/selectPage',);
connectionsRoutes.post('/simconnections/alai/schedulePause',);
connectionsRoutes.get('/simconnections/shopify/getbyWP',);
connectionsRoutes.get('/simconnections/shopify/getbyWPS',);
///
connectionsRoutes.get('/simconnections/sim/associate',);
connectionsRoutes.post('/simconnections/sim/search',);
connectionsRoutes.post('/simconnections/sim/historic',);
connectionsRoutes.post('/simconnections/sim/update',);
///
connectionsRoutes.post('/simconnections/nos/activate',);
connectionsRoutes.get('/simconnections/nos/select',);
connectionsRoutes.get('/simconnections/nos/selectPage',);
//Unificación
connectionsRoutes.post('/simconnections/sim/active',); // True false
connectionsRoutes.patch('/simconnections/sim/pause',);
connectionsRoutes.get('/simconnections/sim/select',);
connectionsRoutes.get('/simconnections/sim/select-phone',);
**/

View File

@@ -53,6 +53,7 @@
"cors": "*", "cors": "*",
"dotenv": "*", "dotenv": "*",
"express": "*", "express": "*",
"http-proxy-middleware": "^3.0.5",
"sim-shared": "sim-shared:*", "sim-shared": "sim-shared:*",
"typescript": "*" "typescript": "*"
}, },

View File

@@ -62,6 +62,7 @@ async function startCron() {
orderRepository orderRepository
) )
await objTask.getPendingOperations()
const PERIODO_PETICIONES = 10 * 60 * 1000 const PERIODO_PETICIONES = 10 * 60 * 1000
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
@@ -80,7 +81,6 @@ async function startCron() {
} }
}, PERIODO_VOLCADO) }, PERIODO_VOLCADO)
await pauseTask.run() await pauseTask.run()
const PERIODO_CANCELACIONES = 60 * 60 * 1000; const PERIODO_CANCELACIONES = 60 * 60 * 1000;
const clacelacionesInterval = setInterval(async () => { const clacelacionesInterval = setInterval(async () => {

View File

@@ -118,9 +118,11 @@ export class PauseTerminateTask {
switch (linea.status.billingStatus) { switch (linea.status.billingStatus) {
case "ACTIVATED": case "ACTIVATED":
let exito = false;
let result = null; let result = null;
// IMPORTANTE COMRPOBAR EL DUE DATE
// Se termina el proceso aqui pero pasa a ser una operación de
// objenious por lo que puede fallar y quedaria registrado en
// la tabla objenious_operation
switch (operacionTipo) { switch (operacionTipo) {
case "suspend": case "suspend":
result = await this.simUsecases.suspend(actionData)() result = await this.simUsecases.suspend(actionData)()

View File

@@ -0,0 +1,25 @@
import express from "express"
import cors from 'cors';
import path from 'path';
import { env } from "packages/sim-shared/config/env/index.js"
const PORT = env.API_PORT
const HOSTNAME = "0.0.0.0"
const app = express()
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/docs", express.static(path.join(process.cwd(), '../../docs')))
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" })
})
app.listen(PORT, HOSTNAME, () => {
console.log("[o] Servidor iniciado en el puerto %d", PORT)
})
export default {}

View File

@@ -0,0 +1,15 @@
import { RabbitManagementClient } from "../infrastructure/RabbitManagementClient.js";
import { Queue } from "../domain/Queue.js";
export class RabbitUseCases {
private client: RabbitManagementClient
constructor(client: RabbitManagementClient) {
this.client = client
}
public async getQueuesStatus(): Promise<Queue[]> {
const queues = await this.client.getQueues()
return queues.sort((a, b) => b.messages - a.messages)
}
}

20
packages/sim-shared/config/env/index.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
import { loadEnvFile } from "node:process";
import path from "node:path";
loadEnvFile(path.join("../../.env")) // Global
export const env = {
ENVIRONMENT: process.env.ENVIORMENT,
API_PORT: parseInt(process.env.API_PORT ?? "3000"),
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST ?? "localhost"),
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "test"),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "test"),
RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE ?? "/"),
RABBITMQ_PORT: parseInt(process.env.RABBITMQ_PORT ?? "5672"),
RABBITMQ_MODULENAME: process.env.MODULENAME,
RABBITMQ_TTL: process.env.RABBITMQ_TTL,
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
};

View File

@@ -1,5 +1,5 @@
import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js" import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js"
import { env } from "./env" import { env } from "./env/index.js"
const rmqUser = env.RABBITMQ_USER const rmqUser = env.RABBITMQ_USER
const rmqPass = env.RABBITMQ_PASSWORD const rmqPass = env.RABBITMQ_PASSWORD
@@ -22,18 +22,5 @@ export const rabbitmqEventBus = new RabbitMQEventBus({
}) })
export async function startRMQClient() { export async function startRMQClient() {
await rabbitmqEventBus.connect().catch(async e => {
console.error("Error en la conexion RMQ")
await rabbitmqEventBus.connect() await rabbitmqEventBus.connect()
})
// Bindings especificos, deberia meterlos en la clase
try {
await rabbitmqEventBus.channel?.assertQueue("sim.nos")
} catch {
console.log("[i] Cola de sims de nos creada")
await rabbitmqEventBus.channel?.bindQueue("sim.nos", "sim.exchange", "sim.nos.*")
}
return rabbitmqEventBus
} }

View File

@@ -80,7 +80,8 @@ export type FinishOrderDTO =
IdOrCorrelationID IdOrCorrelationID
& &
{ {
reason?: string reason?: string,
end_date?: Date
} }
export type ErrorOrderDTO = export type ErrorOrderDTO =

View File

@@ -0,0 +1,7 @@
export interface Queue {
name: string;
messages: number;
ready: number;
unacked: number;
consumers: number;
}

View File

@@ -24,7 +24,7 @@ export namespace SimEvents {
} }
export type reActivation = DomainEvent & { export type reActivation = DomainEvent & {
key: `sim.${string}.reActivate`, key: `sim.${string}.reactivate`,
payload: { payload: {
iccid: string iccid: string
}, },

View File

@@ -12,7 +12,7 @@ export type ObjeniousOperation = {
id?: number; id?: number;
/** Uuid del mensaje asociado a la operacion */ /** Uuid del mensaje asociado a la operacion */
correlation_id?: string; correlation_id?: string;
operation: "activate" | string; // TODO: completar y actualizar operation: "activate" | "suspend" | "terminate" | string; // TODO: completar y actualizar
retry_count?: number; retry_count?: number;
max_retry?: number; max_retry?: number;
max_date_retry?: string | null; max_date_retry?: string | null;
@@ -27,8 +27,7 @@ export type ObjeniousOperation = {
} }
export type ObjeniousOperationChange = { export type ObjeniousOperationChange = {
id?: number; id?: number; operation_id: number;
operation_id: number;
info?: string | null; info?: string | null;
error?: string | null; error?: string | null;
new_status: StatusEnum; new_status: StatusEnum;

View File

@@ -1,6 +1,20 @@
import { describe, it } from "node:test"; import { before, describe, it } from "node:test";
import { ObjeniousOperationsRepository } from "./ObjeniousOperationRepository.js"; import { ObjeniousOperationsRepository } from "./ObjeniousOperationRepository.js";
import { httpObjClient, postgresClient } from "../config/config.test.js"; import { httpObjClient, postgresClient } from "../config/config.test.js";
import { ObjeniousOperation } from "../domain/operationsRepository.port.js";
const correctOperation: ObjeniousOperation = {
iccids: "test",
operation: "activate",
status: "finished"
}
const errorOperation: ObjeniousOperation = {
iccids: "test",
operation: "terminate",
status: "error",
error: "mensaje de error"
}
describe("[Integration] Test API requests", () => { describe("[Integration] Test API requests", () => {
const repository = new ObjeniousOperationsRepository( const repository = new ObjeniousOperationsRepository(
@@ -8,7 +22,18 @@ describe("[Integration] Test API requests", () => {
postgresClient postgresClient
) )
it("Read /lines with multiple iccids", () => { before(async () => {
await repository.createOperation(correctOperation)
await repository.createOperation(errorOperation)
})
it("Read last operation by line", () => {
/**
* Objetivo:
* - Cuando se va a hacer una operacion de sim hay que cancelarla directamente si:
* - Ya hay una en curso del mismo tipo.
* - Ya ha terminado una del mismo tipo.
* - Se ignoran las erroneas
*/
}) })
}) })

View File

@@ -146,6 +146,20 @@ export class ObjeniousOperationsRepository implements IOperationsRepository {
} }
} }
async getLastOperationOfLine(iccid: string) {
const query = `
SELECT * FROM public.objenious_operation
WHERE iccids = $1 and error is null
ORDER BY id asc limit 1
`
const values = [iccid];
const { rows } = await this.pgClient.query(query, values);
return <Result<string, ObjeniousOperation>>{
data: rows[0]
}
}
async updateOperation(data: ObjeniousOperationChange): Promise<Result<string, ObjeniousOperation>> { async updateOperation(data: ObjeniousOperationChange): Promise<Result<string, ObjeniousOperation>> {
const client = await this.pgClient.connect(); const client = await this.pgClient.connect();
const { const {

View File

@@ -302,8 +302,8 @@ export class OrderRepository {
UPDATE order_tracking UPDATE order_tracking
SET SET
status = 'finished', status = 'finished',
update_date = (now() at time zone 'utc'), update_date = now(),
finish_date = (now() at time zone 'utc') finish_date = now()
WHERE id = $1 WHERE id = $1
RETURNING id, status, update_date; RETURNING id, status, update_date;
` `

View File

@@ -0,0 +1,37 @@
import axios, { AxiosInstance } from "axios";
import { Queue } from "sim-shared/domain/Queue.js";
export class RabbitManagementClient {
private client: AxiosInstance;
constructor(args: {
baseURL: string;
user: string;
password: string;
}) {
this.client = axios.create({
baseURL: args.baseURL,
auth: {
username: args.user,
password: args.password,
},
});
}
public async getQueues(): Promise<Queue[]> {
try {
const response = await this.client.get("/queues")
return response.data.map((q: any) => ({
name: q.name,
messages: q.messages,
ready: q.messages_ready,
unacked: q.messages_unacknowledged,
consumers: q.consumers
}))
} catch (err) {
console.error("[RabbitManagementClient] Error obteniendo colas", err);
throw err;
}
}
}

View File

@@ -0,0 +1,107 @@
import { Request, Response } from "express";
import { DashboardUseCases } from "./Dashboard.usecases.js";
import { QueueSummary } from "#domain/Dashboard.js";
import { OrderTracking } from "packages/sim-shared/domain/Order.js";
export class DashboardController {
constructor(private readonly useCases: DashboardUseCases) {}
public async getQueuesFragment(req: Request, res: Response) {
const data = await this.useCases.getDashboardData()
res.send(renderQueuesHtml(data.queues))
}
public async getOrdersFragment(req: Request, res: Response) {
const data = await this.useCases.getDashboardData()
res.send(renderOrdersHtml(data.pendingOrders))
}
public async getFullFragment(req: Request, res: Response) {
const data = await this.useCases.getDashboardData();
res.send(`
${renderQueuesHtml(data.queues)}
${renderOrdersHtml(data.pendingOrders)}
<p class="updated-at">Actualizado: ${new Date(data.generatedAt).toLocaleTimeString("es-ES")}</p>
`)
}
}
function queueBadgeClass(count: number): string {
if (count === 0 ) return "badge--ok"
if (count < 10) return "badge--warn"
return "badge--error"
}
function orderBadgeClass(status: string): string {
if (status === 'finished' ) return "badge--ok"
if (status == 'pending') return "badge--warn"
return "badge--error"
}
function renderQueuesHtml(queues: QueueSummary): string {
const row = (label: string, q: typeof queues.main) => `
<tr>
<td>${label}</td>
<td>${q.name}</td>
<td><span class="badge ${queueBadgeClass(q.messages)}">${q.messages}</span></td>
<td>${q.ready}</td>
<td>${q.unacked}</td>
<td>${q.consumers}</td>
</tr>
`
return `
<section id="queues-section">
<h2>Estado de las colas</h2>
<table class="table">
<thead>
<tr>
<th>Tipo</th>
<th>Cola</th>
<th>Total</th>
<th>Ready</th>
<th>Unacked</th>
<th>Consumers</th>
<tr>
</thead>
<tbody>
${row("Principal", queues.main)}
${row("Reintentos", queues.retry)}
${row("Fallidos (DLX)", queues.dlx)}
</tbody>
</table>
</section>
`
}
function renderOrdersHtml(orders: OrderTracking<unknown>[]): string {
const rows = orders.map(o => `
<tr>
<td>${o.id}</td>
<td>${o.correlation_id}</td>
<td>${o.order_type}</td>
<td><span class="badge ${orderBadgeClass(o.status)}">${o.status}</span></td>
<td>${o.retry_count ?? 0}</td>
<td>${new Date(o.start_date).toLocaleString("es-ES")}</td>
</tr>
`
).join("")
return `
<section id="orders-section">
<h2>Tareas pendientes (${orders.length})</h2>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Correlation ID</th>
<th>Tipo</th>
<th>Estado</th>
<th>Reintentos</th>
<th>Inicio</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</section>
`
}

View File

@@ -0,0 +1,47 @@
import { RabbitManagementClient } from "packages/sim-shared/infrastructure/RabbitManagementClient.js";
import { OrderRepository } from "packages/sim-shared/infrastructure/OrderRepository.js";
import { DashboardData, QueueSummary } from "#domain/Dashboard.js";
import { Queue } from "packages/sim-shared/domain/Queue.js";
import { env } from "#config/env/index.js";
const EMPTY_QUEUE: Queue = { name: "", messages: 0, ready: 0, unacked: 0, consumers: 0}
export class DashboardUseCases {
constructor(
private readonly rabbitClient: RabbitManagementClient,
private readonly orderRepo: OrderRepository,
) {}
public async getDashboardData(): Promise<DashboardData> {
//si uno peta, no rompe al otro
const [queuesResult, pendingResult] = await Promise.allSettled([
this.rabbitClient.getQueues(),
this.orderRepo.getPendingOrders({ limit: 100 }), //por poner un límite
])
let queues: QueueSummary = { main: EMPTY_QUEUE, retry: EMPTY_QUEUE, dlx: EMPTY_QUEUE}
if (queuesResult.status === 'fulfilled') {
const all = queuesResult.value
const find = (name: string) => all.find(q => q.name === name) ?? {...EMPTY_QUEUE, name}
queues = {
main: find(env.QUEUE_MAIN!),
retry: find(env.QUEUE_RETRY!),
dlx: find(env.QUEUE_DLX!)
}
} else {
console.error('[Dasboard] Error obteniendo colas: ', queuesResult.reason)
}
const pendingOrders = (pendingResult.status === "fulfilled" && !pendingResult.value.error)
? pendingResult.value.data ?? []
: [];
if (pendingResult.status === 'rejected') {
console.error('[Dashboard]Error obteniendo tareas: ', pendingResult.reason)
}
return {
queues,
pendingOrders,
generatedAt: new Date().toISOString(),
}
}
}

View File

@@ -0,0 +1,22 @@
import { loadEnvFile } from "node:process";
import path from "node:path";
loadEnvFile(path.join("../../env"))
export const env = {
API_PORT: parseInt(process.env.API_PORT ?? "3010"),
POSTGRES_USER: process.env.POSTGRES_USER,
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
POSTGRES_PORT: process.env.POSTGRES_PORT,
POSTGRES_HOST: process.env.POSTGRES_HOST,
POSTGRES_DATABASE: process.env.POSTGRES_DATABASE,
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST),
RABBITMQ_PORT: process.env.RABBITMQ_PORT,
RABBITMQ_USER: String(process.env.RABBITMQ_USER),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD),
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_VHOST: process.env.RABBITMQ_VHOST,
QUEUE_MAIN: process.env.QUEUE_MAIN,
QUEUE_RETRY: process.env.QUEUE_RETRY,
QUEUE_DLX: process.env.QUEUE_DLX
}

View File

@@ -0,0 +1,13 @@
import { Pool } from "pg";
import { PgClient } from "packages/sim-shared/infrastructure/PgClient.js";
import { env } from "./env/index.js";
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 })

View File

@@ -0,0 +1,8 @@
import { RabbitManagementClient } from "packages/sim-shared/infrastructure/RabbitManagementClient.js";
import { env } from "./env/index.js";
export const rabbitManagementClient = new RabbitManagementClient({
baseURL: env.RABBITMQ_HOST!,
user: env.RABBITMQ_USER!,
password: env.RABBITMQ_PASSWORD!
})

View File

@@ -0,0 +1,14 @@
import { Queue } from "packages/sim-shared/domain/Queue.js";
import { OrderTracking } from "packages/sim-shared/domain/Order.js";
export type QueueSummary = {
main: Queue;
retry: Queue;
dlx: Queue;
}
export interface DashboardData {
queues: QueueSummary;
pendingOrders: OrderTracking<unknown>[];
generatedAt: string;
}

View File

@@ -0,0 +1,45 @@
import express from "express";
import cors from "cors";
import path from "path";
import { env } from "#config/env/index.js";
import { pgPool, postgresClient } from "#config/postgreConfig.js";
import { OrderRepository } from "packages/sim-shared/infrastructure/OrderRepository.js";
import { DashboardController } from "./aplication/Dashboard.controller.js";
import { DashboardUseCases } from "./aplication/Dashboard.usecases.js";
import { createDashboardRouter } from "#adapters/dashboardRoutes.http.js";
import { PgClient } from "packages/sim-shared/infrastructure/PgClient.js";
import { RabbitManagementClient } from "packages/sim-shared/infrastructure/RabbitManagementClient.js";
const pgClient = new PgClient({
pool: pgPool,
})
const rabbitClient = new RabbitManagementClient({
baseURL: env.RABBITMQ_HOST,
user: env.RABBITMQ_USER,
password: env.RABBITMQ_PASSWORD,
})
const orderRepo = new OrderRepository(pgClient)
const useCases = new DashboardUseCases(rabbitClient, orderRepo)
const controller = new DashboardController(useCases)
const dashboardRouter = createDashboardRouter(controller)
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(process.cwd(), "../sim-visualizador-tareas-front")));
app.use("/", dashboardRouter);
app.get("/health", (_req, res) => res.status(200).json({ status: "ok" }));
async function main() {
await pgClient.checkDatabaseConnection();
app.listen(Number(env.API_PORT), "0.0.0.0", () => {
console.log("[o] Visualizador iniciado en el puerto %d", env.API_PORT);
});
}
main().catch((err) => {
console.error("[x] Error arrancando el visualizador:", err);
process.exit(1);
});

View File

@@ -0,0 +1,12 @@
import { Router } from "express";
import { DashboardController } from "../aplication/Dashboard.controller.js";
export function createDashboardRouter(controller: DashboardController): Router {
const router = Router()
router.get("/fragments/queues", (req, res) => controller.getQueuesFragment(req, res))
router.get("/fragments/orders", (req, res) => controller.getOrdersFragment(req, res))
router.get("/fragments/dashboard", (req, res) => controller.getFullFragment(req, res))
return router
}

View File

@@ -0,0 +1,72 @@
{
"name": "sim-visualizador-tareas-back",
"version": "1.0.0",
"type": "module",
"description": "",
"main": "index.ts",
"imports": {
"#config/*.js": {
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#config/*": {
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#adapters/*.js": {
"types": "./infrastructure/*.ts",
"default": "./infrastructure/*.js"
},
"#adapters/*": {
"types": "./infrastructure/*.ts",
"default": "./infrastructure/*.js"
},
"#domain/*.js": {
"types": "./domain/*.ts",
"default": "./domain/*.js"
},
"#domain/*": {
"types": "./domain/*.ts",
"default": "./domain/*.js"
},
"#ports/*.js": {
"types": "./ports/*.ts",
"default": "./ports/*.js"
},
"#ports/*": {
"types": "./ports/*.ts",
"default": "./ports/*.js"
}
},
"scripts": {
"test": "node --import tsx --test ./**/*.test.ts",
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-visualizador-tareas-back/",
"dev": "tsx watch index.ts",
"start": "node ../../dist/packages/sim-visualizador-tareas-back/index.js"
},
"author": "",
"license": "ISC",
"packageManager": "yarn@4.12.0",
"dependencies": {
"@tsconfig/node22": "*",
"amqplib": "^0.10.9",
"axios": "*",
"cors": "*",
"dotenv": "*",
"express": "*",
"sim-shared": "sim-shared:*",
"typescript": "*"
},
"devDependencies": {
"@types/amqplib": "^0.10.8",
"@types/cors": "*",
"@types/express": "*",
"@types/node": "*",
"@types/supertest": "*",
"prettier": "*",
"supertest": "*",
"tsc-alias": "^1.8.16",
"tsx": "*",
"vitest": "*"
}
}

View File

@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist",
"rootDir": "../../",
},
"exclude": [
"node_modules"
],
"include": [
"**/*.ts",
"../../packages/sim-shared/**/*.ts"
],
"files": [
"config/env/index.ts"
]
}

8
sf-sim.code-workspace Normal file
View File

@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

View File

@@ -576,6 +576,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/http-proxy@npm:^1.17.15":
version: 1.17.17
resolution: "@types/http-proxy@npm:1.17.17"
dependencies:
"@types/node": "npm:*"
checksum: 10/893e46e12be576baa471cf2fc13a4f0e413eaf30a5850de8fdbea3040e138ad4171234c59b986cf7137ff20a1582b254bf0c44cfd715d5ed772e1ab94dd75cd1
languageName: node
linkType: hard
"@types/methods@npm:^1.1.4": "@types/methods@npm:^1.1.4":
version: 1.1.4 version: 1.1.4
resolution: "@types/methods@npm:1.1.4" resolution: "@types/methods@npm:1.1.4"
@@ -1154,7 +1163,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.4, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.3": "debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.3":
version: 4.4.3 version: 4.4.3
resolution: "debug@npm:4.4.3" resolution: "debug@npm:4.4.3"
dependencies: dependencies:
@@ -1413,6 +1422,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eventemitter3@npm:^4.0.0":
version: 4.0.7
resolution: "eventemitter3@npm:4.0.7"
checksum: 10/8030029382404942c01d0037079f1b1bc8fed524b5849c237b80549b01e2fc49709e1d0c557fa65ca4498fc9e24cff1475ef7b855121fcc15f9d61f93e282346
languageName: node
linkType: hard
"expect-type@npm:^1.2.2": "expect-type@npm:^1.2.2":
version: 1.3.0 version: 1.3.0
resolution: "expect-type@npm:1.3.0" resolution: "expect-type@npm:1.3.0"
@@ -1527,6 +1543,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"follow-redirects@npm:^1.0.0":
version: 1.16.0
resolution: "follow-redirects@npm:1.16.0"
peerDependenciesMeta:
debug:
optional: true
checksum: 10/3fbe3d80b3b544c22705d837aa5d4a0d07a740d913534a2620b0a004c610af4148e3b58723536dd099aaa1c9d3a155964bde9665d6e5cb331460809a1fc572fd
languageName: node
linkType: hard
"follow-redirects@npm:^1.15.11": "follow-redirects@npm:^1.15.11":
version: 1.15.11 version: 1.15.11
resolution: "follow-redirects@npm:1.15.11" resolution: "follow-redirects@npm:1.15.11"
@@ -1788,6 +1814,31 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"http-proxy-middleware@npm:^3.0.5":
version: 3.0.5
resolution: "http-proxy-middleware@npm:3.0.5"
dependencies:
"@types/http-proxy": "npm:^1.17.15"
debug: "npm:^4.3.6"
http-proxy: "npm:^1.18.1"
is-glob: "npm:^4.0.3"
is-plain-object: "npm:^5.0.0"
micromatch: "npm:^4.0.8"
checksum: 10/83c1956be6451a5f4a2f3c7b3d84085dbd47e1efb5bb684c1ed668a6606c18c7c07be823b0dbba1326955b64cf88de2672492940b0b48d140215fbdb06105c9a
languageName: node
linkType: hard
"http-proxy@npm:^1.18.1":
version: 1.18.1
resolution: "http-proxy@npm:1.18.1"
dependencies:
eventemitter3: "npm:^4.0.0"
follow-redirects: "npm:^1.0.0"
requires-port: "npm:^1.0.0"
checksum: 10/2489e98aba70adbfd8b9d41ed1ff43528be4598c88616c558b109a09eaffe4bb35e551b6c75ac42ed7d948bb7530a22a2be6ef4f0cecacb5927be139f4274594
languageName: node
linkType: hard
"https-proxy-agent@npm:^7.0.1": "https-proxy-agent@npm:^7.0.1":
version: 7.0.6 version: 7.0.6
resolution: "https-proxy-agent@npm:7.0.6" resolution: "https-proxy-agent@npm:7.0.6"
@@ -1865,7 +1916,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-glob@npm:^4.0.1, is-glob@npm:~4.0.1": "is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1":
version: 4.0.3 version: 4.0.3
resolution: "is-glob@npm:4.0.3" resolution: "is-glob@npm:4.0.3"
dependencies: dependencies:
@@ -1881,6 +1932,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-plain-object@npm:^5.0.0":
version: 5.0.0
resolution: "is-plain-object@npm:5.0.0"
checksum: 10/e32d27061eef62c0847d303125440a38660517e586f2f3db7c9d179ae5b6674ab0f469d519b2e25c147a1a3bc87156d0d5f4d8821e0ce4a9ee7fe1fcf11ce45c
languageName: node
linkType: hard
"is-promise@npm:^4.0.0": "is-promise@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "is-promise@npm:4.0.0" resolution: "is-promise@npm:4.0.0"
@@ -2842,6 +2900,7 @@ __metadata:
cors: "npm:*" cors: "npm:*"
dotenv: "npm:*" dotenv: "npm:*"
express: "npm:*" express: "npm:*"
http-proxy-middleware: "npm:^3.0.5"
prettier: "npm:*" prettier: "npm:*"
sim-shared: "sim-shared:*" sim-shared: "sim-shared:*"
supertest: "npm:*" supertest: "npm:*"