@ecashlib/shared-message 1.0.0 → 1.0.2

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 CHANGED
@@ -4,15 +4,16 @@ Shared utilities for Ecash microservices - Message handling, i18n, error helpers
4
4
 
5
5
  ## Features
6
6
 
7
- - **i18n Message System**: Get localized messages from database with caching
7
+ - **i18n Message System**: Get localized messages from database with **Redis caching**
8
8
  - **Parameter Replacement**: Dynamic message templates with `{placeholder}` support
9
9
  - **Language Detection**: Auto-detect language from request headers or query params
10
10
  - **Error Helpers**: Throw errors with dynamic, localized messages
11
+ - **Framework Agnostic**: Works with any Redis implementation via `IRedisCache` interface
11
12
 
12
13
  ## Installation
13
14
 
14
15
  ```bash
15
- npm install @ecash/shared
16
+ npm install @ecashlib/shared-message
16
17
  ```
17
18
 
18
19
  ## Setup
@@ -33,149 +34,298 @@ Create messages in your `messages` collection:
33
34
  }
34
35
  ```
35
36
 
36
- ### 2. Implement MessageServicePort
37
-
38
- Each microservice implements the message service:
37
+ ### 2. Implement Database Service (WITHOUT cache)
39
38
 
40
39
  ```typescript
41
- // ecash-marketplace/src/application/services/message/get-message-content.service.ts
42
- import { MessageServicePort } from "@ecash/shared";
40
+ // ecash-marketplace/src/infrastructure/adapters/outbound/message/message.repository.ts
41
+ import { MessageServicePort } from "@ecashlib/shared-message";
43
42
 
44
- export class GetMessageContentService implements MessageServicePort {
43
+ export class MessageRepository implements MessageServicePort {
45
44
  async getMessageContentByCode(code: string, language: string) {
46
- // Your implementation - query DB/Redis
47
- const cached = await redis.get(`message:${code}:${language}`);
48
- if (cached) return JSON.parse(cached);
49
-
45
+ // ONLY query DB - NO cache logic here
50
46
  const message = await db.collection("messages").findOne({ code });
47
+ if (!message) return null;
48
+
51
49
  return {
52
50
  code: message.code,
53
51
  classificationCode: message.classificationCode,
54
- message: message.content[language],
52
+ message: message.content[language] || null
55
53
  };
56
54
  }
57
55
  }
58
56
  ```
59
57
 
60
- ## Usage
61
-
62
- ### 1. Get Language from Request
58
+ ### 3. Implement IRedisCache Adapter (WITH your Redis wrapper)
63
59
 
64
60
  ```typescript
65
- import { getLanguageFromRequest } from "@ecash/shared";
61
+ // src/infrastructure/adapters/outbound/cache/redis-cache.adapter.ts
62
+ import { IRedisCache } from "@ecashlib/shared-message";
63
+
64
+ export class RedisCacheAdapter implements IRedisCache {
65
+ constructor(private redis: FastifyInstance["redis"]) {}
66
+
67
+ async get(key: string): Promise<string | null> {
68
+ return await this.redis.get(key);
69
+ }
70
+
71
+ async set(key: string, value: string, ttl?: number): Promise<void> {
72
+ if (ttl) {
73
+ await this.redis.setex(key, ttl, value);
74
+ } else {
75
+ await this.redis.set(key, value);
76
+ }
77
+ }
78
+
79
+ async mset(keyValues: Record<string, string>): Promise<void> {
80
+ const pipeline = this.redis.pipeline();
81
+ for (const [key, value] of Object.entries(keyValues)) {
82
+ pipeline.set(key, value);
83
+ }
84
+ await pipeline.exec();
85
+ }
86
+
87
+ async exists(key: string): Promise<boolean> {
88
+ const result = await this.redis.exists(key);
89
+ return result === 1;
90
+ }
91
+
92
+ async del(key: string): Promise<number> {
93
+ return await this.redis.del(key);
94
+ }
95
+
96
+ async delPattern(pattern: string): Promise<string[]> {
97
+ const keys = await this.redis.keys(pattern);
98
+ if (keys.length > 0) {
99
+ await this.redis.del(...keys);
100
+ }
101
+ return keys;
102
+ }
66
103
 
67
- const language = getLanguageFromRequest(request); // "vi", "en", "ko"
104
+ async hgetall(key: string): Promise<Record<string, string>> {
105
+ return await this.redis.hgetall(key);
106
+ }
107
+
108
+ async hset(key: string, field: string, value: string): Promise<void> {
109
+ await this.redis.hset(key, field, value);
110
+ }
111
+
112
+ async hdel(key: string, field?: string): Promise<void> {
113
+ if (field) {
114
+ await this.redis.hdel(key, field);
115
+ } else {
116
+ await this.redis.hdel(key);
117
+ }
118
+ }
119
+ }
68
120
  ```
69
121
 
70
- ### 2. Get Localized Message
122
+ ### 4. Use CachedMessageService (WITH cache)
71
123
 
72
124
  ```typescript
73
- import { getMessage } from "@ecash/shared";
125
+ import { CachedMessageService } from "@ecashlib/shared-message";
126
+ import { RedisCacheAdapter } from "./infrastructure/adapters/outbound/cache/redis-cache.adapter";
127
+ import { MessageRepository } from "./infrastructure/adapters/outbound/message/message.repository";
128
+
129
+ // Setup Redis adapter
130
+ const redisAdapter = new RedisCacheAdapter(fastify.redis);
131
+
132
+ // Create cached service
133
+ const dbService = new MessageRepository(); // Your DB service
134
+ const cachedMessageService = new CachedMessageService(
135
+ redisAdapter,
136
+ dbService,
137
+ 3600 // Cache TTL: 1 hour
138
+ );
139
+ ```
140
+
141
+ ## Cache Flow
74
142
 
75
- const message = await getMessage(messageService, "ECASH_00001", "vi", {
76
- projectTypeCode: "APARTMENT",
77
- errors: "field required",
78
- });
79
- // Returns: "Metadata không hợp lệ theo schema của project type 'APARTMENT': field required"
80
143
  ```
144
+ ┌─────────────────────────────────────────────────────────────┐
145
+ │ getMessageContentByCode() │
146
+ └─────────────────────────────────────────────────────────────┘
147
+
148
+ ┌─────────────┴──────────────┐
149
+ ▼ ▼
150
+ ┌─────────────────┐ ┌─────────────────┐
151
+ │ Check Redis │ MISS │ Query MongoDB │
152
+ │ │ ────────> │ │
153
+ │ message:ECASH_ │ │ messages │
154
+ │ 00001:vi │ │ {code, ...} │
155
+ └─────────────────┘ └─────────────────┘
156
+ │ │
157
+ │ HIT │
158
+ ▼ │
159
+ ┌───────────────┐ │
160
+ │ Return data │ │
161
+ └───────────────┘ │
162
+
163
+
164
+ ┌─────────────────┐
165
+ │ Save to Redis │
166
+ │ message:ECASH_ │
167
+ │ 00001:vi │
168
+ │ TTL: 3600s │
169
+ └─────────────────┘
170
+
171
+
172
+ ┌─────────────────┐
173
+ │ Return data │
174
+ └─────────────────┘
175
+ ```
176
+
177
+ ## Usage
81
178
 
82
- ### 3. Get Message from Request (Shorthand)
179
+ ### 1. Get Message with Cache
83
180
 
84
181
  ```typescript
85
- import { getMessageFromRequest } from "@ecash/shared";
182
+ import { CachedMessageService } from "@ecashlib/shared-message";
86
183
 
87
- const message = await getMessageFromRequest(
88
- request,
89
- messageService,
184
+ const message = await cachedMessageService.getMessageContentByCode(
90
185
  "ECASH_00001",
91
- { projectTypeCode: "APARTMENT" },
186
+ "vi"
92
187
  );
188
+ // First call: Query DB → Save to Redis → Return
189
+ // Next call: Return from Redis (FAST!)
93
190
  ```
94
191
 
95
- ### 4. Throw Message Error
192
+ ### 2. Get Message with Parameters
96
193
 
97
194
  ```typescript
98
- import { throwMessageError } from "@ecash/shared";
195
+ import { getMessage } from "@ecashlib/shared-message";
99
196
 
100
- await throwMessageError(
101
- messageService,
197
+ const message = await getMessage(
198
+ cachedMessageService,
199
+ "ECASH_00001",
102
200
  "vi",
201
+ { projectTypeCode: "APARTMENT", errors: "field required" }
202
+ );
203
+ // Returns: "Metadata không hợp lệ theo schema của project type 'APARTMENT': field required"
204
+ ```
205
+
206
+ ### 3. Get Message from Request
207
+
208
+ ```typescript
209
+ import { getMessageFromRequest } from "@ecashlib/shared-message";
210
+
211
+ const message = await getMessageFromRequest(
212
+ request,
213
+ cachedMessageService,
103
214
  "ECASH_00001",
104
- { projectTypeCode: "APARTMENT", errors: "field required" },
105
- "COMM0400",
215
+ { projectTypeCode: "APARTMENT" }
106
216
  );
107
217
  ```
108
218
 
109
- ### 5. Create Message Error (without throwing)
219
+ ### 4. Invalidate Cache (after update)
110
220
 
111
221
  ```typescript
112
- import { createMessageError } from "@ecash/shared";
222
+ // Invalidate all languages for a message
223
+ await cachedMessageService.invalidate("ECASH_00001");
113
224
 
114
- const error = await createMessageError(messageService, "vi", {
115
- messageCode: "ECASH_00001",
116
- responseCode: "COMM0400",
117
- params: { projectTypeCode: "APARTMENT" },
118
- });
225
+ // Invalidate specific language
226
+ await cachedMessageService.invalidateLanguage("ECASH_00001", "vi");
119
227
 
120
- throw error;
228
+ // Set to cache manually (after create)
229
+ await cachedMessageService.setToCache("ECASH_00001", {
230
+ vi: "Nội dung tiếng Việt",
231
+ en: "English content"
232
+ });
121
233
  ```
122
234
 
123
- ## API Reference
235
+ ### 5. Throw Message Error
124
236
 
125
- ### `getLanguageFromRequest(request, defaultLang?)`
237
+ ```typescript
238
+ import { throwMessageError } from "@ecashlib/shared-message";
126
239
 
127
- Extract language from request headers or query params.
240
+ await throwMessageError(
241
+ cachedMessageService,
242
+ "vi",
243
+ "ECASH_00001",
244
+ { projectTypeCode: "APARTMENT", errors: "field required" },
245
+ "COMM0400"
246
+ );
247
+ ```
128
248
 
129
- - **Priority**: `Accept-Language` header > `?lang=` query > `defaultLang`
130
- - **Returns**: Language code (`"vi"`, `"en"`, `"ko"`)
249
+ ## API Reference
131
250
 
132
- ### `getMessage(messageService, code, language, params?)`
251
+ ### `CachedMessageService`
133
252
 
134
- Get localized message with parameter replacement.
253
+ | Method | Description |
254
+ |--------|-------------|
255
+ | `getMessageContentByCode(code, language)` | Get message with cache logic |
256
+ | `getAllLanguages(code)` | Get all languages from cache |
257
+ | `invalidate(code)` | Delete all languages from cache |
258
+ | `invalidateLanguage(code, language)` | Delete specific language from cache |
259
+ | `setToCache(code, content)` | Manually set message to cache |
135
260
 
136
- - **messageService**: Service implementing `MessageServicePort`
137
- - **code**: Message code from database
138
- - **language**: Target language
139
- - **params**: Object with key-value pairs to replace `{key}` in template
261
+ ### `MessageCache` (Low-level)
140
262
 
141
- ### `getMessageFromRequest(request, messageService, code, params?)`
263
+ | Method | Description |
264
+ |--------|-------------|
265
+ | `get(code, language)` | Get from Redis |
266
+ | `getAll(code)` | Get all languages from Redis |
267
+ | `set(code, language, content, ttl?)` | Set to Redis with TTL |
268
+ | `setAll(code, content, ttl?)` | Set multiple languages to Redis |
269
+ | `delete(code)` | Delete all languages from Redis |
270
+ | `deleteLanguage(code, language)` | Delete specific language |
271
+ | `exists(code, language)` | Check if exists in cache |
272
+ | `flush()` | Delete ALL message cache (use with caution!) |
142
273
 
143
- Shorthand that extracts language from request and gets message.
274
+ ### `IRedisCache` Interface
144
275
 
145
- ### `throwMessageError(messageService, language, messageCode, params?, responseCode?, statusCode?)`
276
+ Your microservice needs to implement this interface to work with the cache system:
146
277
 
147
- Throw error with localized message from database.
278
+ ```typescript
279
+ export interface IRedisCache {
280
+ get(key: string): Promise<string | null>;
281
+ set(key: string, value: string, ttl?: number): Promise<void>;
282
+ mset(keyValues: Record<string, string>): Promise<void>;
283
+ exists(key: string): Promise<boolean>;
284
+ del(key: string): Promise<number>;
285
+ delPattern(pattern: string): Promise<string[]>;
286
+ hgetall(key: string): Promise<Record<string, string>>;
287
+ hset(key: string, field: string, value: string): Promise<void>;
288
+ hdel(key: string, field?: string): Promise<void>;
289
+ }
290
+ ```
148
291
 
149
- ### `createMessageError(messageService, language, options)`
292
+ ## Example: Full Setup in Microservice
150
293
 
151
- Create `MessageError` object without throwing.
294
+ ```typescript
295
+ // 1. Setup
296
+ import { CachedMessageService, getMessage, throwMessageError } from "@ecashlib/shared-message";
297
+ import { RedisCacheAdapter } from "./infrastructure/adapters/outbound/cache/redis-cache.adapter";
298
+ import { MessageRepository } from "./infrastructure/adapters/outbound/message/message.repository";
152
299
 
153
- ## Language Detection
300
+ const redisAdapter = new RedisCacheAdapter(fastify.redis);
301
+ const dbService = new MessageRepository(); // Your DB service
302
+ const messageService = new CachedMessageService(redisAdapter, dbService);
154
303
 
155
- The library detects language in this order:
304
+ // 2. Use in controller/service
305
+ async function getErrorMessage(request: Request) {
306
+ const language = request.headers["accept-language"] || "vi";
156
307
 
157
- 1. **Header**: `Accept-Language: vi-VN, vi;q=0.9` → `"vi"`
158
- 2. **Query**: `?lang=en` → `"en"`
159
- 3. **Default**: `"vi"`
308
+ const message = await messageService.getMessageContentByCode(
309
+ "ECASH_00001",
310
+ language
311
+ );
160
312
 
161
- ## Message Template Syntax
313
+ return message.message;
314
+ }
162
315
 
163
- Use `{key}` placeholders in your message templates:
316
+ // 3. Use with parameters
317
+ async function throwValidationError(request: Request, projectTypeCode: string, errors: string) {
318
+ const language = request.headers["accept-language"] || "vi";
164
319
 
165
- ```json
166
- {
167
- "content": {
168
- "vi": "Xin chào {name}, bạn có {count} tin nhắn mới",
169
- "en": "Hello {name}, you have {count} new messages"
170
- }
320
+ await throwMessageError(
321
+ messageService,
322
+ language,
323
+ "ECASH_00001",
324
+ { projectTypeCode, errors }
325
+ );
171
326
  }
172
327
  ```
173
328
 
174
- ```typescript
175
- getMessage(messageService, "MSG_00001", "vi", { name: "John", count: 5 });
176
- // "Xin chào John, bạn có 5 tin nhắn mới"
177
- ```
178
-
179
329
  ## Publishing
180
330
 
181
331
  ```bash
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Redis Cache Interface
3
+ * Implement this in your microservice to use with fastify.redis
4
+ *
5
+ */
6
+ export interface IRedisCache {
7
+ /**
8
+ * Get string value from Redis
9
+ */
10
+ get(key: string): Promise<string | null>;
11
+ /**
12
+ * Set string value to Redis with optional TTL
13
+ */
14
+ set(key: string, value: string, ttl?: number): Promise<void>;
15
+ /**
16
+ * Set multiple key-value pairs
17
+ */
18
+ mset(keyValues: Record<string, string>): Promise<void>;
19
+ /**
20
+ * Check if key exists
21
+ */
22
+ exists(key: string): Promise<boolean>;
23
+ /**
24
+ * Delete key
25
+ */
26
+ del(key: string): Promise<number>;
27
+ /**
28
+ * Delete all keys matching pattern
29
+ */
30
+ delPattern(pattern: string): Promise<string[]>;
31
+ /**
32
+ * Get all hash fields for a key (HGETALL)
33
+ */
34
+ hgetall(key: string): Promise<Record<string, string>>;
35
+ /**
36
+ * Set hash field (HSET)
37
+ */
38
+ hset(key: string, field: string, value: string): Promise<void>;
39
+ /**
40
+ * Delete hash field (HDEL)
41
+ */
42
+ hdel(key: string, field: string): Promise<void>;
43
+ /**
44
+ * Delete all hash fields (HDEL)
45
+ */
46
+ hdel(key: string): Promise<void>;
47
+ }
48
+ //# sourceMappingURL=cache-interfaces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-interfaces.d.ts","sourceRoot":"","sources":["../../src/cache/cache-interfaces.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAEzC;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7D;;OAEG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvD;;OAEG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtC;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAElC;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE/C;;OAEG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAEtD;;OAEG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/D;;OAEG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhD;;OAEG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ export * from "./message-cache";
2
+ export * from "./cache-interfaces";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC"}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./message-cache"), exports);
18
+ __exportStar(require("./cache-interfaces"), exports);
@@ -0,0 +1,52 @@
1
+ import { IRedisCache } from "./cache-interfaces";
2
+ /**
3
+ * Redis Cache Adapter for Message System
4
+ * Handles caching of messages with TTL support
5
+ *
6
+ * Cache format (matches ecash-cms):
7
+ * - Key: `message:${code}` (Redis hash)
8
+ * - Fields: language codes (vi, en, ko, etc.)
9
+ * - Values: message content
10
+ */
11
+ export declare class MessageCache {
12
+ private readonly redis;
13
+ private readonly defaultTTL;
14
+ constructor(redis: IRedisCache, defaultTTL?: number);
15
+ /**
16
+ * Get message content from cache (hash field)
17
+ */
18
+ get(code: string, language: string): Promise<string | null>;
19
+ /**
20
+ * Get all languages for a message code from cache (entire hash)
21
+ */
22
+ getAll(code: string): Promise<Record<string, string> | null>;
23
+ /**
24
+ * Set message content to cache (hash field)
25
+ */
26
+ set(code: string, language: string, content: string, ttl?: number): Promise<void>;
27
+ /**
28
+ * Set all languages for a message code (entire hash)
29
+ */
30
+ setAll(code: string, content: Record<string, string>, ttl?: number): Promise<void>;
31
+ /**
32
+ * Delete message from cache (all languages - delete hash)
33
+ */
34
+ delete(code: string): Promise<void>;
35
+ /**
36
+ * Delete specific language from cache (delete hash field)
37
+ */
38
+ deleteLanguage(code: string, language: string): Promise<void>;
39
+ /**
40
+ * Clear all message cache
41
+ */
42
+ flush(): Promise<void>;
43
+ /**
44
+ * Check if message exists in cache
45
+ */
46
+ exists(code: string, language: string): Promise<boolean>;
47
+ /**
48
+ * Generate cache key (hash format like ecash-cms)
49
+ */
50
+ private getKey;
51
+ }
52
+ //# sourceMappingURL=message-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-cache.d.ts","sourceRoot":"","sources":["../../src/cache/message-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD;;;;;;;;GAQG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,KAAK,EAAE,WAAW,EAAE,UAAU,GAAE,MAAa;IAKzD;;OAEG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASjE;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IASlE;;OAEG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvF;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxF;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzC;;OAEG;IACG,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnE;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAM9D;;OAEG;IACH,OAAO,CAAC,MAAM;CAGf"}
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MessageCache = void 0;
4
+ /**
5
+ * Redis Cache Adapter for Message System
6
+ * Handles caching of messages with TTL support
7
+ *
8
+ * Cache format (matches ecash-cms):
9
+ * - Key: `message:${code}` (Redis hash)
10
+ * - Fields: language codes (vi, en, ko, etc.)
11
+ * - Values: message content
12
+ */
13
+ class MessageCache {
14
+ constructor(redis, defaultTTL = 3600) {
15
+ this.redis = redis;
16
+ this.defaultTTL = defaultTTL;
17
+ }
18
+ /**
19
+ * Get message content from cache (hash field)
20
+ */
21
+ async get(code, language) {
22
+ const key = this.getKey(code);
23
+ const hash = await this.redis.hgetall(key);
24
+ if (hash && hash[language]) {
25
+ return hash[language];
26
+ }
27
+ return null;
28
+ }
29
+ /**
30
+ * Get all languages for a message code from cache (entire hash)
31
+ */
32
+ async getAll(code) {
33
+ const key = this.getKey(code);
34
+ const hash = await this.redis.hgetall(key);
35
+ if (hash && Object.keys(hash).length > 0) {
36
+ return hash;
37
+ }
38
+ return null;
39
+ }
40
+ /**
41
+ * Set message content to cache (hash field)
42
+ */
43
+ async set(code, language, content, ttl) {
44
+ const key = this.getKey(code);
45
+ await this.redis.hset(key, language, content);
46
+ }
47
+ /**
48
+ * Set all languages for a message code (entire hash)
49
+ */
50
+ async setAll(code, content, ttl) {
51
+ const key = this.getKey(code);
52
+ for (const [language, message] of Object.entries(content)) {
53
+ await this.redis.hset(key, language, message);
54
+ }
55
+ }
56
+ /**
57
+ * Delete message from cache (all languages - delete hash)
58
+ */
59
+ async delete(code) {
60
+ const key = this.getKey(code);
61
+ await this.redis.hdel(key);
62
+ }
63
+ /**
64
+ * Delete specific language from cache (delete hash field)
65
+ */
66
+ async deleteLanguage(code, language) {
67
+ const key = this.getKey(code);
68
+ await this.redis.hdel(key, language);
69
+ }
70
+ /**
71
+ * Clear all message cache
72
+ */
73
+ async flush() {
74
+ const pattern = "message:*";
75
+ await this.redis.delPattern(pattern);
76
+ }
77
+ /**
78
+ * Check if message exists in cache
79
+ */
80
+ async exists(code, language) {
81
+ const key = this.getKey(code);
82
+ const hash = await this.redis.hgetall(key);
83
+ return hash !== undefined && hash[language] !== undefined;
84
+ }
85
+ /**
86
+ * Generate cache key (hash format like ecash-cms)
87
+ */
88
+ getKey(code) {
89
+ return `message:${code}`;
90
+ }
91
+ }
92
+ exports.MessageCache = MessageCache;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./message";
2
+ export * from "./cache";
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./message"), exports);
18
+ __exportStar(require("./cache"), exports);
@@ -0,0 +1,67 @@
1
+ import { IRedisCache } from "../cache/cache-interfaces";
2
+ import { MessageServicePort, MessageContent } from "./types";
3
+ /**
4
+ * Cached Message Service
5
+ *
6
+ * Logic:
7
+ * 1. Check Redis cache FIRST
8
+ * 2. If miss → Query DB via MessageServicePort
9
+ * 3. Save result to Redis cache
10
+ * 4. Return cached data
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { CachedMessageService, IRedisCache } from "@ecashlib/shared-message";
15
+ *
16
+ * // Your Redis adapter implementing IRedisCache
17
+ * class RedisAdapter implements IRedisCache {
18
+ * constructor(private redis: FastifyInstance["redis"]) {}
19
+ * // implement methods...
20
+ * }
21
+ *
22
+ * const adapter = new RedisAdapter(fastify.redis);
23
+ * const cachedService = new CachedMessageService(adapter, dbService, 3600);
24
+ *
25
+ * const message = await cachedService.getMessageContentByCode("ECASH_00001", "vi");
26
+ * ```
27
+ */
28
+ export declare class CachedMessageService implements MessageServicePort {
29
+ private readonly cache;
30
+ private readonly dbService;
31
+ /**
32
+ * @param redis - IRedisCache implementation (your Redis wrapper)
33
+ * @param dbService - Database service implementing MessageServicePort
34
+ * @param cacheTTL - Cache TTL in seconds (default: 3600)
35
+ */
36
+ constructor(redis: IRedisCache, dbService: MessageServicePort, cacheTTL?: number);
37
+ /**
38
+ * Get message content with cache logic:
39
+ * 1. Check Redis cache
40
+ * 2. If miss → Query DB
41
+ * 3. Save to cache
42
+ * 4. Return result
43
+ */
44
+ getMessageContentByCode(code: string, language: string): Promise<MessageContent | null>;
45
+ /**
46
+ * Get all languages for a message code (with cache)
47
+ */
48
+ getAllLanguages(code: string): Promise<Record<string, string> | null>;
49
+ /**
50
+ * Invalidate cache for a message (call after update)
51
+ */
52
+ invalidate(code: string): Promise<void>;
53
+ /**
54
+ * Invalidate cache for a specific language (call after update)
55
+ */
56
+ invalidateLanguage(code: string, language: string): Promise<void>;
57
+ /**
58
+ * Set message to cache directly (use after create)
59
+ */
60
+ setToCache(code: string, content: Record<string, string>): Promise<void>;
61
+ /**
62
+ * Extract classification code from message code
63
+ * e.g., "ECASH_00001" -> "ECASH"
64
+ */
65
+ private extractClassificationCode;
66
+ }
67
+ //# sourceMappingURL=cached-message-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cached-message-service.d.ts","sourceRoot":"","sources":["../../src/message/cached-message-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAExD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,oBAAqB,YAAW,kBAAkB;IAC7D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAE/C;;;;OAIG;gBACS,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,EAAE,QAAQ,CAAC,EAAE,MAAM;IAKhF;;;;;;OAMG;IACG,uBAAuB,CAC3B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAuBjC;;OAEG;IACG,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAW3E;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C;;OAEG;IACG,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvE;;OAEG;IACG,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,OAAO,CAAC,IAAI,CAAC;IAIhB;;;OAGG;IACH,OAAO,CAAC,yBAAyB;CAIlC"}
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CachedMessageService = void 0;
4
+ const message_cache_1 = require("../cache/message-cache");
5
+ /**
6
+ * Cached Message Service
7
+ *
8
+ * Logic:
9
+ * 1. Check Redis cache FIRST
10
+ * 2. If miss → Query DB via MessageServicePort
11
+ * 3. Save result to Redis cache
12
+ * 4. Return cached data
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { CachedMessageService, IRedisCache } from "@ecashlib/shared-message";
17
+ *
18
+ * // Your Redis adapter implementing IRedisCache
19
+ * class RedisAdapter implements IRedisCache {
20
+ * constructor(private redis: FastifyInstance["redis"]) {}
21
+ * // implement methods...
22
+ * }
23
+ *
24
+ * const adapter = new RedisAdapter(fastify.redis);
25
+ * const cachedService = new CachedMessageService(adapter, dbService, 3600);
26
+ *
27
+ * const message = await cachedService.getMessageContentByCode("ECASH_00001", "vi");
28
+ * ```
29
+ */
30
+ class CachedMessageService {
31
+ /**
32
+ * @param redis - IRedisCache implementation (your Redis wrapper)
33
+ * @param dbService - Database service implementing MessageServicePort
34
+ * @param cacheTTL - Cache TTL in seconds (default: 3600)
35
+ */
36
+ constructor(redis, dbService, cacheTTL) {
37
+ this.cache = new message_cache_1.MessageCache(redis, cacheTTL);
38
+ this.dbService = dbService;
39
+ }
40
+ /**
41
+ * Get message content with cache logic:
42
+ * 1. Check Redis cache
43
+ * 2. If miss → Query DB
44
+ * 3. Save to cache
45
+ * 4. Return result
46
+ */
47
+ async getMessageContentByCode(code, language) {
48
+ // 1. Check Redis cache FIRST
49
+ const cached = await this.cache.get(code, language);
50
+ if (cached) {
51
+ return {
52
+ code,
53
+ classificationCode: this.extractClassificationCode(code),
54
+ message: cached
55
+ };
56
+ }
57
+ // 2. Cache miss → Query DB
58
+ const dbResult = await this.dbService.getMessageContentByCode(code, language);
59
+ if (!dbResult) {
60
+ return null;
61
+ }
62
+ // 3. Save to cache
63
+ await this.cache.set(code, language, dbResult.message);
64
+ return dbResult;
65
+ }
66
+ /**
67
+ * Get all languages for a message code (with cache)
68
+ */
69
+ async getAllLanguages(code) {
70
+ const cached = await this.cache.getAll(code);
71
+ if (cached) {
72
+ return cached;
73
+ }
74
+ // Cache miss - would need to query DB
75
+ // This is optional, can be implemented if needed
76
+ return null;
77
+ }
78
+ /**
79
+ * Invalidate cache for a message (call after update)
80
+ */
81
+ async invalidate(code) {
82
+ await this.cache.delete(code);
83
+ }
84
+ /**
85
+ * Invalidate cache for a specific language (call after update)
86
+ */
87
+ async invalidateLanguage(code, language) {
88
+ await this.cache.deleteLanguage(code, language);
89
+ }
90
+ /**
91
+ * Set message to cache directly (use after create)
92
+ */
93
+ async setToCache(code, content) {
94
+ await this.cache.setAll(code, content);
95
+ }
96
+ /**
97
+ * Extract classification code from message code
98
+ * e.g., "ECASH_00001" -> "ECASH"
99
+ */
100
+ extractClassificationCode(code) {
101
+ const parts = code.split("_");
102
+ return parts.slice(0, -1).join("_");
103
+ }
104
+ }
105
+ exports.CachedMessageService = CachedMessageService;
@@ -1,4 +1,5 @@
1
1
  export * from "./types";
2
2
  export * from "./message-helper";
3
3
  export * from "./message-error";
4
+ export * from "./cached-message-service";
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/message/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/message/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC"}
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./types"), exports);
18
18
  __exportStar(require("./message-helper"), exports);
19
19
  __exportStar(require("./message-error"), exports);
20
+ __exportStar(require("./cached-message-service"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"message-helper.d.ts","sourceRoot":"","sources":["../../src/message/message-helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,cAAc,EACvB,WAAW,GAAE,MAAa,GACzB,MAAM,CAgBR;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAC9B,cAAc,EAAE,kBAAkB,EAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,CA6BjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,aAAa,GACrB,MAAM,CAeR;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,cAAc,EACvB,cAAc,EAAE,kBAAkB,EAClC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,CAGjB"}
1
+ {"version":3,"file":"message-helper.d.ts","sourceRoot":"","sources":["../../src/message/message-helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,cAAc,EACvB,WAAW,GAAE,MAAa,GACzB,MAAM,CAgBR;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAC9B,cAAc,EAAE,kBAAkB,EAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,CA8BjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,aAAa,GACrB,MAAM,CAeR;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,cAAc,EACvB,cAAc,EAAE,kBAAkB,EAClC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,CAGjB"}
@@ -65,19 +65,17 @@ async function getMessage(messageService, code, language, params) {
65
65
  console.error(`Error getting message ${code} in ${language}:`, error);
66
66
  }
67
67
  // Fallback to Vietnamese if available
68
- if (language !== "vi") {
69
- try {
70
- const fallback = await messageService.getMessageContentByCode(code, "vi");
71
- if (fallback?.message) {
72
- return formatMessage(fallback.message, params);
73
- }
74
- }
75
- catch (error) {
76
- console.error(`Error getting fallback message ${code}:`, error);
68
+ try {
69
+ const fallback = await messageService.getMessageContentByCode("ECASH_0000", language);
70
+ if (fallback?.message) {
71
+ return formatMessage(fallback.message, params);
77
72
  }
78
73
  }
74
+ catch (error) {
75
+ console.error(`Error getting fallback message ${code}:`, error);
76
+ }
79
77
  // Final fallback - return code
80
- return `[${code}]`;
78
+ return `Có lỗi xẩy ra, vui lòng thử lại sau.`;
81
79
  }
82
80
  /**
83
81
  * Replace {key} placeholders in message template with actual values
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/message/types.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,uBAAuB,CACrB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/message/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,uBAAuB,CACrB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecashlib/shared-message",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Shared utilities for Ecash microservices - Message handling, i18n, error helpers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",