Template del servicio de SIM

This commit is contained in:
2026-01-08 13:36:52 +01:00
parent a49469cd9e
commit 68631ff18f
33 changed files with 5138 additions and 1 deletions

9
.env Normal file
View File

@@ -0,0 +1,9 @@
POSTGRES_PASSWORD='1234'
POSTGRES_USER=postgres
POSTGRES_DB=postgres
POSTGRES_PORT='5432'
DEV_POSTGRES_PORT='5432'
PORT=3000
RABBITMQ_USER='guest'
RABBITMQ_PASSWORD='guest'

5
.yarnrc.yml Normal file
View File

@@ -0,0 +1,5 @@
compressionLevel: mixed
enableGlobalCache: false
nodeLinker: node-modules

2
build.local.sh Executable file
View File

@@ -0,0 +1,2 @@
#/bin/bash
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ build

19
deployment/Dockerfile.dev Normal file
View File

@@ -0,0 +1,19 @@
# stage base para coordinar las fases de build y ejecucion
FROM node:22-alpine AS base
WORKDIR /usr/local/app
COPY ./package.json ./yarn.lock ./
RUN corepack enable && \
corepack prepare yarn@4.12.0 --activate
# compilacion del ts -> js
FROM base AS backend
WORKDIR /usr/local/app
EXPOSE ${PORT}
COPY package*.json ./
# copia el codigo en general
COPY .tsconfig*.json ./
COPY ./packages ./packages
RUN yarn && cat package.json
CMD ["yarn", "run", "dev"]

View File

@@ -0,0 +1,31 @@
CREATE TABLE sim_job_queue (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
job_type TEXT NOT NULL,
sim_id INT, -- NOT NULL???
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT "pending", -- pasar a enum
priority SMALLINT NOT NULL DEFAULT 100, -- trabajamos entre 200 y 0 para 0 la maxima prioridad
retry_count INT NOT NULL DEFAULT 0,
max_retries INT NOT NULL DEFAULT 3, -- lo definiria en runtime
scheduled_for TIMESTAMP NOT NULL DEFAULT now(),
created_at TIMESTAMP NOT NULL DEFAULT now(),
started_at TIMESTAMP,
completed_at TIMESTAMP,
error_msg TEXT,
-- En teoria el check es mas flexible que el enum
CONSTRAINT valid_status CHECK (
status IN ('pending', 'processing', 'completed', 'failed', 'dead')
)
-- Revisar si interesa asociar cada trabajo a un id o permitir que sean independientes
CONSTRAINT fk_sim_id
FOREIGN KEY(sim_id) REFERENCES tableName(tarjetas_sim)
)
-- Indice de pendientes de consumir
CREATE INDEX idx_job_fetch ON sim_job_queue (status, scheduled_for, priority DESC, created_at)
WHERE status = 'pending';
-- Indice del resto de procesos
CREATE INDEX idx_job_monitor ON sim_job_queue (job_type, status, created_at);

View File

@@ -0,0 +1,65 @@
name: sim-eventos
networks:
default:
name: network-test # Tiene que coincidir con el compose objetivo
services:
rabbitmq:
container_name: rabbitmq-broker
image: "rabbitmq:4.2.2-management"
ports:
- "5672:5672"
- "15672:15672"
env_file:
- .env
restart: unless-stopped
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
interval: 30s
timeout: 10s
retries: 5
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
volumes:
- ./rabbitmq_plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
- ./deployment/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./deployment/rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro
sim-gateway:
container_name: sim-gateway
volumes:
- ./:/usr/local/app
- ./node_modules:/usr/local/node_modules
build:
context: ./
dockerfile: deployment/Dockerfile.dev
args:
PORT: "${PORT:-3000}"
develop:
watch:
- path: ./src
action: sync
target: /usr/local/app/src
- path: ./package.json
action: rebuild
ports:
- ${PORT}:${PORT}
env_file:
- .env
restart: unless-stopped
postgresql-sim:
image: postgres:16.1
env_file:
- .env
ports:
- "5432:${DEV_POSTGRES_PORT}"
volumes:
- ./sql-data/:/var/lib/postgres/data
- ./deployment/database/test.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
retries: 5
start_period: 30s
timeout: 10s

View File

@@ -0,0 +1,9 @@
#!/bin/bash
cd /mnt/docker-storage/containers/savefamily/sf-shopify-orders
docker stop sf-shopify-orders-api || true
docker rm sf-shopify-orders-api || true
docker rmi sf-shopify-orders-api || true
docker compose -f docker-compose.yaml up --build -d

View File

@@ -0,0 +1,71 @@
{
"rabbit_version": "4.2.2",
"rabbitmq_version": "4.2.2",
"product_name": "RabbitMQ",
"product_version": "4.2.2",
"users": [
{
"name": "guest",
"password": "guest",
"hashing_algorithm": "rabbit_password_hashing_sha256",
"tags": "administrator"
}
],
"vhosts": [
{
"name": "sim-vhost"
}
],
"permissions": [
{
"user": "guest",
"vhost": "sim-vhost",
"configure": ".*",
"write": ".*",
"read": ".*"
}
],
"topic_permissions": [],
"parameters": [],
"global_parameters": [
{
"name": "cluster_name",
"value": "rabbit@a8d5c6e08439"
},
{
"name": "internal_cluster_id",
"value": "rabbitmq-cluster-id-gXeBLbsUC2W2tU0Bx_QY_w"
}
],
"policies": [],
"exchanges": [
{
"name": "sim.exchange",
"vhost": "sim-vhost",
"type": "direct",
"durable": true,
"auto_delete": false,
"internal": false,
"argurments": {}
}
],
"queues": [
{
"name": "sim.queue",
"vhost": "sim-vhost",
"durable": true,
"auto_delete": false,
"arguments": {}
}
],
"bindings": [
{
"source": "sim.exchange",
"vhost": "sim-vhost",
"destination": "sim.queue",
"destination_type": "queue",
"routing_key": "sim.*",
"arguments": {}
}
]
}

View File

@@ -0,0 +1,11 @@
default_user = guest
default_pass = guest
listeners.tcp.default = 5672
management.tcp.port = 15672
management.load_definitions = /etc/rabbitmq/definitions.json
default_queue_type = quorum

1843
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,38 @@
{
"name": "sim-cola-eventos",
"packageManager": "yarn@4.12.0"
"packageManager": "yarn@4.12.0",
"workspaces": [
"packages/*"
],
"scripts": {
"test": "vitest watch",
"build": "npx tsc",
"start": "node dist/index.js",
"typecheck": "npx tsc --noEmit",
"dev": "yarn workspaces foreach -A run dev",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@tsconfig/node22": "^22.0.5",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"typescript": "^5.9.3"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/node": "^25.0.3",
"@types/supertest": "^6.0.3",
"prettier": "^3.7.4",
"supertest": "^7.1.4",
"tsx": "^4.21.0",
"vitest": "^4.0.16"
},
"imports": {
"#*": "./src/*"
}
}

View File

@@ -0,0 +1,7 @@
import { makeValidator } from 'envalid';
import { isValidEnvString, isValidEnvStringArray } from '#shared/domain/utils/env-validators';
export const ensureEnvStringArray = makeValidator((value: unknown) => isValidEnvStringArray(value));
export const ensureEnvString = makeValidator((value: unknown) => isValidEnvString(value));

45
packages/shared/config/env/index.ts vendored Normal file
View File

@@ -0,0 +1,45 @@
import { bool, cleanEnv, num, port, str } from 'envalid';
import { USER_ROLES } from '#shared/domain/models/authorizer-user.entity';
import { ensureEnvString } from './env-validators';
export const env = cleanEnv(process.env, {
ENVIRONMENT: str({
choices: ['local', 'development', 'production']
}),
USE_IN_MEMORY_REPOSITORIES: bool(),
ALERTS_API_PORT: port(),
COMPANIES_COMMAND_URL: ensureEnvString(),
DEVICES_COMMAND_URL: ensureEnvString(),
USERS_COMMAND_URL: ensureEnvString(),
REDIS_PASSWORD: ensureEnvString(),
REDIS_PORT: port(),
REDIS_HOST: ensureEnvString(),
POSTGRES_USER: ensureEnvString(),
POSTGRES_PASSWORD: ensureEnvString(),
POSTGRES_PORT: port(),
POSTGRES_HOST: ensureEnvString(),
POSTGRES_DATABASE: ensureEnvString(),
RABBITMQ_HOST: ensureEnvString(),
RABBITMQ_USER: ensureEnvString(),
RABBITMQ_PASSWORD: ensureEnvString(),
RABBITMQ_EXCHANGE: ensureEnvString(),
RABBITMQ_PORT: port(),
RABBITMQ_MODULENAME: ensureEnvString(),
RABBITMQ_TTL: port(),
RABBITMQ_SECURE: bool(),
RABBITMQ_RETRY_INTERVAL: num(),
SYSTEM_USER_ID: ensureEnvString(),
SYSTEM_USER_EMAIL: ensureEnvString(),
SYSTEM_USER_ROLE: str({
choices: [
USER_ROLES.ADMIN,
USER_ROLES.CLIENT,
USER_ROLES.DELEGATION_MANAGER,
USER_ROLES.OPERATOR,
USER_ROLES.ROOM_STAFF
],
default: USER_ROLES.ADMIN
})
});

View File

@@ -0,0 +1,12 @@
import { env } from '#api/config/env';
export const eventBusConfig = {
host: env.RABBITMQ_HOST,
user: env.RABBITMQ_USER,
password: env.RABBITMQ_PASSWORD,
exchange: env.RABBITMQ_EXCHANGE,
port: env.RABBITMQ_PORT,
modulename: env.RABBITMQ_MODULENAME,
ttl: env.RABBITMQ_TTL,
secure: env.RABBITMQ_SECURE
};

2
packages/shared/index.ts Normal file
View File

@@ -0,0 +1,2 @@
console.log("shared")
export default {}

View File

@@ -0,0 +1,31 @@
{
"name": "sim-shared",
"version": "1.0.0",
"description": "",
"main": "src/app.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx index.ts"
},
"author": "",
"license": "ISC",
"private": true,
"packageManager": "yarn@4.12.0",
"dependencies": {
"@tsconfig/node22": "*",
"cors": "*",
"dotenv": "*",
"express": "*",
"typescript": "*"
},
"devDependencies": {
"@types/cors": "*",
"@types/express": "*",
"@types/node": "*",
"@types/supertest": "*",
"prettier": "*",
"supertest": "*",
"tsx": "*",
"vitest": "*"
}
}

View File

@@ -0,0 +1,34 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/shared",
"baseUrl": ".",
"paths": {
"#adapters/*": [
"src/adapters/*"
],
"#domain/*": [
"src/domain/*"
],
"#ports/*": [
"src/ports/*"
],
"#tests/*": [
"__tests__/*"
],
"#shared/*": [
"./*"
]
}
},
"exclude": [
"node_modules"
],
"include": [
"**/*.ts",
"src/**/*.d.ts"
],
"files": [
"index.ts"
]
}

View File

@@ -0,0 +1,13 @@
ENVIORMENT=development
RABBITMQ_HOST=rabittmq-broker
RABBITMQ_PORT=5672
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
RABBITMQ_SECURE=false
POSTGRES_DATABASE=postres
POSTGRES_HOST=postgresql-sim-1
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

View File

@@ -0,0 +1,7 @@
import { makeValidator } from 'envalid';
import { isValidEnvString, isValidEnvStringArray } from '#shared/domain/utils/env-validators';
export const ensureEnvStringArray = makeValidator((value: unknown) => isValidEnvStringArray(value));
export const ensureEnvString = makeValidator((value: unknown) => isValidEnvString(value));

View File

@@ -0,0 +1,17 @@
export const env = {
ENVIRONMENT: process.env.ENVIORMENT,
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: process.env.RABBITMQ_HOST,
RABBITMQ_USER: process.env.RABBITMQ_USER,
RABBITMQ_PASSWORD: process.env.RABBITMQ_PASSWORD,
RABBITMQ_EXCHANGE: process.env.RABBITMQ_EXCHANGE,
RABBITMQ_PORT: process.env.RABBITMQ_PORT,
RABBITMQ_MODULENAME: process.env.MODULENAME,
RABBITMQ_TTL: process.env.RABBITMQ_TTL,
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
};

View File

@@ -0,0 +1,12 @@
import { env } from '#api/config/env';
export const eventBusConfig = {
host: env.RABBITMQ_HOST,
user: env.RABBITMQ_USER,
password: env.RABBITMQ_PASSWORD,
exchange: env.RABBITMQ_EXCHANGE,
port: env.RABBITMQ_PORT,
modulename: env.RABBITMQ_MODULENAME,
ttl: env.RABBITMQ_TTL,
secure: env.RABBITMQ_SECURE
};

View File

@@ -0,0 +1,43 @@
import client, { ChannelModel, connect, Connection } from "amqplib"
import { env } from "config/env"
import { Channel } from "node:diagnostics_channel"
const rmqUser = env.RABBITMQ_USER
const rmqPass = env.RABBITMQ_PASSWORD
const rmqHost = env.RABBITMQ_HOST
const rmqPort = Number(env.RABBITMQ_PORT)
const rmqSecure = env.RABBITMQ_SECURE
class RabbitConnection {
connection?: Connection
channel?: Channel
connected: Boolean = false
protected async createConnection(): Promise<ChannelModel> {
const { hostname, port, secure } = { hostname: rmqHost, port: rmqPort, secure: rmqSecure }
const { username, password } = { username: rmqUser, password: rmqPass };
const protocol = secure ? 'amqps' : 'amqp';
const connection = await connect({
protocol,
hostname,
port,
username,
password
});
connection.on('error', (error: unknown) => {
console.error(`[EVENT BUS] Rabbitmq connection error :: ${error}`);
Promise.reject(error);
});
return connection;
}
}
while (true) {
console.log("test")
}
export default {}

View File

@@ -0,0 +1,32 @@
{
"name": "sim-gestor-eventos",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx index.ts"
},
"author": "",
"license": "ISC",
"packageManager": "yarn@4.12.0",
"dependencies": {
"@tsconfig/node22": "*",
"amqplib": "^0.10.9",
"cors": "*",
"dotenv": "*",
"express": "*",
"typescript": "*"
},
"devDependencies": {
"@types/amqplib": "^0.10.8",
"@types/cors": "*",
"@types/express": "*",
"@types/node": "*",
"@types/supertest": "*",
"prettier": "*",
"supertest": "*",
"tsx": "*",
"vitest": "*"
}
}

View File

@@ -0,0 +1,34 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/sim-gestor-eventos",
"baseUrl": ".",
"paths": {
"#adapters/*": [
"src/adapters/*"
],
"#domain/*": [
"src/domain/*"
],
"#ports/*": [
"src/ports/*"
],
"#tests/*": [
"__tests__/*"
],
"#shared/*": [
"../shared/*"
],
}
},
"exclude": [
"node_modules"
],
"include": [
"**/*.ts",
"src/**/*.d.ts"
],
"files": [
"index.ts"
]
}

View File

@@ -0,0 +1 @@
[rabbitmq_management].

2
run.local.sh Executable file
View File

@@ -0,0 +1,2 @@
#/bin/bash
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ up

29
src/app.ts Normal file
View File

@@ -0,0 +1,29 @@
import express from 'express';
import cors from 'cors';
import { config } from './config';
const PORT = config.port;
const server = express();
// Middleware
server.use(cors());
// Webhooks often require raw body for signature verification if not handled by express.json()
// using verify option in body-parser/express.json to get raw body if needed.
server.use(express.json({
verify: (req: any, res, buf) => {
req.rawBody = buf;
}
}));
server.use(express.urlencoded({ extended: true }));
// Health check
server.get('/health', (req, res) => {
res.status(200).send({ resp: 'OK' });
});
server.listen(PORT, () => {
console.log(`[Server] Server is running on port ${PORT} in ${config.env} mode`);
console.log(`[Server] Webhook endpoint: http://localhost:${PORT}/api/webhooks`);
});

12
src/config/index.ts Normal file
View File

@@ -0,0 +1,12 @@
import dotenv from 'dotenv';
dotenv.config();
export const config = {
port: process.env.PORT || 3000,
env: process.env.NODE_ENV || 'development',
};
export const rabbitConfing = {
}

View File

@@ -0,0 +1,70 @@
/**
* Configuracion de los host iniciales y a que topics escuchan
*/
import { ScheduledEvent, SetupSubscription, SubscriptionData } from "#controllers/subscriptions.types.js"
import { webhooksConfig } from "."
export function generateSubscriptions(initialHosts?: SetupSubscription[]) {
const initialhs = initialHosts || INITIAL_HOSTS
const topicSubscriberMap = new Map<string, SubscriptionData>()
for (const setup of initialhs) {
const topic = setup.topic
if (topicSubscriberMap.get(topic) == undefined) {
topicSubscriberMap.set(topic, {
topic: topic,
subscriptors: []
})
}
topicSubscriberMap.get(topic)!.subscriptors.push({
...setup
})
}
return topicSubscriberMap
}
type InitialHost = {
topic: string,
host: string,
port: string,
endpoint: string,
method: "POST" | "PUT" | "DELETE",
secretkey: string,
}
export const INITIAL_HOSTS: InitialHost[] = [
{
topic: "order/create",
host: "localhost",
port: "3500",
endpoint: "/order/create",
method: "POST",
secretkey: webhooksConfig.apiSecret || "1234",
},
{
topic: "order/create",
host: "localhost",
port: "3000",
endpoint: '/webhooks/shopify/orders',
method: "POST",
secretkey: webhooksConfig.apiSecret || "1234",
},
{
topic: "order/create",
host: "sf-shopify-orders-api",
port: "3000",
endpoint: '/webhooks/shopify/orders',
method: "POST",
secretkey: webhooksConfig.apiSecret || "1234",
}
]
export const INITIAL_EVENTS: ScheduledEvent[] = [
{
topic: "order/create",
numberOfMessages: -1,
periodMs: 5000
}
]

3
src/rabbit.ts Normal file
View File

@@ -0,0 +1,3 @@
import rabbit from "rabbitmq-stream-js-client"
const client = await rabbit.connect()

2
stop.local.sh Executable file
View File

@@ -0,0 +1,2 @@
#/bin/bash
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ down -v

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./"
},
"include": [
"**/*.ts"
],
"exclude": [
"dist"
]
}

2617
yarn.lock Normal file

File diff suppressed because it is too large Load Diff