@ecashlib/shared-message

v1.0.3

Published

Shared utilities for Ecash microservices - Message handling, i18n, error helpers

Readme

@ecash/shared

Shared utilities for Ecash microservices - Message handling, i18n, error helpers.

Features

  • i18n Message System: Get localized messages from database with Redis caching
  • Parameter Replacement: Dynamic message templates with {placeholder} support
  • Language Detection: Auto-detect language from request headers or query params
  • Error Helpers: Throw errors with dynamic, localized messages
  • Framework Agnostic: Works with any Redis implementation via IRedisCache interface

Installation

npm install @ecashlib/shared-message

Setup

1. Prepare Database Messages

Create messages in your messages collection:

{
  "code": "ECASH_00001",
  "classificationCode": "ECASH",
  "content": {
    "vi": "Metadata không hợp lệ theo schema của project type '{projectTypeCode}': {errors}",
    "en": "Metadata is invalid according to project type '{projectTypeCode}' schema: {errors}",
    "ko": "프로젝트 유형 '{projectTypeCode}'의 스키마에 따라 메타데이터가 유효하지 않습니다: {errors}"
  }
}

2. Implement Database Service (WITHOUT cache)

// ecash-marketplace/src/infrastructure/adapters/outbound/message/message.repository.ts
import { MessageServicePort } from "@ecashlib/shared-message";

export class MessageRepository implements MessageServicePort {
  async getMessageContentByCode(code: string, language: string) {
    // ONLY query DB - NO cache logic here
    const message = await db.collection("messages").findOne({ code });
    if (!message) return null;

    return {
      code: message.code,
      classificationCode: message.classificationCode,
      message: message.content[language] || null
    };
  }
}

3. Implement IRedisCache Adapter (WITH your Redis wrapper)

// src/infrastructure/adapters/outbound/cache/redis-cache.adapter.ts
import { IRedisCache } from "@ecashlib/shared-message";

export class RedisCacheAdapter implements IRedisCache {
  constructor(private redis: FastifyInstance["redis"]) {}

  async get(key: string): Promise<string | null> {
    return await this.redis.get(key);
  }

  async set(key: string, value: string, ttl?: number): Promise<void> {
    if (ttl) {
      await this.redis.setex(key, ttl, value);
    } else {
      await this.redis.set(key, value);
    }
  }

  async mset(keyValues: Record<string, string>): Promise<void> {
    const pipeline = this.redis.pipeline();
    for (const [key, value] of Object.entries(keyValues)) {
      pipeline.set(key, value);
    }
    await pipeline.exec();
  }

  async exists(key: string): Promise<boolean> {
    const result = await this.redis.exists(key);
    return result === 1;
  }

  async del(key: string): Promise<number> {
    return await this.redis.del(key);
  }

  async delPattern(pattern: string): Promise<string[]> {
    const keys = await this.redis.keys(pattern);
    if (keys.length > 0) {
      await this.redis.del(...keys);
    }
    return keys;
  }

  async hgetall(key: string): Promise<Record<string, string>> {
    return await this.redis.hgetall(key);
  }

  async hset(key: string, field: string, value: string): Promise<void> {
    await this.redis.hset(key, field, value);
  }

  async hdel(key: string, field?: string): Promise<void> {
    if (field) {
      await this.redis.hdel(key, field);
    } else {
      await this.redis.hdel(key);
    }
  }
}

4. Use CachedMessageService (WITH cache)

import { CachedMessageService } from "@ecashlib/shared-message";
import { RedisCacheAdapter } from "./infrastructure/adapters/outbound/cache/redis-cache.adapter";
import { MessageRepository } from "./infrastructure/adapters/outbound/message/message.repository";

// Setup Redis adapter
const redisAdapter = new RedisCacheAdapter(fastify.redis);

// Create cached service
const dbService = new MessageRepository(); // Your DB service
const cachedMessageService = new CachedMessageService(
  redisAdapter,
  dbService,
  3600 // Cache TTL: 1 hour
);

Cache Flow

┌─────────────────────────────────────────────────────────────┐
│                    getMessageContentByCode()                 │
└─────────────────────────────────────────────────────────────┘
                              │
              ┌─────────────┴──────────────┐
              ▼                              ▼
    ┌─────────────────┐          ┌─────────────────┐
    │  Check Redis    │   MISS    │   Query MongoDB │
    │                 │ ────────> │                 │
    │ message:ECASH_  │          │  messages       │
    │ 00001:vi        │          │  {code, ...}    │
    └─────────────────┘          └─────────────────┘
              │                              │
              │ HIT                         │
              ▼                             │
      ┌───────────────┐                  │
      │ Return data   │                  │
      └───────────────┘                  │
                                     │
                                     ▼
                            ┌─────────────────┐
                            │ Save to Redis   │
                            │ message:ECASH_  │
                            │ 00001:vi        │
                            │ TTL: 3600s      │
                            └─────────────────┘
                                     │
                                     ▼
                            ┌─────────────────┐
                            │ Return data     │
                            └─────────────────┘

Usage

1. Get Message with Cache

import { CachedMessageService } from "@ecashlib/shared-message";

const message = await cachedMessageService.getMessageContentByCode(
  "ECASH_00001",
  "vi"
);
// First call: Query DB → Save to Redis → Return
// Next call: Return from Redis (FAST!)

2. Get Message with Parameters

import { getMessage } from "@ecashlib/shared-message";

const message = await getMessage(
  cachedMessageService,
  "ECASH_00001",
  "vi",
  { projectTypeCode: "APARTMENT", errors: "field required" }
);
// Returns: "Metadata không hợp lệ theo schema của project type 'APARTMENT': field required"

3. Get Message from Request

import { getMessageFromRequest } from "@ecashlib/shared-message";

const message = await getMessageFromRequest(
  request,
  cachedMessageService,
  "ECASH_00001",
  { projectTypeCode: "APARTMENT" }
);

4. Invalidate Cache (after update)

// Invalidate all languages for a message
await cachedMessageService.invalidate("ECASH_00001");

// Invalidate specific language
await cachedMessageService.invalidateLanguage("ECASH_00001", "vi");

// Set to cache manually (after create)
await cachedMessageService.setToCache("ECASH_00001", {
  vi: "Nội dung tiếng Việt",
  en: "English content"
});

5. Throw Message Error

import { throwMessageError } from "@ecashlib/shared-message";

await throwMessageError(
  cachedMessageService,
  "vi",
  "ECASH_00001",
  { projectTypeCode: "APARTMENT", errors: "field required" },
  "COMM0400"
);

API Reference

CachedMessageService

| Method | Description | |--------|-------------| | getMessageContentByCode(code, language) | Get message with cache logic | | getAllLanguages(code) | Get all languages from cache | | invalidate(code) | Delete all languages from cache | | invalidateLanguage(code, language) | Delete specific language from cache | | setToCache(code, content) | Manually set message to cache |

MessageCache (Low-level)

| Method | Description | |--------|-------------| | get(code, language) | Get from Redis | | getAll(code) | Get all languages from Redis | | set(code, language, content, ttl?) | Set to Redis with TTL | | setAll(code, content, ttl?) | Set multiple languages to Redis | | delete(code) | Delete all languages from Redis | | deleteLanguage(code, language) | Delete specific language | | exists(code, language) | Check if exists in cache | | flush() | Delete ALL message cache (use with caution!) |

IRedisCache Interface

Your microservice needs to implement this interface to work with the cache system:

export interface IRedisCache {
  get(key: string): Promise<string | null>;
  set(key: string, value: string, ttl?: number): Promise<void>;
  mset(keyValues: Record<string, string>): Promise<void>;
  exists(key: string): Promise<boolean>;
  del(key: string): Promise<number>;
  delPattern(pattern: string): Promise<string[]>;
  hgetall(key: string): Promise<Record<string, string>>;
  hset(key: string, field: string, value: string): Promise<void>;
  hdel(key: string, field?: string): Promise<void>;
}

Example: Full Setup in Microservice

// 1. Setup
import { CachedMessageService, getMessage, throwMessageError } from "@ecashlib/shared-message";
import { RedisCacheAdapter } from "./infrastructure/adapters/outbound/cache/redis-cache.adapter";
import { MessageRepository } from "./infrastructure/adapters/outbound/message/message.repository";

const redisAdapter = new RedisCacheAdapter(fastify.redis);
const dbService = new MessageRepository(); // Your DB service
const messageService = new CachedMessageService(redisAdapter, dbService);

// 2. Use in controller/service
async function getErrorMessage(request: Request) {
  const language = request.headers["accept-language"] || "vi";

  const message = await messageService.getMessageContentByCode(
    "ECASH_00001",
    language
  );

  return message.message;
}

// 3. Use with parameters
async function throwValidationError(request: Request, projectTypeCode: string, errors: string) {
  const language = request.headers["accept-language"] || "vi";

  await throwMessageError(
    messageService,
    language,
    "ECASH_00001",
    { projectTypeCode, errors }
  );
}

Publishing

cd ecash-shared
npm run build
npm publish

License

PRIVATE - Ecash internal use only.