Merge pull request #2 from aabril/feat/database_and_feed_model

02 - adds mongodb database connection and odm and form the feed model
This commit is contained in:
2025-07-28 18:41:20 +02:00
committed by GitHub
14 changed files with 1058 additions and 39 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules node_modules
dist dist
*.bk

15
Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM node:24-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:24-slim AS production
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]

View File

@ -2,17 +2,88 @@
## Changelog ## Changelog
- Inicializamos el proyecto, usando npm init, con la node@latest (v24.4.1) - Inicializamos proyecto:
- Añadimos dependencias que voy a usar - Usando npm init, con la node@latest (v24.4.1)
- Creo una estructura de directorio inicial
- Añado un primer test (database.test.ts) para jest - [#1 PR : feat/project_structure ](https://github.com/aabril/dailytrends/pull/1)
- Añadimos dependencias que voy a usar
- Creo una estructura de directorio inicial
- Añado un primer test (database.test.ts) para jest
- [#2 PR : feat/database_and_feed_model ](https://github.com/aabril/dailytrends/pull/2)
- Añadimos `moongose` a las dependencias
- Añado un docker-compose con mongo local (luego lo ampliaré para esta propia app)
- Modificar el docker para tenerlo multistage y reducir el tamaño de las imagenes de contenedores
- añadir documentación de Docker, docker-compose
- añadir documentación de las capas de abstracción de "Feed"
- añadir tests para la conexión con la base de datos con Mocks (el mocking aqui a veces se complica)
- añadir primeras definiciones de Feed, empezaremos de lowerst a higher abstraction: tipo -> modelo -> repo -> servicio -> controller
## Feed layer abstractions
From higher to lower:
- Layer 1: HTTP/Controller (Highest Abstraction)
- `FeedController.ts` : Handles HTTP requests/responses, API endpoints
- Layer 2: Business Logic/Service
- `FeedService.ts` : Implements business rules, validation, orchestration
- Layer 3: Data Access/Repository
- `FeedRepository.ts` : Abstracts database operations, CRUD methods
- Layer 4: Data Model/Schema
- `Feed.ts` : Mongoose schema, database validations, indexes
- Layer 5: Type Definitions (Lowest Abstraction)
- `Feed.ts` : TypeScript interfaces, enums, DTOs
- `config.ts` : Configuration settings
## Dockerfile simple to multistage
I rebuild the Dockerfile to be multistage, since the image was heavy because all the node_modules dependencies.
The size of the image has been reduced from 717Mb to 376.
dailytrends-app-legacy latest 96a2dfe15361 3 minutes ago 717MB
dailytrends-app-light latest 7436142e1301 3 seconds ago 376MB
###### legacy
```Dockerfile
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
```
###### light
```Dockerfile
FROM node:24-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:24-slim AS production
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
```

26
docker-compose.yml Normal file
View File

@ -0,0 +1,26 @@
services:
mongo:
image: mongo:6
container_name: dailytrends_mongo
restart: always
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
app:
build: .
container_name: dailytrends_app
ports:
- "3000:3000"
environment:
- MONGO_URI=mongodb://mongo:27017/dailytrends
depends_on:
- mongo
volumes:
- .:/app
- /app/node_modules
command: npm run dev # for development
# change to "npm start" for production
volumes:
mongo_data:

15
jest.config.ts Normal file
View File

@ -0,0 +1,15 @@
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
transform: {
'^.+\\.ts$': ['ts-jest', { useESM: true }],
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
};
export default config;

446
package-lock.json generated
View File

@ -8,6 +8,10 @@
"name": "dailytrends", "name": "dailytrends",
"version": "0.0.1", "version": "0.0.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": {
"mongoose": "^8.16.5",
"pino": "^9.7.0"
},
"devDependencies": { "devDependencies": {
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^24.1.0", "@types/node": "^24.1.0",
@ -15,6 +19,7 @@
"@typescript-eslint/parser": "^8.38.0", "@typescript-eslint/parser": "^8.38.0",
"eslint": "^9.32.0", "eslint": "^9.32.0",
"jest": "^30.0.5", "jest": "^30.0.5",
"pino-pretty": "^13.0.0",
"ts-jest": "^29.4.0", "ts-jest": "^29.4.0",
"tsx": "^4.20.3", "tsx": "^4.20.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
@ -1877,6 +1882,15 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"node_modules/@mongodb-js/saslprep": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz",
"integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==",
"license": "MIT",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12", "version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@ -2140,6 +2154,21 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
"license": "MIT"
},
"node_modules/@types/whatwg-url": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
"license": "MIT",
"dependencies": {
"@types/webidl-conversions": "*"
}
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.33", "version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@ -2805,6 +2834,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/babel-jest": { "node_modules/babel-jest": {
"version": "30.0.5", "version": "30.0.5",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz",
@ -2989,6 +3027,15 @@
"node-int64": "^0.4.0" "node-int64": "^0.4.0"
} }
}, },
"node_modules/bson": {
"version": "6.10.4",
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
"integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
"license": "Apache-2.0",
"engines": {
"node": ">=16.20.1"
}
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -3203,6 +3250,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -3241,11 +3295,20 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@ -3363,6 +3426,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -3726,6 +3799,13 @@
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
} }
}, },
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -3777,6 +3857,22 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"dev": true,
"license": "MIT"
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@ -4050,6 +4146,13 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"dev": true,
"license": "MIT"
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@ -4957,6 +5060,16 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -5031,6 +5144,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/kareem": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
"integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -5145,6 +5267,12 @@
"tmpl": "1.0.5" "tmpl": "1.0.5"
} }
}, },
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"license": "MIT"
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -5202,6 +5330,16 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@ -5212,11 +5350,109 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/mongodb": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz",
"integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==",
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.9",
"bson": "^6.10.4",
"mongodb-connection-string-url": "^3.0.0"
},
"engines": {
"node": ">=16.20.1"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.188.0",
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
"snappy": "^7.2.2",
"socks": "^2.7.1"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"@mongodb-js/zstd": {
"optional": true
},
"gcp-metadata": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
},
"socks": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
"license": "Apache-2.0",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
"whatwg-url": "^14.1.0 || ^13.0.0"
}
},
"node_modules/mongoose": {
"version": "8.16.5",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.16.5.tgz",
"integrity": "sha512-Ey84Jf1c426FEMj16+j/VqFKupFN2Ey5uAy6TTAN9HlFP4OcunL7j9O7vTuwAipzlZdjctxP0OK9MRJ4Aa/jNg==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.4",
"kareem": "2.6.3",
"mongodb": "~6.17.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
"sift": "17.1.3"
},
"engines": {
"node": ">=16.20.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mongoose"
}
},
"node_modules/mpath": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/mquery": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
"integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
"license": "MIT",
"dependencies": {
"debug": "4.x"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/napi-postinstall": { "node_modules/napi-postinstall": {
@ -5279,6 +5515,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/once": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -5478,6 +5723,68 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pino": {
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.7.0.tgz",
"integrity": "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^5.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-pretty": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz",
"integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"colorette": "^2.0.7",
"dateformat": "^4.6.3",
"fast-copy": "^3.0.2",
"fast-safe-stringify": "^2.1.1",
"help-me": "^5.0.0",
"joycon": "^3.1.1",
"minimist": "^1.2.6",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pump": "^3.0.0",
"secure-json-parse": "^2.4.0",
"sonic-boom": "^4.0.1",
"strip-json-comments": "^3.1.1"
},
"bin": {
"pino-pretty": "bin.js"
}
},
"node_modules/pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
"license": "MIT"
},
"node_modules/pirates": { "node_modules/pirates": {
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@ -5595,11 +5902,37 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -5643,6 +5976,12 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@ -5650,6 +5989,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"license": "MIT",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -5738,6 +6086,22 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -5774,6 +6138,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sift": {
"version": "17.1.3",
"resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
"license": "MIT"
},
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@ -5797,6 +6167,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -5818,6 +6197,24 @@
"source-map": "^0.6.0" "source-map": "^0.6.0"
} }
}, },
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"license": "MIT",
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": { "node_modules/sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -6112,6 +6509,15 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
}
},
"node_modules/tmpl": { "node_modules/tmpl": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@ -6132,6 +6538,18 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@ -6463,6 +6881,28 @@
"makeerror": "1.0.12" "makeerror": "1.0.12"
} }
}, },
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -30,8 +30,13 @@
"@typescript-eslint/parser": "^8.38.0", "@typescript-eslint/parser": "^8.38.0",
"eslint": "^9.32.0", "eslint": "^9.32.0",
"jest": "^30.0.5", "jest": "^30.0.5",
"pino-pretty": "^13.0.0",
"ts-jest": "^29.4.0", "ts-jest": "^29.4.0",
"tsx": "^4.20.3", "tsx": "^4.20.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
},
"dependencies": {
"mongoose": "^8.16.5",
"pino": "^9.7.0"
} }
} }

View File

@ -0,0 +1,13 @@
import { config } from '../config/config.js';
describe('Configuration', () => {
test('should load configuration successfully', () => {
expect(config).toBeDefined();
expect(config.mongodbUri).toBeDefined();
expect(config.nodeEnv).toBeDefined();
});
test('should have valid environment', () => {
expect(['development', 'production', 'test']).toContain(config.nodeEnv);
});
});

View File

@ -1,6 +1,220 @@
// test temporal para probar que jest funciona import mongoose from 'mongoose';
import { DatabaseConnection } from '../config/database.js';
import { Logger } from '../utils/logger.js';
// Mock mongoose
jest.mock('mongoose', () => {
const mockConnection = {
readyState: 0,
on: jest.fn(),
once: jest.fn()
};
return {
connect: jest.fn(),
disconnect: jest.fn(),
connection: mockConnection
};
});
// Mock Logger
jest.mock('../utils/logger.js', () => ({
Logger: {
database: {
connected: jest.fn(),
disconnected: jest.fn(),
reconnected: jest.fn(),
alreadyConnected: jest.fn(),
notConnected: jest.fn()
},
error: jest.fn()
}
}));
// Mock config
jest.mock('../config/config.js', () => ({
config: {
mongodbUri: 'mongodb://localhost:27017/test'
}
}));
describe('DatabaseConnection', () => { describe('DatabaseConnection', () => {
describe('Singleton Pattern', () => { let mockMongoose: jest.Mocked<typeof mongoose>;
test('should return the same instance', () => {}) let mockLogger: jest.Mocked<typeof Logger>;
}) let databaseConnection: DatabaseConnection;
})
beforeEach(() => {
jest.clearAllMocks();
mockMongoose = mongoose as jest.Mocked<typeof mongoose>;
mockLogger = Logger as jest.Mocked<typeof Logger>;
// Reset singleton instance
(DatabaseConnection as any).instance = null;
databaseConnection = DatabaseConnection.getInstance();
});
describe('Singleton Pattern', () => {
test('should return the same instance', () => {
const instance1 = DatabaseConnection.getInstance();
const instance2 = DatabaseConnection.getInstance();
expect(instance1).toBe(instance2);
});
});
describe('connect', () => {
test('should connect to MongoDB successfully', async () => {
mockMongoose.connect.mockResolvedValueOnce(mockMongoose);
Object.defineProperty(mockMongoose.connection, 'readyState', { value: 1, writable: true });
await databaseConnection.connect();
expect(mockMongoose.connect).toHaveBeenCalledWith('mongodb://localhost:27017/test', {
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
bufferCommands: false
});
expect(mockLogger.database.connected).toHaveBeenCalled();
});
test('should not connect if already connected', async () => {
// Set as already connected
(databaseConnection as any).isConnected = true;
await databaseConnection.connect();
expect(mockMongoose.connect).not.toHaveBeenCalled();
expect(mockLogger.database.alreadyConnected).toHaveBeenCalled();
});
test('should handle connection errors', async () => {
const error = new Error('Connection failed');
mockMongoose.connect.mockRejectedValueOnce(error);
await expect(databaseConnection.connect()).rejects.toThrow('Connection failed');
expect(mockLogger.error).toHaveBeenCalledWith('Failed to connect to MongoDB', { error });
});
test('should set up connection event listeners', async () => {
mockMongoose.connect.mockResolvedValueOnce(mockMongoose);
Object.defineProperty(mockMongoose.connection, 'readyState', { value: 1, writable: true });
await databaseConnection.connect();
expect(mockMongoose.connection.on).toHaveBeenCalledWith('error', expect.any(Function));
expect(mockMongoose.connection.on).toHaveBeenCalledWith('disconnected', expect.any(Function));
expect(mockMongoose.connection.on).toHaveBeenCalledWith('reconnected', expect.any(Function));
});
});
describe('disconnect', () => {
test('should disconnect from MongoDB successfully', async () => {
// Set as connected
(databaseConnection as any).isConnected = true;
mockMongoose.disconnect.mockResolvedValueOnce(undefined);
await databaseConnection.disconnect();
expect(mockMongoose.disconnect).toHaveBeenCalled();
expect(mockLogger.database.disconnected).toHaveBeenCalled();
expect((databaseConnection as any).isConnected).toBe(false);
});
test('should not disconnect if not connected', async () => {
// Set as not connected
(databaseConnection as any).isConnected = false;
await databaseConnection.disconnect();
expect(mockMongoose.disconnect).not.toHaveBeenCalled();
expect(mockLogger.database.notConnected).toHaveBeenCalled();
});
test('should handle disconnection errors', async () => {
const error = new Error('Disconnection failed');
(databaseConnection as any).isConnected = true;
mockMongoose.disconnect.mockRejectedValueOnce(error);
await expect(databaseConnection.disconnect()).rejects.toThrow('Disconnection failed');
expect(mockLogger.error).toHaveBeenCalledWith('Error disconnecting from MongoDB', { error });
});
});
describe('isConnected', () => {
test('should return true when connected', () => {
(databaseConnection as any).isConnected = true;
expect(databaseConnection.getConnectionStatus()).toBe(true);
});
test('should return false when not connected', () => {
(databaseConnection as any).isConnected = false;
expect(databaseConnection.getConnectionStatus()).toBe(false);
});
});
describe('Event Handlers', () => {
test('should handle connection error event', async () => {
mockMongoose.connect.mockResolvedValueOnce(mockMongoose);
Object.defineProperty(mockMongoose.connection, 'readyState', { value: 1, writable: true });
let errorHandler: Function;
(mockMongoose.connection.on as jest.Mock).mockImplementation((event, handler) => {
if (event === 'error') {
errorHandler = handler;
}
return mockMongoose.connection;
});
await databaseConnection.connect();
// Simulate error event
const testError = new Error('Connection error');
errorHandler!(testError);
expect(mockLogger.error).toHaveBeenCalledWith('MongoDB connection error', { error: testError });
expect((databaseConnection as any).isConnected).toBe(false);
});
test('should handle disconnected event', async () => {
mockMongoose.connect.mockResolvedValueOnce(mockMongoose);
Object.defineProperty(mockMongoose.connection, 'readyState', { value: 1, writable: true });
let disconnectedHandler: Function;
(mockMongoose.connection.on as jest.Mock).mockImplementation((event, handler) => {
if (event === 'disconnected') {
disconnectedHandler = handler;
}
return mockMongoose.connection;
});
await databaseConnection.connect();
// Simulate disconnected event
disconnectedHandler!();
expect(mockLogger.database.disconnected).toHaveBeenCalled();
expect((databaseConnection as any).isConnected).toBe(false);
});
test('should handle reconnected event', async () => {
mockMongoose.connect.mockResolvedValueOnce(mockMongoose);
Object.defineProperty(mockMongoose.connection, 'readyState', { value: 1, writable: true });
let reconnectedHandler: Function;
(mockMongoose.connection.on as jest.Mock).mockImplementation((event, handler) => {
if (event === 'reconnected') {
reconnectedHandler = handler;
}
return mockMongoose.connection;
});
await databaseConnection.connect();
// Simulate reconnected event
reconnectedHandler!();
expect(mockLogger.database.reconnected).toHaveBeenCalled();
expect((databaseConnection as any).isConnected).toBe(true);
});
});
});

49
src/config/config.ts Normal file
View File

@ -0,0 +1,49 @@
export interface IConfig {
mongodbUri: string;
nodeEnv: string;
}
class Config implements IConfig {
private static instance: Config;
public readonly mongodbUri: string;
public readonly nodeEnv: string;
private constructor() {
this.mongodbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/dailytrends';
this.nodeEnv = process.env.NODE_ENV || 'development';
this.validateConfig();
}
public static getInstance(): Config {
if (!Config.instance) {
Config.instance = new Config();
}
return Config.instance;
}
private validateConfig(): void {
if (!this.mongodbUri) {
throw new Error('MONGODB_URI is required');
}
}
public isDevelopment(): boolean {
return this.nodeEnv === 'development';
}
public isProduction(): boolean {
return this.nodeEnv === 'production';
}
public isTest(): boolean {
return this.nodeEnv === 'test';
}
}
export const config = Config.getInstance();
export default config;

View File

@ -1,17 +1,95 @@
// aqui la conexión con MongoDB, usando mongoose o cualquier otro odm que vaya a usar import mongoose from 'mongoose';
// las conexión a bases de datos normalmente deberían ser Singleton para reutilizar la conexión import { config } from '../config/config.js';
// motivo: pues no saturar la base de datos ni saturarla con multiples conexiones import { Logger } from '../utils/logger.js';
export class DatabaseConnection { export class DatabaseConnection {
private static instance: DatabaseConnection; private static instance: DatabaseConnection;
// private isConnected: boolean = false; // a implementar private isConnected: boolean = false;
private constructor() {} private constructor() {}
public static getInstance(): DatabaseConnection { public static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) { if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection(); DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
} }
return DatabaseConnection.instance;
}
public async connect(): Promise<void> {
if (this.isConnected) {
Logger.database.alreadyConnected();
return;
}
try {
await mongoose.connect(config.mongodbUri, {
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
bufferCommands: false,
});
this.isConnected = true;
Logger.database.connected();
mongoose.connection.on('error', (error) => {
Logger.error('MongoDB connection error', { error });
this.isConnected = false;
});
mongoose.connection.on('disconnected', () => {
Logger.database.disconnected();
this.isConnected = false;
});
mongoose.connection.on('reconnected', () => {
Logger.database.reconnected();
this.isConnected = true;
});
} catch (error) {
Logger.error('Failed to connect to MongoDB', { error });
this.isConnected = false;
throw error;
}
}
public async disconnect(): Promise<void> {
if (!this.isConnected) {
Logger.database.notConnected();
return;
}
try {
await mongoose.disconnect();
this.isConnected = false;
Logger.database.disconnected();
} catch (error) {
Logger.error('Error disconnecting from MongoDB', { error });
throw error;
}
}
public getConnectionStatus(): boolean {
return this.isConnected && mongoose.connection.readyState === 1;
}
public async healthCheck(): Promise<{ status: string; message: string }> {
try {
if (!this.isConnected) {
return { status: 'error', message: 'Database not connected' };
}
await mongoose.connection.db?.admin().ping();
return { status: 'ok', message: 'Database connection is healthy' };
} catch (error) {
return {
status: 'error',
message: `Database health check failed: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
} }
export const database = DatabaseConnection.getInstance();
export default database;

View File

@ -1,5 +1,31 @@
// Aquí el modelo Feed import mongoose, { Schema, Document } from 'mongoose';
import { IFeed } from '../types/Feed.js';
// Si usase mongoose, supongo que será diretamente el modelo de mongoose export interface IFeedDocument extends IFeed, Document {
// Ya veré si uso algun otro ODM _id: string;
}
const feedSchema = new Schema<IFeedDocument>({
}, {
timestamps: true,
toJSON: {
transform: function(doc, ret) {
ret.id = ret._id;
delete (ret as any)._id;
delete (ret as any).__v;
return ret;
}
},
toObject: {
transform: function(doc, ret) {
ret.id = ret._id;
delete (ret as any)._id;
delete (ret as any).__v;
return ret;
}
}
});
export const Feed = mongoose.model<IFeedDocument>('Feed', feedSchema);
export default Feed;

View File

@ -1 +1,18 @@
// Aquí exportaré las interfaces que vaya a necesitas: las básicas, dtos, enums, etc. export enum NewsSource {
EL_PAIS = 'El País',
EL_MUNDO = 'El Mundo',
MANUAL = 'Manual'
}
export interface IFeed {
_id?: string;
title: string;
description: string;
url: string;
source: NewsSource;
publishedAt: Date;
imageUrl?: string;
category?: string;
createdAt?: Date;
updatedAt?: Date;
}

View File

@ -1,2 +1,51 @@
// Aquí "abstraeré" los logs, para evitar el uso de console.log (por seguridad, y por si queremos extraerlo a algun servicio) import pino from 'pino';
// En mi dia usaba winston, llegué a usar pino que era más ligero, supongo que usaré pino, ya veremos. import { config } from '../config/config.js';
// Create the logger instance with Pino
const logger = pino({
level: config.nodeEnv === 'production' ? 'info' : 'debug',
transport: config.nodeEnv === 'production' ? undefined : {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname'
}
},
// Production configuration
...(config.nodeEnv === 'production' && {
formatters: {
level: (label) => {
return { level: label };
}
}
})
});
export const Logger = {
error: (message: string, meta?: any) => {
logger.error(message, meta);
},
warn: (message: string, meta?: any) => {
logger.warn(message, meta);
},
info: (message: string, meta?: any) => {
logger.info(message, meta);
},
debug: (message: string, meta?: any) => {
logger.debug(message, meta);
},
database: {
connected: () => logger.info('✅ Database connected successfully'),
disconnected: () => logger.info('✅ Database disconnected'),
reconnected: () => logger.info('📡 MongoDB reconnected'),
alreadyConnected: () => logger.info('🔄 Database is already connected'),
notConnected: () => logger.warn('⚠️ Database is not connected')
},
};
export default Logger;