@yellowpanther/shared 1.0.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.
- package/README.md +21 -0
- package/package.json +25 -0
- package/src/config/index.js +168 -0
- package/src/index.js +13 -0
- package/src/queue/compressionQueue.js +34 -0
- package/src/queue/index.js +8 -0
- package/src/queue/newsTranslateQueue.js +29 -0
- package/src/queue/queueRegistry.js +15 -0
- package/src/redis/redisClient.js +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @yellowpanther/shared
|
|
2
|
+
|
|
3
|
+
Reusable shared utilities for Yellow Panther CMS microservices.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Redis client setup for BullMQ queues
|
|
8
|
+
- Queue registry for background jobs
|
|
9
|
+
- Config loader (env-based)
|
|
10
|
+
- Common Sequelize DB client
|
|
11
|
+
- Auth utilities (JWT with HttpOnly cookies)
|
|
12
|
+
- Common middleware (logger, error handler)
|
|
13
|
+
- Shared Sequelize models (User, Tag, Category, Language, etc.)
|
|
14
|
+
- Common API routes (Tag, Category, Language)
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### Install locally (for development)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm link
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yellowpanther/shared",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Reusable shared utilities for YP microservices (e.g., redis, queue, config)",
|
|
5
|
+
"author": "Yellow Panther",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"package.json",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
"./config": "./src/config/index.js",
|
|
15
|
+
"./redis/redisClient": "./src/redis/redisClient.js",
|
|
16
|
+
"./queue/compressionQueue": "./src/queue/compressionQueue.js",
|
|
17
|
+
"./queue/newsTranslationQueue": "./src/queue/newsTranslationQueue.js",
|
|
18
|
+
"./queue/queueRegistry": "./src/queue/queueRegistry.js"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"lint": "eslint .",
|
|
22
|
+
"format": "prettier --write ."
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT"
|
|
25
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const dotenv = require('dotenv');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const Joi = require('joi');
|
|
4
|
+
|
|
5
|
+
dotenv.config({ path: path.join(__dirname, '../../.env') });
|
|
6
|
+
|
|
7
|
+
const envValidation = Joi.object()
|
|
8
|
+
.keys({
|
|
9
|
+
APP_NAME: Joi.string().default('ip-cms'),
|
|
10
|
+
NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
|
|
11
|
+
PORT: Joi.number().default(4006),
|
|
12
|
+
DB_HOST: Joi.string().default('localhost'),
|
|
13
|
+
DB_USER: Joi.string().required(),
|
|
14
|
+
DB_PASS: Joi.string().required(),
|
|
15
|
+
DB_NAME: Joi.string().required(),
|
|
16
|
+
JWT_SECRET: Joi.string().required().description('JWT secret key'),
|
|
17
|
+
JWT_ACCESS_EXPIRATION_MINUTES: Joi.number().default(30),
|
|
18
|
+
JWT_REFRESH_EXPIRATION_DAYS: Joi.number().default(30),
|
|
19
|
+
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: Joi.number().default(10),
|
|
20
|
+
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: Joi.number().default(10),
|
|
21
|
+
JWT_2FA_EXPIRATION_MINUTES: Joi.number().default(30),
|
|
22
|
+
LOG_FOLDER: Joi.string().required(),
|
|
23
|
+
LOG_FILE: Joi.string().required(),
|
|
24
|
+
LOG_LEVEL: Joi.string().required(),
|
|
25
|
+
|
|
26
|
+
REDIS_HOST: Joi.string().default('127.0.0.1'),
|
|
27
|
+
REDIS_PORT: Joi.number().default(6379),
|
|
28
|
+
REDIS_USE_PASSWORD: Joi.string().default('no'),
|
|
29
|
+
REDIS_PASSWORD: Joi.string(),
|
|
30
|
+
|
|
31
|
+
SMTP_HOST: Joi.string(),
|
|
32
|
+
SMTP_PORT: Joi.string(),
|
|
33
|
+
SMTP_SECURE: Joi.string(),
|
|
34
|
+
SMTP_USER: Joi.string(),
|
|
35
|
+
SMTP_PASS: Joi.string(),
|
|
36
|
+
SMTP_FROM_NAME: Joi.string(),
|
|
37
|
+
SMTP_FROM_EMAIL: Joi.string(),
|
|
38
|
+
FRONTEND_BASE_URL: Joi.string(),
|
|
39
|
+
|
|
40
|
+
COOKIE_SECURE: Joi.string().valid('true', 'false').default('false'),
|
|
41
|
+
COOKIE_SAME_SITE: Joi.string().valid('Strict', 'Lax', 'None').default('Strict'),
|
|
42
|
+
COOKIE_DOMAIN: Joi.string().default('localhost'),
|
|
43
|
+
ACCESS_TOKEN_COOKIE_EXPIRE: Joi.number().default(3600000),
|
|
44
|
+
REFRESH_TOKEN_COOKIE_EXPIRE: Joi.number().default(604800000),
|
|
45
|
+
|
|
46
|
+
STORAGE_DRIVER: Joi.string().required(),
|
|
47
|
+
LOCAL_UPLOAD_PATH: Joi.string().required(),
|
|
48
|
+
AWS_ACCESS_KEY_ID: Joi.string().required(),
|
|
49
|
+
AWS_SECRET_ACCESS_KEY: Joi.string().required(),
|
|
50
|
+
AWS_REGION: Joi.string().required(),
|
|
51
|
+
AWS_S3_BUCKET_NAME: Joi.string().required(),
|
|
52
|
+
MEDIA_BASE_URL: Joi.string().required(),
|
|
53
|
+
|
|
54
|
+
COMPRESSION_PROFILE: Joi.string().valid('low', 'medium', 'high').default('medium'),
|
|
55
|
+
IMAGE_COMPRESSION_THRESHOLD: Joi.number().default(300 * 1024),
|
|
56
|
+
IMAGE_COMPRESSION_QUALITY: Joi.number(),
|
|
57
|
+
IMAGE_MAX_WIDTH: Joi.number(),
|
|
58
|
+
IMAGE_KEEP_ORIGINAL_EXTENSION: Joi.boolean().default(true),
|
|
59
|
+
VIDEO_COMPRESSION_THRESHOLD: Joi.number().default(1024 * 1024),
|
|
60
|
+
VIDEO_COMPRESSION_CRF_LOW: Joi.number().default(30),
|
|
61
|
+
VIDEO_COMPRESSION_CRF_MEDIUM: Joi.number().default(28),
|
|
62
|
+
VIDEO_COMPRESSION_CRF_HIGH: Joi.number().default(24),
|
|
63
|
+
PDF_COMPRESSION_ENABLED: Joi.boolean().default(true),
|
|
64
|
+
PDF_COMPRESSION_QUALITY: Joi.string().default('ebook')
|
|
65
|
+
|
|
66
|
+
})
|
|
67
|
+
.unknown();
|
|
68
|
+
|
|
69
|
+
const { value: envVar, error } = envValidation
|
|
70
|
+
.prefs({ errors: { label: 'key' }, convert: true })
|
|
71
|
+
.validate(process.env);
|
|
72
|
+
|
|
73
|
+
if (error) {
|
|
74
|
+
throw new Error(`Config validation error: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const profile = envVar.COMPRESSION_PROFILE || 'medium';
|
|
78
|
+
|
|
79
|
+
const imagePresets = {
|
|
80
|
+
low: { quality: envVar.COMPRESSION_IMAGE_LOW_QUALITY, maxWidth: envVar.COMPRESSION_IMAGE_LOW_WIDTH },
|
|
81
|
+
medium: { quality: envVar.COMPRESSION_IMAGE_MEDIUM_QUALITY, maxWidth: envVar.COMPRESSION_IMAGE_MEDIUM_WIDTH },
|
|
82
|
+
high: { quality: envVar.COMPRESSION_IMAGE_HIGH_QUALITY, maxWidth: envVar.COMPRESSION_IMAGE_HIGH_WIDTH }
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const videoPresets = {
|
|
86
|
+
low: envVar.VIDEO_COMPRESSION_CRF_LOW,
|
|
87
|
+
medium: envVar.VIDEO_COMPRESSION_CRF_MEDIUM,
|
|
88
|
+
high: envVar.VIDEO_COMPRESSION_CRF_HIGH
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
name: process.env.APP_NAME || 'ip-cms',
|
|
93
|
+
nodeEnv: envVar.NODE_ENV,
|
|
94
|
+
port: envVar.PORT,
|
|
95
|
+
dbHost: envVar.DB_HOST,
|
|
96
|
+
dbUser: envVar.DB_USER,
|
|
97
|
+
dbPass: envVar.DB_PASS,
|
|
98
|
+
dbName: envVar.DB_NAME,
|
|
99
|
+
app: {
|
|
100
|
+
name: process.env.APP_NAME || 'ip-cms',
|
|
101
|
+
env: process.env.NODE_ENV || 'development',
|
|
102
|
+
port: process.env.PORT || 3000
|
|
103
|
+
},
|
|
104
|
+
jwt: {
|
|
105
|
+
secret: envVar.JWT_SECRET,
|
|
106
|
+
accessExpirationMinutes: envVar.JWT_ACCESS_EXPIRATION_MINUTES,
|
|
107
|
+
refreshExpirationDays: envVar.JWT_REFRESH_EXPIRATION_DAYS,
|
|
108
|
+
resetPasswordExpirationMinutes: envVar.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
|
|
109
|
+
verifyEmailExpirationMinutes: envVar.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
|
|
110
|
+
twoFAExpirationMinutes: envVar.JWT_2FA_EXPIRATION_MINUTES,
|
|
111
|
+
},
|
|
112
|
+
logConfig: {
|
|
113
|
+
logFolder: envVar.LOG_FOLDER,
|
|
114
|
+
logFile: envVar.LOG_FILE,
|
|
115
|
+
logLevel: envVar.LOG_LEVEL,
|
|
116
|
+
},
|
|
117
|
+
redis: {
|
|
118
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
119
|
+
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
|
120
|
+
password: process.env.REDIS_PASSWORD || '',
|
|
121
|
+
usePassword: process.env.REDIS_USE_PASSWORD || 'NO'
|
|
122
|
+
},
|
|
123
|
+
smtp: {
|
|
124
|
+
host: process.env.SMTP_HOST,
|
|
125
|
+
port: process.env.SMTP_PORT,
|
|
126
|
+
secure: process.env.SMTP_SECURE === 'true',
|
|
127
|
+
user: process.env.SMTP_USER,
|
|
128
|
+
pass: process.env.SMTP_PASS,
|
|
129
|
+
fromName: process.env.SMTP_FROM_NAME,
|
|
130
|
+
fromEmail: process.env.SMTP_FROM_EMAIL
|
|
131
|
+
},
|
|
132
|
+
frontendBaseUrl: process.env.FRONTEND_BASE_URL || 'http://localhost:4001',
|
|
133
|
+
cookie: {
|
|
134
|
+
secure: envVar.COOKIE_SECURE === 'true',
|
|
135
|
+
sameSite: envVar.COOKIE_SAME_SITE || 'None',
|
|
136
|
+
domain: '', // envVar.COOKIE_DOMAIN || '.ypstagingserver.com',
|
|
137
|
+
accessExpire: parseInt(envVar.ACCESS_TOKEN_COOKIE_EXPIRE) || 60 * 60 * 1000, // 1 hour
|
|
138
|
+
refreshExpire: parseInt(envVar.REFRESH_TOKEN_COOKIE_EXPIRE) || 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
139
|
+
},
|
|
140
|
+
storage: {
|
|
141
|
+
driver: envVar.STORAGE_DRIVER || 'local', // 's3' or 'local',
|
|
142
|
+
localUploadPath: envVar.LOCAL_UPLOAD_PATH || 'uploads/',
|
|
143
|
+
mediaBaseUrl: envVar.MEDIA_BASE_URL,
|
|
144
|
+
s3: {
|
|
145
|
+
accessKeyId: envVar.AWS_ACCESS_KEY_ID,
|
|
146
|
+
secretAccessKey: envVar.AWS_SECRET_ACCESS_KEY,
|
|
147
|
+
region: envVar.AWS_REGION,
|
|
148
|
+
bucket: envVar.AWS_S3_BUCKET_NAME,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
compression: {
|
|
152
|
+
profile,
|
|
153
|
+
image: {
|
|
154
|
+
quality: parseInt(imagePresets[profile].quality, 10),
|
|
155
|
+
maxWidth: parseInt(imagePresets[profile].maxWidth, 10),
|
|
156
|
+
threshold: parseInt(envVar.IMAGE_COMPRESSION_THRESHOLD, 10),
|
|
157
|
+
keepOriginalExtension: envVar.IMAGE_KEEP_ORIGINAL_EXTENSION === true || envVar.IMAGE_KEEP_ORIGINAL_EXTENSION === 'true',
|
|
158
|
+
},
|
|
159
|
+
video: {
|
|
160
|
+
crf: parseInt(videoPresets[profile], 10),
|
|
161
|
+
threshold: parseInt(envVar.VIDEO_COMPRESSION_THRESHOLD, 10),
|
|
162
|
+
},
|
|
163
|
+
pdf: {
|
|
164
|
+
enabled: envVar.PDF_COMPRESSION_ENABLED === true || envVar.PDF_COMPRESSION_ENABLED === 'true',
|
|
165
|
+
quality: envVar.PDF_COMPRESSION_QUALITY
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
// ✅ Redis
|
|
3
|
+
redisClient: require('./redis/redisClient'),
|
|
4
|
+
|
|
5
|
+
// ✅ Config
|
|
6
|
+
config: require('./config'),
|
|
7
|
+
|
|
8
|
+
// ✅ Queues
|
|
9
|
+
compressionQueue: require('./queue/compressionQueue'),
|
|
10
|
+
addCompressionJob: require('./queue/compressionQueue').addCompressionJob,
|
|
11
|
+
newsTranslateQueue: require('./queue/newsTranslateQueue'),
|
|
12
|
+
addNewsTranslationJob: require('./queue/newsTranslateQueue').addNewsTranslationJob
|
|
13
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const { Queue } = require('bullmq');
|
|
2
|
+
const redisClient = require('../redis/redisClient');
|
|
3
|
+
|
|
4
|
+
const compressionQueue = new Queue('file-compression-queue', {
|
|
5
|
+
connection: redisClient,
|
|
6
|
+
defaultJobOptions: {
|
|
7
|
+
removeOnComplete: {
|
|
8
|
+
age: 3600, // remove jobs after 1 hour
|
|
9
|
+
count: 1000 // or keep latest 1000
|
|
10
|
+
},
|
|
11
|
+
removeOnFail: {
|
|
12
|
+
age: 86400 // remove failed jobs after 24h
|
|
13
|
+
},
|
|
14
|
+
attempts: 3,
|
|
15
|
+
backoff: {
|
|
16
|
+
type: 'exponential',
|
|
17
|
+
delay: 5000
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Add a compression job
|
|
24
|
+
* @param {Object} jobData
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
async function addCompressionJob(jobData) {
|
|
28
|
+
await compressionQueue.add('compress-file', jobData);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
compressionQueue,
|
|
33
|
+
addCompressionJob
|
|
34
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { Queue } = require('bullmq');
|
|
2
|
+
const redisClient = require('../redis/redisClient');
|
|
3
|
+
|
|
4
|
+
const newsTranslationQueue = new Queue('newsTranslationQueue', {
|
|
5
|
+
connection: redisClient,
|
|
6
|
+
defaultJobOptions: {
|
|
7
|
+
removeOnComplete: false,
|
|
8
|
+
removeOnFail: false,
|
|
9
|
+
attempts: 3,
|
|
10
|
+
backoff: {
|
|
11
|
+
type: 'exponential',
|
|
12
|
+
delay: 1000
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Add a news translation job
|
|
19
|
+
* @param {Object} jobData
|
|
20
|
+
* @returns {Promise<void>}
|
|
21
|
+
*/
|
|
22
|
+
async function addNewsTranslationJob({ news_id, user_id, translation, blocks }) {
|
|
23
|
+
await newsTranslationQueue.add('translateNews', { news_id, user_id, translation, blocks });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
newsTranslationQueue,
|
|
28
|
+
addNewsTranslationJob
|
|
29
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { compressionQueue } = require('../queue/compressionQueue');
|
|
2
|
+
const { newsTranslationQueue } = require('./newsTranslateQueue');
|
|
3
|
+
// You can register more queues here like translationQueue, videoProcessingQueue, etc.
|
|
4
|
+
|
|
5
|
+
module.exports = [
|
|
6
|
+
{
|
|
7
|
+
name: 'compressionQueue',
|
|
8
|
+
instance: compressionQueue
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'newsTranslationQueue',
|
|
12
|
+
instance: newsTranslationQueue
|
|
13
|
+
}
|
|
14
|
+
// Add more { name, instance } here
|
|
15
|
+
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const Redis = require('ioredis');
|
|
2
|
+
require('dotenv').config();
|
|
3
|
+
|
|
4
|
+
const redisClient = new Redis(process.env.REDIS_URL, {
|
|
5
|
+
maxRetriesPerRequest: null,
|
|
6
|
+
enableReadyCheck: true
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
redisClient.on('connect', () => {
|
|
10
|
+
console.log('✅ Shared Redis client connected');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
redisClient.on('error', err => {
|
|
14
|
+
console.error('❌ Redis connection error:', err);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
module.exports = redisClient;
|