@ecashlib/shared-message 1.0.0 → 1.0.1

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,76 @@
1
+ import { IRedisCache } from "./cache-interfaces";
2
+ /**
3
+ * Redis Cache Adapter for Message System
4
+ * Handles caching of messages with TTL support
5
+ *
6
+ * @example
7
+ * import { MessageCache, IRedisCache } from "@ecashlib/shared-message";
8
+ *
9
+ * class YourRedisAdapter implements IRedisCache {
10
+ * // implement methods using your redis wrapper
11
+ * }
12
+ *
13
+ * const adapter = new YourRedisAdapter();
14
+ * const cache = new MessageCache(adapter, 3600);
15
+ */
16
+ export declare class MessageCache {
17
+ private readonly redis;
18
+ private readonly defaultTTL;
19
+ constructor(redis: IRedisCache, defaultTTL?: number);
20
+ /**
21
+ * Get message content from cache
22
+ * @param code - Message code (e.g., "ECASH_00001")
23
+ * @param language - Language code (e.g., "vi", "en")
24
+ * @returns Message content or null if not found
25
+ */
26
+ get(code: string, language: string): Promise<string | null>;
27
+ /**
28
+ * Get all languages for a message code from cache
29
+ * @param code - Message code
30
+ * @returns Object with all language translations
31
+ */
32
+ getAll(code: string): Promise<Record<string, string> | null>;
33
+ /**
34
+ * Set message content to cache with TTL
35
+ * @param code - Message code
36
+ * @param language - Language code
37
+ * @param content - Message content
38
+ * @param ttl - Time to live in seconds (optional, uses default)
39
+ */
40
+ set(code: string, language: string, content: string, ttl?: number): Promise<void>;
41
+ /**
42
+ * Set all languages for a message code
43
+ * @param code - Message code
44
+ * @param content - Object with all language translations
45
+ * @param ttl - Time to live in seconds (optional)
46
+ */
47
+ setAll(code: string, content: Record<string, string>, ttl?: number): Promise<void>;
48
+ /**
49
+ * Delete message from cache (all languages)
50
+ * @param code - Message code
51
+ */
52
+ delete(code: string): Promise<void>;
53
+ /**
54
+ * Delete specific language from cache
55
+ * @param code - Message code
56
+ * @param language - Language code
57
+ */
58
+ deleteLanguage(code: string, language: string): Promise<void>;
59
+ /**
60
+ * Clear all message cache (use with caution!)
61
+ */
62
+ flush(): Promise<void>;
63
+ /**
64
+ * Check if message exists in cache
65
+ */
66
+ exists(code: string, language: string): Promise<boolean>;
67
+ /**
68
+ * Helper to delete keys by pattern
69
+ */
70
+ private delPattern;
71
+ /**
72
+ * Generate cache key
73
+ */
74
+ private getKey;
75
+ }
76
+ //# 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;;;;;;;;;;;;;GAaG;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;;;;;OAKG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAMjE;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAuBlE;;;;;;OAMG;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;;;;;OAKG;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;IAiBxF;;;OAGG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzC;;;;OAIG;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;IAK9D;;OAEG;YACW,UAAU;IAUxB;;OAEG;IACH,OAAO,CAAC,MAAM;CAGf"}
@@ -0,0 +1,137 @@
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
+ * @example
9
+ * import { MessageCache, IRedisCache } from "@ecashlib/shared-message";
10
+ *
11
+ * class YourRedisAdapter implements IRedisCache {
12
+ * // implement methods using your redis wrapper
13
+ * }
14
+ *
15
+ * const adapter = new YourRedisAdapter();
16
+ * const cache = new MessageCache(adapter, 3600);
17
+ */
18
+ class MessageCache {
19
+ constructor(redis, defaultTTL = 3600) {
20
+ this.redis = redis;
21
+ this.defaultTTL = defaultTTL;
22
+ }
23
+ /**
24
+ * Get message content from cache
25
+ * @param code - Message code (e.g., "ECASH_00001")
26
+ * @param language - Language code (e.g., "vi", "en")
27
+ * @returns Message content or null if not found
28
+ */
29
+ async get(code, language) {
30
+ const key = this.getKey(code, language);
31
+ const cached = await this.redis.get(key);
32
+ return cached;
33
+ }
34
+ /**
35
+ * Get all languages for a message code from cache
36
+ * @param code - Message code
37
+ * @returns Object with all language translations
38
+ */
39
+ async getAll(code) {
40
+ const pattern = `message:${code}:*`;
41
+ const keys = await this.delPattern(pattern);
42
+ if (keys.length === 0) {
43
+ return null;
44
+ }
45
+ const result = {};
46
+ for (const key of keys) {
47
+ const value = await this.redis.get(key);
48
+ if (value) {
49
+ // Extract language from key: "message:ECASH_00001:vi" -> "vi"
50
+ const language = key.split(":").pop();
51
+ if (language) {
52
+ result[language] = value;
53
+ }
54
+ }
55
+ }
56
+ return Object.keys(result).length > 0 ? result : null;
57
+ }
58
+ /**
59
+ * Set message content to cache with TTL
60
+ * @param code - Message code
61
+ * @param language - Language code
62
+ * @param content - Message content
63
+ * @param ttl - Time to live in seconds (optional, uses default)
64
+ */
65
+ async set(code, language, content, ttl) {
66
+ const key = this.getKey(code, language);
67
+ await this.redis.set(key, content, ttl ?? this.defaultTTL);
68
+ }
69
+ /**
70
+ * Set all languages for a message code
71
+ * @param code - Message code
72
+ * @param content - Object with all language translations
73
+ * @param ttl - Time to live in seconds (optional)
74
+ */
75
+ async setAll(code, content, ttl) {
76
+ const expireTtl = ttl ?? this.defaultTTL;
77
+ const keyValues = {};
78
+ for (const [language, message] of Object.entries(content)) {
79
+ const key = this.getKey(code, language);
80
+ keyValues[key] = message;
81
+ }
82
+ await this.redis.mset(keyValues);
83
+ // Set TTL for each key individually since mset doesn't support TTL
84
+ for (const key of Object.keys(keyValues)) {
85
+ await this.redis.set(key, keyValues[key], expireTtl);
86
+ }
87
+ }
88
+ /**
89
+ * Delete message from cache (all languages)
90
+ * @param code - Message code
91
+ */
92
+ async delete(code) {
93
+ const pattern = `message:${code}:*`;
94
+ await this.delPattern(pattern);
95
+ }
96
+ /**
97
+ * Delete specific language from cache
98
+ * @param code - Message code
99
+ * @param language - Language code
100
+ */
101
+ async deleteLanguage(code, language) {
102
+ const key = this.getKey(code, language);
103
+ await this.redis.del(key);
104
+ }
105
+ /**
106
+ * Clear all message cache (use with caution!)
107
+ */
108
+ async flush() {
109
+ const pattern = "message:*";
110
+ await this.delPattern(pattern);
111
+ }
112
+ /**
113
+ * Check if message exists in cache
114
+ */
115
+ async exists(code, language) {
116
+ const key = this.getKey(code, language);
117
+ return await this.redis.exists(key);
118
+ }
119
+ /**
120
+ * Helper to delete keys by pattern
121
+ */
122
+ async delPattern(pattern) {
123
+ // This requires the adapter to implement delPattern properly
124
+ // For fastify.redis, keys() returns array, then we call del()
125
+ const keys = [];
126
+ // We need to scan for keys matching pattern
127
+ // Since IRedisCache.delPattern returns the deleted keys, we use it
128
+ return await this.redis.delPattern(pattern);
129
+ }
130
+ /**
131
+ * Generate cache key
132
+ */
133
+ getKey(code, language) {
134
+ return `message:${code}:${language}`;
135
+ }
136
+ }
137
+ 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":"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.1",
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",