@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 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,8 @@
1
+ const { compressionQueue } = require('./compressionQueue');
2
+
3
+ const allQueues = [compressionQueue];
4
+
5
+ module.exports = {
6
+ allQueues,
7
+ queueAdapters: allQueues.map(q => ({ name: q.name, bullMqAdapter: q }))
8
+ };
@@ -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;