diff --git a/.gitignore b/.gitignore index 76add87..2441240 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -dist \ No newline at end of file +dist +*.bk \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6f9425b --- /dev/null +++ b/Dockerfile @@ -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"] + diff --git a/README.md b/README.md index dc9d8b6..7568a09 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,88 @@ - - - - - - - ## Changelog -- Inicializamos el proyecto, usando npm init, con la node@latest (v24.4.1) -- Añadimos dependencias que voy a usar -- Creo una estructura de directorio inicial -- Añado un primer test (database.test.ts) para jest +- Inicializamos proyecto: + - Usando npm init, con la node@latest (v24.4.1) + +- [#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"] + +``` + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..105bc34 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..72ec980 --- /dev/null +++ b/jest.config.ts @@ -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; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 238c82c..abc36e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "dailytrends", "version": "0.0.1", "license": "AGPL-3.0-or-later", + "dependencies": { + "mongoose": "^8.16.5", + "pino": "^9.7.0" + }, "devDependencies": { "@types/jest": "^30.0.0", "@types/node": "^24.1.0", @@ -15,6 +19,7 @@ "@typescript-eslint/parser": "^8.38.0", "eslint": "^9.32.0", "jest": "^30.0.5", + "pino-pretty": "^13.0.0", "ts-jest": "^29.4.0", "tsx": "^4.20.3", "typescript": "^5.8.3" @@ -1877,6 +1882,15 @@ "@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": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2140,6 +2154,21 @@ "dev": true, "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": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -2805,6 +2834,15 @@ "dev": true, "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": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", @@ -2989,6 +3027,15 @@ "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3203,6 +3250,13 @@ "dev": true, "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": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3241,11 +3295,20 @@ "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": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3363,6 +3426,16 @@ "dev": true, "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": { "version": "1.3.2", "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_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": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3777,6 +3857,22 @@ "dev": true, "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": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -4050,6 +4146,13 @@ "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": { "version": "2.0.2", "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" } }, + "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5031,6 +5144,15 @@ "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": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5145,6 +5267,12 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5202,6 +5330,16 @@ "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": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -5212,11 +5350,109 @@ "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": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/napi-postinstall": { @@ -5279,6 +5515,15 @@ "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": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5478,6 +5723,68 @@ "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": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -5595,11 +5902,37 @@ "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": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5643,6 +5976,12 @@ ], "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": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5650,6 +5989,15 @@ "dev": true, "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5738,6 +6086,22 @@ "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": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -5774,6 +6138,12 @@ "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": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -5797,6 +6167,15 @@ "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": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5818,6 +6197,24 @@ "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -6112,6 +6509,15 @@ "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": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6132,6 +6538,18 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -6463,6 +6881,28 @@ "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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 8adb73a..d115a73 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,13 @@ "@typescript-eslint/parser": "^8.38.0", "eslint": "^9.32.0", "jest": "^30.0.5", + "pino-pretty": "^13.0.0", "ts-jest": "^29.4.0", "tsx": "^4.20.3", "typescript": "^5.8.3" + }, + "dependencies": { + "mongoose": "^8.16.5", + "pino": "^9.7.0" } } diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts new file mode 100644 index 0000000..a9ac08e --- /dev/null +++ b/src/__tests__/config.test.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/src/__tests__/database.test.ts b/src/__tests__/database.test.ts index 79e8580..807bd6b 100644 --- a/src/__tests__/database.test.ts +++ b/src/__tests__/database.test.ts @@ -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('Singleton Pattern', () => { - test('should return the same instance', () => {}) - }) -}) \ No newline at end of file + let mockMongoose: jest.Mocked; + let mockLogger: jest.Mocked; + let databaseConnection: DatabaseConnection; + + beforeEach(() => { + jest.clearAllMocks(); + mockMongoose = mongoose as jest.Mocked; + mockLogger = Logger as jest.Mocked; + + // 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); + }); + }); +}); \ No newline at end of file diff --git a/src/config/config.ts b/src/config/config.ts new file mode 100644 index 0000000..0e5069c --- /dev/null +++ b/src/config/config.ts @@ -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; \ No newline at end of file diff --git a/src/config/database.ts b/src/config/database.ts index c0828e3..f566719 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -1,17 +1,95 @@ -// aqui la conexión con MongoDB, usando mongoose o cualquier otro odm que vaya a usar -// las conexión a bases de datos normalmente deberían ser Singleton para reutilizar la conexión -// motivo: pues no saturar la base de datos ni saturarla con multiples conexiones +import mongoose from 'mongoose'; +import { config } from '../config/config.js'; +import { Logger } from '../utils/logger.js'; export class DatabaseConnection { - private static instance: DatabaseConnection; - // private isConnected: boolean = false; // a implementar - - private constructor() {} - - public static getInstance(): DatabaseConnection { - if (!DatabaseConnection.instance) { - DatabaseConnection.instance = new DatabaseConnection(); - } - return DatabaseConnection.instance; + private static instance: DatabaseConnection; + private isConnected: boolean = false; + + private constructor() {} + + public static getInstance(): DatabaseConnection { + if (!DatabaseConnection.instance) { + DatabaseConnection.instance = new DatabaseConnection(); } + return DatabaseConnection.instance; + } + + public async connect(): Promise { + 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 { + 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; \ No newline at end of file diff --git a/src/models/Feed.ts b/src/models/Feed.ts index 13a1260..5da0e23 100644 --- a/src/models/Feed.ts +++ b/src/models/Feed.ts @@ -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 -// Ya veré si uso algun otro ODM +export interface IFeedDocument extends IFeed, Document { + _id: string; +} +const feedSchema = new Schema({ + }, { + 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('Feed', feedSchema); +export default Feed; \ No newline at end of file diff --git a/src/types/Feed.ts b/src/types/Feed.ts index 67167a0..79db4c5 100644 --- a/src/types/Feed.ts +++ b/src/types/Feed.ts @@ -1 +1,18 @@ -// Aquí exportaré las interfaces que vaya a necesitas: las básicas, dtos, enums, etc. \ No newline at end of file +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; +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts index cdef6e8..ad98dd7 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -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) -// En mi dia usaba winston, llegué a usar pino que era más ligero, supongo que usaré pino, ya veremos. \ No newline at end of file +import pino from 'pino'; +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; \ No newline at end of file