nodejs-quickstart-structure 1.6.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +10 -8
  3. package/bin/index.js +1 -0
  4. package/docs/generateCase.md +127 -76
  5. package/docs/generatorFlow.md +15 -3
  6. package/docs/ruleDevelop.md +25 -0
  7. package/lib/generator.js +9 -2
  8. package/lib/modules/app-setup.js +7 -2
  9. package/lib/modules/caching-setup.js +56 -0
  10. package/lib/modules/config-files.js +30 -5
  11. package/lib/modules/database-setup.js +13 -1
  12. package/lib/prompts.js +10 -2
  13. package/package.json +1 -1
  14. package/templates/clean-architecture/ts/src/config/swagger.ts.ejs +1 -2
  15. package/templates/common/.env.example.ejs +39 -0
  16. package/templates/common/.gitlab-ci.yml.ejs +35 -0
  17. package/templates/common/Jenkinsfile.ejs +41 -0
  18. package/templates/common/README.md.ejs +121 -106
  19. package/templates/common/caching/clean/js/CreateUser.js.ejs +25 -0
  20. package/templates/common/caching/clean/js/GetAllUsers.js.ejs +33 -0
  21. package/templates/common/caching/clean/ts/createUser.ts.ejs +23 -0
  22. package/templates/common/caching/clean/ts/getAllUsers.ts.ejs +30 -0
  23. package/templates/common/caching/js/redisClient.js.ejs +71 -0
  24. package/templates/common/caching/ts/redisClient.ts.ejs +76 -0
  25. package/templates/common/docker-compose.yml.ejs +156 -139
  26. package/templates/common/package.json.ejs +4 -0
  27. package/templates/mvc/js/src/controllers/userController.js.ejs +22 -0
  28. package/templates/mvc/ts/src/config/swagger.ts.ejs +1 -2
  29. package/templates/mvc/ts/src/controllers/userController.ts.ejs +22 -0
  30. /package/templates/clean-architecture/js/src/interfaces/controllers/{UserController.js → userController.js} +0 -0
  31. /package/templates/db/mysql/{V1__Initial_Setup.sql → V1__Initial_Setup.sql.ejs} +0 -0
  32. /package/templates/db/postgres/{V1__Initial_Setup.sql → V1__Initial_Setup.sql.ejs} +0 -0
package/lib/prompts.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import inquirer from 'inquirer';
2
2
 
3
3
  const validateName = (name) => {
4
- return /^[a-zA-Z0-9-_]+$/.test(name) ? true : 'Project name may only include letters, numbers, underscores and hashes.';
4
+ return /^[a-zA-Z0-9-_]+$/.test(name) ? true : 'Project name may only include letters, numbers, underscores and dashes.';
5
5
  };
6
6
 
7
7
  export const getProjectDetails = async (options = {}) => {
@@ -62,11 +62,19 @@ export const getProjectDetails = async (options = {}) => {
62
62
  default: 'REST APIs',
63
63
  when: !options.communication
64
64
  },
65
+ {
66
+ type: 'list',
67
+ name: 'caching',
68
+ message: 'Caching Layer:',
69
+ choices: ['None', 'Redis'],
70
+ default: 'None',
71
+ when: (answers) => !options.caching && (options.database || answers.database) !== 'None'
72
+ },
65
73
  {
66
74
  type: 'list',
67
75
  name: 'ciProvider',
68
76
  message: 'Select CI/CD Provider:',
69
- choices: ['None', 'GitHub Actions', 'Jenkins'],
77
+ choices: ['None', 'GitHub Actions', 'Jenkins', 'GitLab CI'],
70
78
  default: 'None',
71
79
  when: !options.ciProvider
72
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-quickstart-structure",
3
- "version": "1.6.1",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "description": "A CLI to scaffold Node.js microservices with MVC or Clean Architecture",
6
6
  "main": "bin/index.js",
@@ -1,5 +1,4 @@
1
- <% if (communication === 'REST APIs') { %>
2
- import swaggerJsdoc from 'swagger-jsdoc';
1
+ <% if (communication === 'REST APIs') { %>import swaggerJsdoc from 'swagger-jsdoc';
3
2
 
4
3
  const options = {
5
4
  definition: {
@@ -0,0 +1,39 @@
1
+ # Application
2
+ PORT=3000
3
+ NODE_ENV=development
4
+
5
+ <%_ if (database !== 'None') { -%>
6
+ # Database
7
+ <%_ if (database === 'MySQL') { -%>
8
+ DB_HOST=localhost
9
+ DB_PORT=3306
10
+ DB_USER=root
11
+ DB_PASSWORD=root
12
+ DB_NAME=<%= dbName %>
13
+ <%_ } -%>
14
+ <%_ if (database === 'PostgreSQL') { -%>
15
+ DB_HOST=localhost
16
+ DB_PORT=5432
17
+ DB_USER=postgres
18
+ DB_PASSWORD=root
19
+ DB_NAME=<%= dbName %>
20
+ <%_ } -%>
21
+ <%_ if (database === 'MongoDB') { -%>
22
+ DB_URI=mongodb://localhost:27017/<%= dbName %>
23
+ <%_ } -%>
24
+ <%_ } -%>
25
+
26
+
27
+ <%_ if (communication === 'Kafka') { -%>
28
+ # Communication
29
+ KAFKA_BROKER=localhost:9092
30
+ KAFKA_CLIENT_ID=<%= projectName %>
31
+ KAFKA_GROUP_ID=<%= projectName %>-group
32
+ <%_ } -%>
33
+
34
+ <%_ if (caching === 'Redis') { -%>
35
+ # Caching
36
+ REDIS_HOST=localhost
37
+ REDIS_PORT=6379
38
+ REDIS_PASSWORD=
39
+ <%_ } -%>
@@ -0,0 +1,35 @@
1
+ variables:
2
+ NODE_ENV: 'test'
3
+
4
+ stages:
5
+ - lint
6
+ - test
7
+ - build
8
+
9
+ cache:
10
+ paths:
11
+ - node_modules/
12
+
13
+ install_dependencies:
14
+ stage: .pre
15
+ image: node:18-alpine
16
+ script:
17
+ - npm ci
18
+
19
+ lint_code:
20
+ stage: lint
21
+ image: node:18-alpine
22
+ script:
23
+ - npm run lint
24
+
25
+ run_tests:
26
+ stage: test
27
+ image: node:18-alpine
28
+ script:
29
+ - npm test
30
+
31
+ build_app:
32
+ stage: build
33
+ image: node:18-alpine
34
+ script:
35
+ - npm run build --if-present
@@ -24,6 +24,47 @@ pipeline {
24
24
  sh 'npm test'
25
25
  }
26
26
  }
27
+
28
+ // stage('Build') {
29
+ // steps {
30
+ // sh 'npm run build'
31
+ // }
32
+ // }
33
+
34
+ // stage('SonarQube Analysis') {
35
+ // environment {
36
+ // scannerHome = tool 'SonarScanner'
37
+ // }
38
+ // steps {
39
+ // withSonarQubeEnv('SonarQube') {
40
+ // sh "${scannerHome}/bin/sonar-scanner"
41
+ // }
42
+ // }
43
+ // }
44
+
45
+ // stage('Security Scan') {
46
+ // steps {
47
+ // sh 'npm audit --audit-level=high'
48
+ // }
49
+ // }
50
+
51
+ // stage('Docker Build & Push') {
52
+ // steps {
53
+ // script {
54
+ // docker.withRegistry('https://registry.hub.docker.com', 'docker-hub-credentials') {
55
+ // def appImage = docker.build("my-image:${env.BUILD_ID}")
56
+ // appImage.push()
57
+ // appImage.push("latest")
58
+ // }
59
+ // }
60
+ // }
61
+ // }
62
+
63
+ // stage('Deploy to Staging') {
64
+ // steps {
65
+ // sh './scripts/deploy.sh staging'
66
+ // }
67
+ // }
27
68
  }
28
69
 
29
70
  post {
@@ -1,107 +1,122 @@
1
- # <%= projectName %>
2
-
3
- ![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)
4
- ![License](https://img.shields.io/badge/License-ISC-blue.svg)
5
- <% if (language === 'TypeScript') { %>![TypeScript](https://img.shields.io/badge/Language-TypeScript-blue.svg)<% } else { %>![JavaScript](https://img.shields.io/badge/Language-JavaScript-yellow.svg)<% } %>
6
-
7
- A production-ready Node.js microservice generated with **<%= architecture %>** and **<%= database %>**.
8
- This project comes pre-configured with industry-standard tooling for **Code Quality**, **Testing**, and **Security**.
9
-
10
- ## 🚀 Key Features
11
-
12
- - **Architecture**: <%= architecture %> (<% if (architecture === 'Clean Architecture') { %>Domain, UseCases, Infrastructure<% } else { %>MVC Pattern<% } %>).
13
- - **Database**: <%= database %> <% if (database !== 'MongoDB') { %>with **Flyway** migrations<% } else { %>with **Mongoose** schemas<% } %>.
14
- - **Security**: Helmet, CORS, Rate Limiting, HPP.
15
- - **Quality**: Eslint, Prettier, Husky, Lint-Staged.
16
- - **Testing**: Jest (Unit & Integration).
17
- - **DevOps**: Multi-stage Docker build, CI/CD ready.
18
-
19
- ## 🔄 CI/CD Pipeline
20
- <% if (ciProvider === 'GitHub Actions') { -%>
21
- This project includes a **GitHub Actions** workflow located in `.github/workflows/ci.yml`.
22
- It automatically runs:
23
- - Linting
24
- - Tests
25
- - Builds
26
- <% } else if (ciProvider === 'Jenkins') { -%>
27
- This project includes a **Jenkinsfile** for comprehensive CI/CD.
28
- Pipeline stages:
29
- - Install Dependencies
30
- - Lint
31
- - Test
32
- <% } else { -%>
33
- CI/CD is not currently configured, but the project is ready for integration.
34
- <% } -%>
35
-
36
- ## 🛠️ Getting Started
37
-
38
- ### 1. Prerequisites
39
- - Node.js (v18+)
40
- - Docker & Docker Compose
41
-
42
- ### 2. Quick Start
43
- ```bash
44
- # Initialize Git (Required for Husky)
45
- git init
46
-
47
- # Install dependencies
48
- npm install
49
-
50
- # Setup Git Hooks (Husky)
51
- npm run prepare
52
-
53
- # Start Infrastructure (DB, etc.)
54
- docker-compose up -d
55
-
56
- # Run Development Server
57
- npm run dev
58
- ```
59
-
60
- ### 3. Development Standards
61
- Ensure your code meets quality standards before committing:
62
-
63
- ```bash
64
- # Run Linter
65
- npm run lint
66
-
67
- # Run Tests
68
- npm test
69
-
70
- # Format Code
71
- npm run format
72
- ```
73
-
74
- ## 📂 Project Structure
75
-
76
- The project follows **<%= architecture %>** principles.
77
- <% if (communication === 'Kafka') { -%>
78
- Microservices communication handled via **Kafka**.
79
- <% } else { -%>
80
- API is exposed via **REST**.
81
- A Swagger UI for API documentation is available at:
82
- - **URL**: `http://localhost:3000/api-docs` (Dynamic based on PORT)
83
- <% } -%>
84
-
85
- ## 📝 Logging
86
- This project uses **Winston** for structured logging.
87
- - **Development**: Logs are printed to the console.
88
- - **Production**: Logs are saved to files:
89
- - `error.log`: Only error level logs.
90
- - `combined.log`: All logs.
91
-
92
- ## 🐳 Docker Deployment
93
- This project uses a **Multi-Stage Dockerfile** for optimized production images.
94
-
95
- ```bash
96
- # Build Production Image
97
- docker build -t <%= projectName %> .
98
-
99
- # Run Container
100
- docker run -p 3000:3000 -e DATABASE_URL=... <%= projectName %>
101
- ```
102
-
103
- ## 🔒 Security Features
104
- - **Helmet**: Sets secure HTTP headers.
105
- - **CORS**: Configured for cross-origin requests.
106
- - **Rate Limiting**: Protects against DDoS / Brute-force.
1
+ # <%= projectName %>
2
+
3
+ ![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)
4
+ ![License](https://img.shields.io/badge/License-ISC-blue.svg)
5
+ <% if (language === 'TypeScript') { %>![TypeScript](https://img.shields.io/badge/Language-TypeScript-blue.svg)<% } else { %>![JavaScript](https://img.shields.io/badge/Language-JavaScript-yellow.svg)<% } %>
6
+
7
+ A production-ready Node.js microservice generated with **<%= architecture %>** and **<%= database %>**.
8
+ This project comes pre-configured with industry-standard tooling for **Code Quality**, **Testing**, and **Security**.
9
+
10
+ ## 🚀 Key Features
11
+
12
+ - **Architecture**: <%= architecture %> (<% if (architecture === 'Clean Architecture') { %>Domain, UseCases, Infrastructure<% } else { %>MVC Pattern<% } %>).
13
+ - **Database**: <%= database %> <% if (database !== 'MongoDB') { %>with **Flyway** migrations<% } else { %>with **Mongoose** schemas<% } %>.
14
+ - **Security**: Helmet, CORS, Rate Limiting, HPP.
15
+ - **Quality**: Eslint, Prettier, Husky, Lint-Staged.
16
+ - **Testing**: Jest (Unit & Integration).
17
+ - **DevOps**: Multi-stage Docker build, CI/CD ready.
18
+
19
+ ## 🔄 CI/CD Pipeline
20
+ <%_ if (ciProvider === 'GitHub Actions') { -%>
21
+ ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/yourusername/<%= projectName %>/ci.yml?branch=main)
22
+ This project includes a **GitHub Actions** workflow located in `.github/workflows/ci.yml`.
23
+ It automatically runs:
24
+ - Linting
25
+ - Tests
26
+ - Builds
27
+ <%_ } else if (ciProvider === 'GitLab CI') { -%>
28
+ ![GitLab CI Status](https://img.shields.io/gitlab/pipeline/yourusername/<%= projectName %>?branch=main)
29
+ This project includes a **GitLab CI** configuration in `.gitlab-ci.yml`.
30
+ It automatically runs:
31
+ - Linting
32
+ - Tests
33
+ - Builds
34
+ <% } else if (ciProvider === 'Jenkins') { -%>
35
+ This project includes a **Jenkinsfile** for comprehensive CI/CD.
36
+ Pipeline stages:
37
+ - Install Dependencies
38
+ - Lint
39
+ - Test
40
+ <% } else { -%>
41
+ CI/CD is not currently configured, but the project is ready for integration.
42
+ <% } -%>
43
+
44
+ ## 🛠️ Getting Started
45
+
46
+ ### 1. Prerequisites
47
+ - Node.js (v18+)
48
+ - Docker & Docker Compose
49
+
50
+ ### 2. Quick Start
51
+ ```bash
52
+ # Initialize Git (Required for Husky)
53
+ git init
54
+
55
+ # Install dependencies
56
+ npm install
57
+
58
+ # Setup Git Hooks (Husky)
59
+ npm run prepare
60
+
61
+ # Start Infrastructure (DB, etc.)
62
+ docker-compose up -d
63
+
64
+ # Run Development Server
65
+ npm run dev
66
+ ```
67
+
68
+ ### 3. Development Standards
69
+ Ensure your code meets quality standards before committing:
70
+
71
+ ```bash
72
+ # Run Linter
73
+ npm run lint
74
+
75
+ # Run Tests
76
+ npm test
77
+
78
+ # Format Code
79
+ npm run format
80
+ ```
81
+
82
+ ## 📂 Project Structure
83
+
84
+ The project follows **<%= architecture %>** principles.
85
+ <% if (communication === 'Kafka') { -%>
86
+ Microservices communication handled via **Kafka**.
87
+ <% } else { -%>
88
+ API is exposed via **REST**.
89
+ A Swagger UI for API documentation is available at:
90
+ - **URL**: `http://localhost:3000/api-docs` (Dynamic based on PORT)
91
+ <% } -%>
92
+
93
+ <% if (caching === 'Redis') { -%>
94
+ ## ⚡ Caching
95
+ This project uses **Redis** for caching.
96
+ - **Client**: `ioredis`
97
+ - **Connection**: Configured via `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD` in `.env`.
98
+ <% } -%>
99
+
100
+ ## 📝 Logging
101
+ This project uses **Winston** for structured logging.
102
+ - **Development**: Logs are printed to the console.
103
+ - **Production**: Logs are saved to files:
104
+ - `error.log`: Only error level logs.
105
+ - `combined.log`: All logs.
106
+
107
+ ## 🐳 Docker Deployment
108
+ This project uses a **Multi-Stage Dockerfile** for optimized production images.
109
+
110
+ ```bash
111
+ # Build Production Image
112
+ docker build -t <%= projectName %> .
113
+
114
+ # Run Container
115
+ docker run -p 3000:3000 -e DATABASE_URL=... <%= projectName %>
116
+ ```
117
+
118
+ ## 🔒 Security Features
119
+ - **Helmet**: Sets secure HTTP headers.
120
+ - **CORS**: Configured for cross-origin requests.
121
+ - **Rate Limiting**: Protects against DDoS / Brute-force.
107
122
  - **HPP**: Prevents HTTP Parameter Pollution attacks.
@@ -0,0 +1,25 @@
1
+ const User = require('../domain/models/User');
2
+ const redis = require('../infrastructure/caching/redisClient');
3
+ const logger = require('../infrastructure/log/logger');
4
+
5
+ class CreateUser {
6
+ constructor(userRepository) {
7
+ this.userRepository = userRepository;
8
+ }
9
+
10
+ async execute(name, email) {
11
+ const user = new User(null, name, email);
12
+ const savedUser = await this.userRepository.save(user);
13
+
14
+ try {
15
+ await redis.del('users:all');
16
+ logger.info('Invalidated users:all cache');
17
+ } catch (error) {
18
+ logger.error('Redis error (del):', error);
19
+ }
20
+
21
+ return savedUser;
22
+ }
23
+ }
24
+
25
+ module.exports = CreateUser;
@@ -0,0 +1,33 @@
1
+ const redis = require('../infrastructure/caching/redisClient');
2
+ const logger = require('../infrastructure/log/logger');
3
+
4
+ class GetAllUsers {
5
+ constructor(userRepository) {
6
+ this.userRepository = userRepository;
7
+ }
8
+
9
+ async execute() {
10
+ const cacheKey = 'users:all';
11
+ try {
12
+ const cachedUsers = await redis.get(cacheKey);
13
+ if (cachedUsers) {
14
+ logger.info('Serving users from cache');
15
+ return cachedUsers;
16
+ }
17
+ } catch (error) {
18
+ logger.error('Redis error (get):', error);
19
+ }
20
+
21
+ const users = await this.userRepository.getUsers();
22
+
23
+ try {
24
+ await redis.set(cacheKey, users, 60);
25
+ } catch (error) {
26
+ logger.error('Redis error (set):', error);
27
+ }
28
+
29
+ return users;
30
+ }
31
+ }
32
+
33
+ module.exports = GetAllUsers;
@@ -0,0 +1,23 @@
1
+ import { User } from '@/domain/user';
2
+
3
+ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
4
+ import redis from '@/infrastructure/caching/redisClient';
5
+ import logger from '@/infrastructure/log/logger';
6
+
7
+ export default class CreateUser {
8
+ constructor(private userRepository: UserRepository) {}
9
+
10
+ async execute(name: string, email: string) {
11
+ const user = new User(null, name, email);
12
+ const savedUser = await this.userRepository.save(user);
13
+
14
+ try {
15
+ await redis.del('users:all');
16
+ logger.info('Invalidated users:all cache');
17
+ } catch (error) {
18
+ logger.error('Redis error (del):', error);
19
+ }
20
+
21
+ return savedUser;
22
+ }
23
+ }
@@ -0,0 +1,30 @@
1
+ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
2
+ import redis from '@/infrastructure/caching/redisClient';
3
+ import logger from '@/infrastructure/log/logger';
4
+
5
+ export default class GetAllUsers {
6
+ constructor(private userRepository: UserRepository) {}
7
+
8
+ async execute() {
9
+ const cacheKey = 'users:all';
10
+ try {
11
+ const cachedUsers = await redis.get(cacheKey);
12
+ if (cachedUsers) {
13
+ logger.info('Serving users from cache');
14
+ return cachedUsers;
15
+ }
16
+ } catch (error) {
17
+ logger.error('Redis error (get):', error);
18
+ }
19
+
20
+ const users = await this.userRepository.getUsers();
21
+
22
+ try {
23
+ await redis.set(cacheKey, users, 60);
24
+ } catch (error) {
25
+ logger.error('Redis error (set):', error);
26
+ }
27
+
28
+ return users;
29
+ }
30
+ }
@@ -0,0 +1,71 @@
1
+ const Redis = require('ioredis');
2
+ require('dotenv').config();
3
+ const logger = require('<%- loggerPath %>');
4
+
5
+ class RedisService {
6
+ constructor() {
7
+ this.client = new Redis({
8
+ host: process.env.REDIS_HOST || 'localhost',
9
+ port: Number(process.env.REDIS_PORT) || 6379,
10
+ password: process.env.REDIS_PASSWORD || undefined,
11
+ retryStrategy: (times) => Math.min(times * 50, 2000),
12
+ });
13
+
14
+ this.client.on('connect', () => {
15
+ logger.info('Redis connected');
16
+ });
17
+
18
+ this.client.on('error', (err) => {
19
+ logger.error('Redis error:', err);
20
+ });
21
+ }
22
+
23
+ static getInstance() {
24
+ if (!RedisService.instance) {
25
+ RedisService.instance = new RedisService();
26
+ }
27
+ return RedisService.instance;
28
+ }
29
+
30
+ async get(key) {
31
+ try {
32
+ const data = await this.client.get(key);
33
+ return data ? JSON.parse(data) : null;
34
+ } catch (error) {
35
+ logger.error(`Redis get error for key ${key}:`, error);
36
+ return null;
37
+ }
38
+ }
39
+
40
+ async set(key, value, ttl) {
41
+ try {
42
+ const data = JSON.stringify(value);
43
+ if (ttl) {
44
+ await this.client.set(key, data, 'EX', ttl);
45
+ } else {
46
+ await this.client.set(key, data);
47
+ }
48
+ } catch (error) {
49
+ logger.error(`Redis set error for key ${key}:`, error);
50
+ }
51
+ }
52
+
53
+ async del(key) {
54
+ try {
55
+ await this.client.del(key);
56
+ } catch (error) {
57
+ logger.error(`Redis del error for key ${key}:`, error);
58
+ }
59
+ }
60
+
61
+ async getOrSet(key, fetcher, ttl = 3600) {
62
+ const cached = await this.get(key);
63
+ if (cached) return cached;
64
+
65
+ const data = await fetcher();
66
+ if (data) await this.set(key, data, ttl);
67
+ return data;
68
+ }
69
+ }
70
+
71
+ module.exports = RedisService.getInstance();
@@ -0,0 +1,76 @@
1
+ import Redis from 'ioredis';
2
+ import dotenv from 'dotenv';
3
+ import logger from '<%- loggerPath %>';
4
+
5
+ dotenv.config();
6
+
7
+ class RedisService {
8
+ private client: Redis;
9
+ private static instance: RedisService;
10
+
11
+ private constructor() {
12
+ this.client = new Redis({
13
+ host: process.env.REDIS_HOST || 'localhost',
14
+ port: Number(process.env.REDIS_PORT) || 6379,
15
+ password: process.env.REDIS_PASSWORD || undefined,
16
+ retryStrategy: (times) => Math.min(times * 50, 2000),
17
+ });
18
+
19
+ this.client.on('connect', () => {
20
+ logger.info('Redis connected');
21
+ });
22
+
23
+ this.client.on('error', (err) => {
24
+ logger.error('Redis error:', err);
25
+ });
26
+ }
27
+
28
+ public static getInstance(): RedisService {
29
+ if (!RedisService.instance) {
30
+ RedisService.instance = new RedisService();
31
+ }
32
+ return RedisService.instance;
33
+ }
34
+
35
+ public async get<T>(key: string): Promise<T | null> {
36
+ try {
37
+ const data = await this.client.get(key);
38
+ return data ? JSON.parse(data) : null;
39
+ } catch (error) {
40
+ logger.error(`Redis get error for key ${key}:`, error);
41
+ return null;
42
+ }
43
+ }
44
+
45
+ public async set(key: string, value: unknown, ttl?: number): Promise<void> {
46
+ try {
47
+ const data = JSON.stringify(value);
48
+ if (ttl) {
49
+ await this.client.set(key, data, 'EX', ttl);
50
+ } else {
51
+ await this.client.set(key, data);
52
+ }
53
+ } catch (error) {
54
+ logger.error(`Redis set error for key ${key}:`, error);
55
+ }
56
+ }
57
+
58
+ public async del(key: string): Promise<void> {
59
+ try {
60
+ await this.client.del(key);
61
+ } catch (error) {
62
+ logger.error(`Redis del error for key ${key}:`, error);
63
+ }
64
+ }
65
+
66
+ public async getOrSet<T>(key: string, fetcher: () => Promise<T>, ttl: number = 3600): Promise<T> {
67
+ const cached = await this.get<T>(key);
68
+ if (cached) return cached;
69
+
70
+ const data = await fetcher();
71
+ if (data) await this.set(key, data, ttl);
72
+ return data;
73
+ }
74
+ }
75
+
76
+ export default RedisService.getInstance();