npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bts-soft

v1.0.1

Published

BTS Software Monorepo for NestJS Toolkit

Readme

@bts-soft/core

The Definitive Enterprise Meta-Framework for NestJS

@bts-soft/core is not just a package; it is the architectural backbone of the BTS Soft enterprise ecosystem. It streamlines the development of high-performance, secure, and scalable NestJS applications by consolidating five specialized packages into a unified, high-level API.

This documentation serves as the comprehensive technical manual for the entire core infrastructure, covering everything from low-level Redis atomic operations to high-level multi-channel notification strategies.


Core Vision & Architecture

The primary objective of @bts-soft/core is to eliminate "infrastructure boilerplate." Instead of configuring Redis, Cloudinary, BullMQ, and Nodemailer repeatedly for every microservice, developers can import a single module that provides pre-validated, secure, and performant implementations of these essential services.

Architectural Philosophy

The ecosystem is built on three core pillars:

  1. Protocol Agnostic: Every component is designed to work seamlessly with both REST (OpenAPI) and GraphQL (Apollo).
  2. Security by Default: Global interceptors and specialized decorators protect against common vulnerabilities like SQL Injection and XSS from the moment the application starts.
  3. Extensible Patterns: By utilizing the Strategy and Command patterns, the system allows for swapping providers (e.g., moving from Redis to Memcached, or Cloudinary to S3) without changing the business logic.

Module Index

| Package | Purpose | Key technologies | | :--- | :--- | :--- | | @bts-soft/validation | Domain-driven validation and security | Class-Validator, Class-Transformer | | @bts-soft/cache | Enterprise-grade Redis abstraction | ioredis, cache-manager | | @bts-soft/notifications | Reliable multi-channel delivery | BullMQ, Nodemailer, Twilio, FCM | | @bts-soft/upload | Media management and processing | Cloudinary, Strategy/Command Pattern | | @bts-soft/common | Infrastructure glue and standard bases | RXJS, TypeORM, Apollo |


Deep Dive: @bts-soft/validation

The validation module is the first line of defense for any BTS Soft application. It moves beyond simple "type checking" and implements complex domain rules and security sanitization.

Philosophy: Security-First Validation

Every text-based decorator in this package includes a hidden security layer. By default, it applies the SQL_INJECTION_REGEX to prevent malicious payloads from reaching the database layer. Additionally, it leverages class-transformer to normalize data (e.g., trimming whitespace and converting to lowercase) before the business logic ever sees it.


Decorator Reference (Exhaustive)

1. @EmailField(nullable?: boolean, isGraphql?: boolean)

Validates an email address and normalizes it to lowercase.

  • Validators: IsEmail, IsOptional, Matches (SQLi).
  • Transform: toLower.
  • Usage (REST):
class LoginDto {
  @EmailField()
  email: string;
}
  • Usage (GraphQL):
@InputType()
class RegisterInput {
  @EmailField(false, true)
  email: string;
}

2. @PasswordField(min?: number, max?: number, nullable?: boolean, isGraphql?: boolean)

Enforces a high-security password policy.

  • Rules: Must contain at least one uppercase letter, one lowercase letter, one digit, and one special character.
  • Default Constraints: Min: 8, Max: 16.
  • Usage:
class ChangePasswordDto {
  @PasswordField(12, 32)
  newPassword: string;
}

3. @PhoneField(format?: CountryCode, nullable?: boolean, isGraphql?: boolean)

Validates and cleans international phone numbers.

  • Logic: Automatically removes non-digit characters (except +) before validation.
  • Default Format: EG (Egypt).
  • Usage:
class ProfileDto {
  @PhoneField('SA')
  whatsappNumber: string;
}

4. @NationalIdField(nullable?: boolean, isGraphql?: boolean)

Strict validation for Egyptian National IDs.

  • Rules: Exactly 14 digits, must start with 2 or 3.
  • Cleaning: Removes any non-digit input automatically.
  • Usage:
class IdentityDto {
  @NationalIdField()
  nationalId: string;
}

5. @NameField(nullable?: boolean, isGraphql?: boolean)

Validates personal names with automatic title-case capitalization.

  • Logic: Capitalizes the first letter of every name segment.
  • Constraints: 2-100 characters.
  • Usage:
class UpdateUserDto {
  @NameField()
  fullName: string; // "omar sabry" -> "Omar Sabry"
}

6. @DescriptionField(nullable?: boolean, isGraphql?: boolean)

Designed for long-form text content like biographies or comments.

  • Constraints: 10-2000 characters.
  • Logic: Allows more characters than TextField (includes newlines and specialized punctuation).
  • Usage:
class UpdateBioDto {
  @DescriptionField()
  biography: string;
}

7. @NumberField(isInteger?: boolean, min?: number, max?: number, nullable?: boolean, isGraphql?: boolean)

Versatile numeric validation.

  • Options: Toggle between integer and float.
  • Usage:
class ProductDto {
  @NumberField(true, 1, 1000)
  stockCount: number;

  @NumberField(false, 0.01)
  price: number;
}

8. @UsernameField(nullable?: boolean, isGraphql?: boolean)

Validates standard system usernames.

  • Rules: 3-30 characters, Alphanumeric + Underscore, must start with a letter.
  • Usage:
class SetUsernameDto {
  @UsernameField()
  username: string;
}

9. @TextField(text: string, min?: number, max?: number, nullable?: boolean, isGraphql?: boolean)

The "Swiss Army Knife" for general text inputs.

  • Features: Customizable error messages, length limits, and SQLi protection.
  • Default: Min 1, Max 255.
  • Usage:
class SearchDto {
  @TextField('Search Query', 3, 50)
  q: string;
}

10. @CapitalField(text: string, min?: number, max?: number, nullable?: boolean, isGraphql?: boolean)

Similar to TextField but enforces capitalization on every word.

  • Usage: Useful for City names, Country names, or Titles.

11. @DateField(nullable?: boolean, isGraphql?: boolean)

Validates and converts input into a JavaScript Date object.

  • Usage:
class EventDto {
  @DateField()
  startDate: Date;
}

12. @BooleanField(nullable?: boolean, isGraphql?: boolean)

Strict boolean validation.

  • Usage:
class PreferencesDto {
  @BooleanField()
  isPublic: boolean;
}

13. @EnumField(entity: object, nullable?: boolean, isGraphql?: boolean)

Synchronizes validation with TypeScript Enums.

  • Usage:
enum UserRole { ADMIN = 'admin', USER = 'user' }

class UpdateRoleDto {
  @EnumField(UserRole)
  role: UserRole;
}

14. @UrlField(nullable?: boolean, isGraphql?: boolean)

Validates complete web URLs.

  • Logic: Enforces protocol and converts host to lowercase.

15. @IdField(length?: number, nullable?: boolean, isGraphql?: boolean)

Generic length-based ID validation (useful for ULID/UUID patterns).


Utility Exports

The validation package also exports the underlying transformation functions for manual use:

  • LowerWords(value: string): Converts strings to lowercase.
  • CapitalizeWords(value: string): Converts strings to Title Case.

Deep Dive: @bts-soft/cache

The caching module provides an enterprise-ready wrapper around Redis, designed to handle high-throughput operations with type safety and automatic serialization.

The IRedisInterface Contract

The RedisService implements the IRedisInterface, ensuring a consistent API surface across the entire application ecosystem.


Core Key-Value Operations

These are the most commonly used methods for simple state management.

set(key: string, value: any, ttl?: number): Promise<void>

Stores a value in Redis with automatic JSON stringification.

  • TTL: Default is 3600 seconds (1 hour).
  • Example:
await redisService.set('user:session:123', { id: 123, role: 'admin' }, 600);

get<T = any>(key: string): Promise<T | null>

Retrieves and parses a value from Redis.

  • Generic Type Support: Automatically casts the result to your interface.
  • Example:
const session = await redisService.get<UserSession>('user:session:123');

del(key: string): Promise<void>

Removes a key from the database.

mSet(data: Record<string, any>): Promise<void>

Sets multiple key-value pairs atomically using a Redis pipeline.

  • Example:
await redisService.mSet({
  'config:theme': 'dark',
  'config:lang': 'ar'
});

String & Atomic Operations

Perfect for building counters, distributed sequences, and atomic flags.

incr(key: string): Promise<number>

Increments the numeric value of a key by 1.

  • Use Case: Page view counters, attempt limiters.

incrBy(key: string, increment: number): Promise<number>

Increments a value by a specific integer amount.

decr(key: string): Promise<number>

Decrements the value by 1.

getSet(key: string, value: any): Promise<string | null>

Atomically sets a new value and returns the old value.

  • Use Case: Atomic state transitions.

strlen(key: string): Promise<number>

Returns the byte length of the stored string.


Complex Data Structures

1. Hashes (Object-like Storage)

Ideal for storing entities without serializing the entire object every time.

  • hSet(key, field, value): Set a field in a hash.
  • hGet(key, field): Get a specific field.
  • hGetAll(key): Retrieve the entire object.
  • hIncrBy(key, field, amount): Atomic increment of a field.
  • hSetNX(key, field, value): Set only if field doesn't exist.

2. Sets (Unique Collections)

Manage unique lists of IDs, tags, or permissions.

  • sAdd(key, ...members): Add unique items.
  • sMembers(key): Get all unique items.
  • sIsMember(key, member): Check membership.
  • sInter(key1, key2): Find common items between sets.
  • sUnion(key1, key2): Merge sets uniquely.

3. Sorted Sets (Scored Rankings)

The ultimate tool for leaderboards, activity feeds, and priority queues.

  • zAdd(key, score, member): Add item with a specific numeric score.
  • zRange(key, start, stop): Get members by index (Sorted by score).
  • zRank(key, member): Get the position of a member in the list.
  • zRemRangeByScore(key, min, max): Cleanup old or low-score data.

4. Lists (Linear Order)

  • lPush(key, value): Prepend to list.
  • rPop(key): Remove and return the last item (Queue logic).
  • lTrim(key, start, stop): Maintain a fixed-size history (Capping).

Advanced Data Types

Geospatial Indexing

Build "Nearby" features (Find stores, users, or assets).

  • geoAdd(key, long, lat, member): Index a coordinate.
  • geoDist(member1, member2, unit): Calculate distance between two points.
  • geoPos(key, member): Get coordinates for a member.

HyperLogLog (Probabilistic Counting)

Count unique items across millions of entries with minimal memory (approx 12KB).

  • pfAdd(key, ...elements): Observe an element.
  • pfCount(key): Get approximate unique count.

Distributed Locking & Messaging

Distributed Locking

The RedisService includes a high-level lock implementation to prevent race conditions in distributed systems.

const lockValue = await redisService.acquireLock('process:order:789', 'worker-1', 5000);
if (lockValue) {
  try {
    // Perform sensitive operation
  } finally {
    await redisService.releaseLock('process:order:789', lockValue);
  }
}

Pub/Sub Messaging

Enable real-time communication between microservices.

// Subscriber
await redisService.subscribe('events:new-user', (msg) => {
  console.log('New user joined:', JSON.parse(msg));
});

// Publisher
await redisService.publish('events:new-user', { id: 1, name: 'Omar' });

Deep Dive: @bts-soft/notifications

The notification module is a high-availability delivery engine designed to handle massive volumes of transactional and marketing messages across multiple protocols without slowing down your primary application.


Reliability Engineering: The Queue System

All notifications are processed asynchronously using BullMQ and Redis. This architecture provides several critical benefits:

  1. Non-Blocking: Your API returns a 200 OK immediately after the job is queued, without waiting for external APIs (like Twilio or Firebase).
  2. Strict Retries: If a provider is down, the system automatically retries with an exponential backoff policy.
  3. Concurrency Control: You can limit the number of parallel notifications to avoid hitting external API rate limits.

Backoff Configuration

  • Max Attempts: 3
  • Strategy: Exponential
  • Initial Delay: 5,000ms
  • Progression: 5s -> 10s -> 20s

Channel Deep-Dive (8+ Integrated Channels)

1. Email (EMAIL)

  • Technology: Nodemailer.
  • Support: SMTP, SES, Gmail, Outlook, Mailgun.
  • Example Payload:
await notificationService.send(ChannelType.EMAIL, {
  recipientId: '[email protected]',
  subject: 'Welcome to BTS Soft',
  body: 'Thank you for joining our platform.',
  channelOptions: {
    html: '<h1>Welcome!</h1>', // Optional HTML
    attachments: [{ filename: 'terms.pdf', path: './docs/terms.pdf' }]
  }
});

2. WhatsApp (WHATSAPP)

  • Provider: Twilio WhatsApp API.
  • Normalizer: Automatically handles Egyptian and international formats.
  • Example Payload:
await notificationService.send(ChannelType.WHATSAPP, {
  recipientId: '01012345678', // Auto-converts to whatsapp:+201012345678
  body: 'Your verification code is 4567'
});

3. SMS (SMS)

  • Provider: Twilio SMS.
  • Usage:
await notificationService.send(ChannelType.SMS, {
  recipientId: '+201112223344',
  body: 'Critical security alert on your account.'
});

4. Telegram (TELEGRAM)

  • Technology: Telegraf (Telegram Bot API).
  • Features: Markdown support, link previews.
  • Usage:
await notificationService.send(ChannelType.TELEGRAM, {
  recipientId: 'chat_id_here',
  body: '*Important Update*\nClick [here](https://bts-soft.com) to view.',
  channelOptions: { parse_mode: 'MarkdownV2' }
});

5. Firebase Push (FIREBASE_FCM)

  • Technology: Firebase Admin SDK.
  • Support: Android, iOS, and Web.
  • Usage:
await notificationService.send(ChannelType.FIREBASE_FCM, {
  recipientId: 'device_fcm_token',
  title: 'Order Delivered',
  body: 'Your package is at your doorstep.',
  channelOptions: {
    data: { orderId: '789' },
    options: { priority: 'high' }
  }
});

6. Discord (DISCORD)

  • Logic: Webhook-based integration.
  • Usage:
await notificationService.send(ChannelType.DISCORD, {
  body: 'New deployment successful!',
  channelOptions: {
    username: 'BTS Bot',
    embeds: [{ title: 'Build Info', color: 3066993 }]
  }
});

7. Microsoft Teams (TEAMS)

  • Logic: Incoming Webhooks (Message Cards).
  • Usage:
await notificationService.send(ChannelType.TEAMS, {
  body: 'New support ticket created.',
  channelOptions: { themeColor: '0078D4' }
});

8. Facebook Messenger (MESSENGER)

  • Provider: Graph API.
  • Usage:
await notificationService.send(ChannelType.MESSENGER, {
  recipientId: 'psid_here',
  body: 'Hello! How can we help you today?'
});

Deep Dive: @bts-soft/upload

The upload module is a production-hardened media orchestration service. It is designed to handle the complexities of multi-part streams, file validation, and cloud storage management.

Architecture Patterns

The service is built on three pillars of software engineering:

  1. Strategy Pattern: The IUploadStrategy interface allows you to define how files are stored. The default implementation is CloudinaryUploadStrategy, but you can easily plug in Amazon S3 or Google Cloud Storage.

  2. Command Pattern: Each upload type (Image, Video, Audio, Raw File) is encapsulated in a command. This allows the system to apply specific optimizations (like chunked video upload) without cluttering the main service.

  3. Observer Pattern: Successful and failed uploads trigger events that IUploadObserver instances can listen to. This is used for global logging, analytics, and cleanup tasks.


Media Type Specifications

| Media Type | File Extensions | Size Limit | Processing Logic | | :--- | :--- | :--- | :--- | | Images | jpg, png, webp, gif | 5 MB | Format optimization, fetch_format: auto | | Videos | mp4, webm, avi, mov | 100 MB | Chunked upload (6MB chunks), Duration extraction | | Audio | mp3, wav, ogg, m4a | 50 MB | Treated as a "video" resource for waveform generation | | Raw Files | pdf, doc, zip, txt | 10 MB | Stored as 'raw' resources with original headers |


Provider Integration (Cloudinary)

To initialize the upload system, ensure your environment is configured:

CLOUDINARY_CLOUD_NAME=your_name
CLOUDINARY_API_KEY=your_key
CLOUDINARY_API_SECRET=your_secret

The system automatically handles the creation of a Cloudinary client instance and injects it into the default strategies.


Deep Dive: @bts-soft/common

The common module is the "Standard Library" of the BTS Soft ecosystem. It provides the essential infrastructure that ensures all microservices and modules speak the same language.


Standardized Global Interceptors

By calling setupInterceptors(app) in your main.ts, you enable a powerful suite of request-processing logic:

  1. ClassSerializerInterceptor: Uses class-transformer to filter out sensitive fields (marked with @Exclude()) and include computed properties (marked with @Expose()).

  2. SqlInjectionInterceptor: A global scanner that intercepts all incoming request payloads (Body, Query, Params) and checks every string against the SQL_INJECTION_REGEX. If a violation is caught, it automatically throws a 400 Bad Request before the controller logic is executed.

  3. GeneralResponseInterceptor: The most visible part of the common module. It ensures that every response, whether it's a single entity, a list, or an error, follows the exact same JSON structure.

The Standard Response Envelope

{
  "success": true,
  "statusCode": 200,
  "message": "Request successful",
  "timeStamp": "2024-03-21T10:00:00.000Z",
  "data": { ... },
  "items": [ ... ],
  "pagination": {
     "total": 100,
     "page": 1,
     "limit": 10
  }
}

Shared Base Classes

1. BaseEntity (The Persistence Foundation)

All database entities in the system should extend BaseEntity.

  • ULID Integration: Instead of predictable numeric IDs, it uses ULIDs (Universally Unique Lexicographically Sortable Identifiers). These are 26-character strings that are both unique and sortable by creation time.
  • Audit Trails: Automatically generates createdAt and updatedAt timestamps.
  • Lifecycle Hooks: Includes pre-configured AfterInsert, AfterUpdate, and BeforeRemove logging to help with debugging database interactions in production.

2. BaseResponse (The API Contract)

Used as a base class for DTOs and GraphQL Object Types to ensure consistency in manual response construction.


Infrastructure Modules

The common package also provides pre-configured NestJS modules:

  • ConfigModule: A wrapper around @nestjs/config with built-in validation.
  • ThrottlingModule: Pre-configured rate limiting to prevent Brute-Force and DDoS attacks.
  • TranslationModule: Integrated nestjs-i18n support for multi-language applications (Arabic/English).
  • GraphqlModule: The standard Apollo Server setup with custom error filters that bridge the gap between GraphQL and HTTP status codes.

Scenario-Based Integration Guides

To understand the full power of @bts-soft/core, let's look at how these modules interact in real-world scenarios.

Scenario A: Building a "User Registration" Flow

This scenario demonstrates the interaction between Validation, Cache, Common, and Notifications.

  1. Input Validation: The RegisterDto uses @EmailField, @NameField, and @PasswordField. The input is automatically cleaned (SQLi protection), name is capitalized ("john doe" -> "John Doe"), and email is lowercased.

  2. Duplicate Check (Cache): Before hitting the database, the service checks Redis using redisService.exists('registration:lock:' + email) to prevent rapid-fire duplicate registrations (Idempotency).

  3. Persistence (Common): The User entity extends BaseEntity. It is saved with a auto-generated ULID.

  4. Welcome Message (Notifications): A background job is queued via notificationService.send(ChannelType.EMAIL, ...). The background processor handles the Nodemailer handshake while the API returns a response.

  5. Response Formatting (Common): The GeneralResponseInterceptor catches the return value and wraps it in the standard success envelope before sending it to the client.

Scenario B: Building an "Image Gallery with Search"

This scenario demonstrates Upload, Validation, and Cache.

  1. Image Upload: The controller receives a stream. uploadService.uploadImageCore processes it using the CloudinaryUploadStrategy and notifies the LoggingObserver.

  2. Metadata Search: The search query is validated via @TextField('Query', 3, 50).

  3. Result Caching: Search results are cached in Redis using a Hash (hSet). Subsequent searches for the same term are served in under 1ms.


Technical Appendix: The Complete API Dictionary

This section provides an exhaustive reference for internal services. Every method is documented with its signature, parameter requirements, and a real-world example.

1. RedisService (@bts-soft/cache)

The RedisService is a high-level wrapper for ioredis and cache-manager.

Core Key-Value Operations

| Method | Signature | Description | Example | | :--- | :--- | :--- | :--- | | set | (key, value, ttl?) | Stores any JS object or primitive. | await set('k', {a:1}, 60) | | get | <T>(key) | Retrieves and JSON-parses value. | await get<User>('u1') | | del | (key) | Deletes a key. | await del('old_key') | | mSet | (data) | Multi-set via pipeline. | await mSet({a:1, b:2}) | | mGet | (keys) | Multi-get. | await mGet(['a', 'b']) | | exists| (key) | Checks key existence. | await exists('token') | | expire| (key, sec) | Updates TTL. | await expire('k', 3600) | | ttl | (key) | Gets remaining seconds. | await ttl('k') |

Atomic Counters & Numeric Groups

| Method | Signature | Description | Example | | :--- | :--- | :--- | :--- | | incr | (key) | Increments by 1. | await incr('views') | | incrBy | (key, n) | Increments by integer N. | await incrBy('score', 10) | | decr | (key) | Decrements by 1. | await decr('retries') | | decrBy | (key, n) | Decrements by integer N. | await decrBy('balance', 5) | | getSet | (key, val) | Set new, return old. | await getSet('v', 5) |

Hash Operations (Field-Level Access)

| Method | Signature | Description | Example | | :--- | :--- | :--- | :--- | | hSet | (k, f, v) | Sets field in hash. | await hSet('u:1', 'n', 'O') | | hGet | <T>(k, f) | Gets field value. | await hGet<string>('u:1', 'n')| | hGetAll| (k) | Gets all fields as Object. | await hGetAll('u:1') | | hDel | (k, f) | Deletes field. | await hDel('u:1', 'n') | | hKeys | (k) | Returns all field names. | await hKeys('u:1') | | hVals | (k) | Returns all field values. | await hVals('u:1') | | hLen | (k) | Field count. | await hLen('u:1') | | hIncrBy| (k, f, n) | Incr field by N. | await hIncrBy('u:1', 'v', 1)|

Set Operations (Uniqueness)

| Method | Signature | Description | Example | | :--- | :--- | :--- | :--- | | sAdd | (k, ...m) | Adds members. | await sAdd('tags', 'a', 'b') | | sRem | (k, ...m) | Removes members. | await sRem('tags', 'a') | | sMembers| (k) | Returns all members. | await sMembers('tags') | | sCard | (k) | Set count. | await sCard('tags') | | sIsMember| (k, m) | Membership check. | await sIsMember('tags', 'a')| | sInter | (...k) | Set intersection. | await sInter('s1', 's2') | | sUnion | (...k) | Set union. | await sUnion('s1', 's2') |

Sorted Sets (Rankings)

| Method | Signature | Description | Example | | :--- | :--- | :--- | :--- | | zAdd | (k, s, m) | Add item with score. | await zAdd('lb', 100, 'u1') | | zRange | (k, s, t) | Range by index. | await zRange('lb', 0, 10) | | zRank | (k, m) | Member rank. | await zRank('lb', 'u1') | | zScore | (k, m) | Member score. | await zScore('lb', 'u1') | | zRem | (k, m) | Remove member. | await zRem('lb', 'u1') |

Geospatial Operations

| Method | Signature | Description | Example | | :--- | :--- | :--- | :--- | | geoAdd | (k, lo, la, m) | Store coordinate. | await geoAdd('idx', 31, 30, 'P')| | geoDist | (m1, m2, u) | Distance calculation. | await geoDist('P1', 'P2', 'km')| | geoPos | (k, m) | Get Lat/Long. | await geoPos('idx', 'P') |

HyperLogLog (Approximate Counting)

| Method | Signature | Description | Example | | :--- | :--- | :--- | :--- | | pfAdd | (k, ...e) | Add elements. | await pfAdd('uv', '1', '2') | | pfCount | (...k) | Get cardinality. | await pfCount('uv') |


Security Audit & Hardening Guide

The @bts-soft/core package is built with a "Zero Trust" mindset toward external input. Here is how we enforce security across different layers.

1. Database Security (SQL Injection)

We use a two-tier defense system against SQL injection:

  • Logic Level: Every TextField, EmailField, and DescriptionField in the validation module includes a Matches(SQL_INJECTION_REGEX) check. This ensures that potentially malicious strings are caught before they ever reach your service.
  • Infrastructure Level: The SqlInjectionInterceptor provides a global safety net by scanning every request property.

2. Output Sanitization (XSS)

The ClassSerializerInterceptor is mandatory. It ensures that:

  • Sensitive internal data (like user passwords or internal DB IDs) are stripped from the response.
  • Only fields explicitly marked with @Expose() are sent to the client.

3. Rate Limiting (Brute Force)

By using the ThrottlingModule, you can define per-endpoint limits based on the IP or the User ID.

@Throttle({ default: { limit: 10, ttl: 60000 } })
@Post('login')
async login() { ... }

4. Media Safety

The Upload module provides:

  • Extension white-listing to prevent uploading executable files (like .exe, .sh).
  • Size caps to prevent Disk Exhaustion attacks.
  • Strategy-level validation to ensure the cloud provider is reputable and secure.

Deployment, CI/CD, and Operations

A production-ready application requires a production-ready infrastructure.

Environment Variable Reference

Ensure the following variables are defined in your .env or CI secrets:

General

  • NODE_ENV: production | development | test
  • PORT: Default 3000

Redis Configuration

  • REDIS_HOST: e.g., localhost
  • REDIS_PORT: Default 6379
  • REDIS_PASSWORD: Optional

Notifications Configuration

  • EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS, EMAIL_SENDER
  • TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_SMS_NUMBER, TWILIO_WHATSAPP_NUMBER
  • TELEGRAM_BOT_TOKEN
  • FIREBASE_SERVICE_ACCOUNT_PATH

Media Configuration

  • CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET

The Giant Book of Usage Examples

This section provides complete, copy-pasteable implementations of every core feature for both REST and GraphQL architectures.


1. Advanced Cache Patterns

Pattern: "Cache-Aside" for High Traffic Entities

Best for User Profiles, Product Details, or Settings.

REST Controller Implementation:

@Get(':id')
async getProfile(@Param('id') id: string) {
  const cacheKey = `user:profile:${id}`;
  
  // 1. Try to get from Cache
  const cachedUser = await this.redis.get<User>(cacheKey);
  if (cachedUser) return cachedUser;

  // 2. Fetch from DB if not in Cache
  const user = await this.userRepo.findOneBy({ id });
  if (!user) throw new NotFoundException();

  // 3. Store in Cache with 10 min TTL
  await this.redis.set(cacheKey, user, 600);
  
  return user;
}

GraphQL Resolver Implementation:

@Query(() => User)
async userProfile(@Args('id') id: string) {
  const cacheKey = `user:profile:${id}`;
  
  return await this.redis.getOrSet(cacheKey, async () => {
    return await this.userRepo.findOneBy({ id });
  }, 600);
}

Pattern: "Atomic Rate Limiter" (Custom Logic)

When the standard Throttler isn't enough.

async isAllowed(userId: string, action: string): Promise<boolean> {
  const key = `limit:${userId}:${action}`;
  const count = await this.redis.incr(key);
  
  if (count === 1) {
    await this.redis.expire(key, 60); // Set 1 min window on first attempt
  }
  
  return count <= 5; // Allow 5 actions per minute
}

2. Multi-Channel Notification Orchestration

Pattern: "The Preference-Aware Broadcaster"

Sends notifications based on user opt-in channels.

async notifyUser(user: User, payload: any) {
  const jobs = [];

  if (user.wantsEmail) {
    jobs.push(this.notif.send(ChannelType.EMAIL, {
      recipientId: user.email,
      ...payload
    }));
  }

  if (user.wantsSms) {
    jobs.push(this.notif.send(ChannelType.SMS, {
      recipientId: user.phone,
      ...payload
    }));
  }

  await Promise.all(jobs);
}

3. Media Handling Lifecycle

Pattern: "Avatar Upload with Auto-Cleanup"

Uploads a new avatar and deletes the old one from Cloudinary.

async updateAvatar(userId: string, file: UploadFile) {
  const user = await this.userRepo.findOneBy({ id: userId });
  
  // 1. Upload new image
  const result = await this.upload.uploadImageCore(file, 'avatars');
  
  // 2. Delete old image if exists
  if (user.avatarUrl) {
    const publicId = this.getPublicIdFromUrl(user.avatarUrl);
    await this.upload.deleteImage(publicId);
  }
  
  // 3. Update DB
  user.avatarUrl = result.url;
  await this.userRepo.save(user);
}

Frequently Asked Questions (FAQ)

General

Q1: Can I use @bts-soft/core with NestJS 10?
A: No, version 2.2.4+ requires NestJS 11 due to dependency alignment and peer dependency overrides.

Q2: Does this package include TypeORM?
A: It includes @bts-soft/common which depends on TypeORM, but you must still install the driver (e.g., pg, mysql2) in your main application.


Validation

Q3: How do I disable SQL injection checks for a specific field?
A: You should use the standard class-validator decorators (like @IsString()) instead of the @TextField composite decorators.

Q4: Can I add custom transformation logic to @PhoneField?
A: No, the PhoneField uses a non-configurable regex cleaner. If you need custom cleaning, use Transform() manually.


Cache

Q5: What happens if the Redis server goes down?
A: The RedisService will throw errors. It is recommended to wrap cache calls in try/catch or use a circuit breaker if your application must remain functional without cache.


Exhaustive Redis API Guide

The RedisService provides a high-level, type-safe interface for interacting with Redis. This section contains a line-by-line documentation of every method available in the service.


Basic Key-Value Operations

1. set

Description: Stores a value in Redis with automatic serialization of objects and primitives. Interface: set(key: string, value: any, ttl: number = 3600): Promise<void> Parameters:

  • key: The unique identifier in Redis.
  • value: The data to store. Can be a string, number, or plain JavaScript object.
  • ttl: Time-To-Live in seconds. Default is 1 hour. Return: Promise<void> Examples:
// REST Example
await this.redisService.set(`user:${id}`, userData, 3600);

// GraphQL Example
await this.redisService.set(`profile:${userId}`, profileData, 1800);

2. get

Description: Retrieves and deserializes a value from Redis. Interface: get<T = any>(key: string): Promise<T | null> Parameters:

  • key: The key to retrieve. Return: Promise<T | null>. Returns null if the key does not exist. Examples:
// REST Example
const user = await this.redisService.get<UserEntity>(`user:${id}`);

// GraphQL Example
const profile = await this.redisService.get<ProfileInput>(`profile:${userId}`);

3. del

Description: Deletes one or more keys from Redis. Interface: del(...keys: string[]): Promise<void> Parameters:

  • keys: One or more keys to delete. Return: Promise<void> Example:
await this.redisService.del(`user:${id}`, `session:${token}`);

4. exists

Description: Checks if a key exists in Redis. Interface: exists(key: string): Promise<boolean> Return: true if it exists, false otherwise.

5. expire

Description: Sets or updates the TTL of a key. Interface: expire(key: string, seconds: number): Promise<boolean>

6. ttl

Description: Gets the remaining TTL of a key. Interface: ttl(key: string): Promise<number>


String & Numeric Operations

7. incr

Description: Atomically increments the numeric value of a key. Interface: incr(key: string): Promise<number> Usage: const newCount = await this.redisService.incr('hit_counter');

8. incrBy

Description: Increments a key by a specific integer. Interface: incrBy(key: string, increment: number): Promise<number>

9. incrByFloat

Description: Increments a key by a floating-point number. Interface: incrByFloat(key: string, increment: number): Promise<number>

10. decr

Description: Atomically decrements a key. Interface: decr(key: string): Promise<number>

11. getSet

Description: Sets a new value and returns the old one. This is an atomic operation. Interface: getSet(key: string, value: any): Promise<string | null>

12. strlen

Description: Returns the length of a string value. Interface: strlen(key: string): Promise<number>


Hash Operations (Object Manipulation)

13. hSet

Description: Sets a field in a Redis hash. Interface: hSet(key: string, field: string, value: any): Promise<void>

14. hGet

Description: Gets a field from a Redis hash. Interface: hGet<T = any>(key: string, field: string): Promise<T | null>

15. hGetAll

Description: Gets all fields and values from a Redis hash as a JavaScript object. Interface: hGetAll(key: string): Promise<Record<string, any>>

16. hDel

Description: Deletes fields from a hash. Interface: hDel(key: string, ...fields: string[]): Promise<void>

17. hKeys

Description: Gets all field names in a hash. Interface: hKeys(key: string): Promise<string[]>

18. hVals

Description: Gets all field values in a hash. Interface: hVals(key: string): Promise<any[]>

19. hLen

Description: Gets the number of fields in a hash. Interface: hLen(key: string): Promise<number>

20. hIncrBy

Description: Increments a numeric hash field. Interface: hIncrBy(key: string, field: string, increment: number): Promise<number>


Set Operations (Unique Collections)

21. sAdd

Description: Adds one or more members to a set. Interface: sAdd(key: string, ...members: any[]): Promise<void>

22. sRem

Description: Removes members from a set. Interface: sRem(key: string, ...members: any[]): Promise<void>

23. sMembers

Description: Gets all members of a set. Interface: sMembers(key: string): Promise<any[]>

24. sIsMember

Description: Checks if a value is a member of a set. Interface: sIsMember(key: string, member: any): Promise<boolean>

25. sCard

Description: Gets the number of members in a set. Interface: sCard(key: string): Promise<number>

26. sInter

Description: Finds the intersection of multiple sets. Interface: sInter(...keys: string[]): Promise<any[]>


27. sUnion

Description: Returns the union of multiple sets. Interface: sUnion(...keys: string[]): Promise<any[]>

28. sDiff

Description: Returns the difference between the first set and all successive sets. Interface: sDiff(...keys: string[]): Promise<any[]>


Sorted Set Operations (Leaderboards & Priority)

29. zAdd

Description: Adds a member with a specific score to a sorted set. Interface: zAdd(key: string, score: number, member: any): Promise<void>

30. zRange

Description: Returns a range of members by their index. Interface: zRange(key: string, start: number, stop: number): Promise<any[]>

31. zRevRange

Description: Returns a range of members, ordered from high to low score. Interface: zRevRange(key: string, start: number, stop: number): Promise<any[]>

32. zRank

Description: Returns the rank (index) of a member, sorted by score. Interface: zRank(key: string, member: any): Promise<number | null>

33. zScore

Description: Returns the score associated with a member. Interface: zScore(key: string, member: any): Promise<number | null>

34. zCard

Description: Gets the number of elements in a sorted set. Interface: zCard(key: string): Promise<number>

35. zCount

Description: Counts members with scores within a specific range. Interface: zCount(key: string, min: number, max: number): Promise<number>

36. zRem

Description: Removes one or more members. Interface: zRem(key: string, ...members: any[]): Promise<void>


List Operations (Queues & Stacks)

37. lPush

Description: Prepends a value to a list. Interface: lPush(key: string, ...values: any[]): Promise<number>

38. rPush

Description: Appends a value to a list. Interface: rPush(key: string, ...values: any[]): Promise<number>

39. lPop

Description: Removes and returns the first element. Interface: lPop<T = any>(key: string): Promise<T | null>

40. rPop

Description: Removes and returns the last element. Interface: rPop<T = any>(key: string): Promise<T | null>

41. lRange

Description: Returns a range of elements from a list. Interface: lRange(key: string, start: number, stop: number): Promise<any[]>

42. lLen

Description: Returns the length of a list. Interface: lLen(key: string): Promise<number>

43. lTrim

Description: Trims a list to a specific range (Atomic capping). Interface: lTrim(key: string, start: number, stop: number): Promise<void>


Advanced Tooling: Pub/Sub & Locking

44. publish

Description: Posts a message to a channel. Interface: publish(channel: string, message: any): Promise<void>

45. subscribe

Description: Listens for messages on a channel. Interface: subscribe(channel: string, callback: (message: string) => void): Promise<void>

46. acquireLock

Description: Attempts to acquire a distributed lock. Interface: acquireLock(resource: string, value: string, ttl: number): Promise<string | null>

47. releaseLock

Description: Safely releases a distributed lock using Lua scripting. Interface: releaseLock(resource: string, value: string): Promise<boolean>


Notification Provider Master Reference

This section provides a deep technical dive into every supported notification channel, including its underlying technology, payload schema, and configuration secrets.


1. Email (Nodemailer Engine)

The email channel is built for massive scale, supporting both direct SMTP and high-volume API relays.

Configuration Matrix

| Variable | Description | Example | | :--- | :--- | :--- | | EMAIL_HOST | SMTP Server | smtp.gmail.com | | EMAIL_PORT | Connection Port | 465 (SSL) or 587 (TLS) | | EMAIL_USER | Auth Login | [email protected] | | EMAIL_PASS | Auth Password | password_or_app_token | | EMAIL_SERVICE| Pre-defined service | gmail, outlook, sendgrid |

Advanced Usage: Attachments & Inline Images

await notificationService.send(ChannelType.EMAIL, {
  recipientId: '[email protected]',
  subject: 'Monthly Report',
  body: 'Please see the attached report.',
  channelOptions: {
    attachments: [
      {
        filename: 'report.pdf',
        content: pdfBuffer,
        contentType: 'application/pdf'
      }
    ]
  }
});

2. WhatsApp & SMS (Twilio Hub)

Both channels leverage the Twilio REST API with custom normalization for Middle-Eastern phone formats.

The Normalization Logic

Every phone number passed to recipientId is passed through a sanitizer:

  1. Trims whitespace and dashes.
  2. Handles 00 prefix by converting to +.
  3. Automatically detects Egyptian 01 formats and prepends +20.
  4. Ensures the whatsapp: prefix is correctly applied for the WhatsApp channel.

Configuration Matrix

| Variable | Description | | :--- | :--- | | TWILIO_ACCOUNT_SID | Your unique Twilio account identifier. | | TWILIO_AUTH_TOKEN | Secret token for API authentication. | | TWILIO_SMS_NUMBER | The registered 10-digit Twilio phone number. | | TWILIO_WHATSAPP_NUMBER| The "Sandbox" or production WhatsApp number. |


3. Telegram (Bot API integration)

The Telegram channel is the fastest way to build real-time monitoring and alert systems.

Configuration

  • TELEGRAM_BOT_TOKEN: Obtained from @BotFather.

Rich Formatting Support

Telegram supports HTML and MarkdownV2. You can trigger these via channelOptions.

await notificationService.send(ChannelType.TELEGRAM, {
  recipientId: '@monitoring_channel',
  body: '<b>CRITICAL:</b> Database CPU is at 95%',
  channelOptions: { parse_mode: 'HTML' }
});

4. Firebase Cloud Messaging (FCM)

The FCM channel is the standard for mobile and web push notifications in the BTS Soft ecosystem.

Device Token Management

  • recipientId: Must be a valid FCM device token or topic name (prefixed with /topics/).

Rich Payloads

You can pass custom data and notification options to fine-tune the delivery.

await notificationService.send(ChannelType.FIREBASE_FCM, {
  recipientId: 'EXPO_TOKEN_123',
  title: 'Flash Sale! ⚡',
  body: 'Get 50% off for the next 4 hours.',
  channelOptions: {
    data: { url: '/deals/flash-sale' },
    options: {
      priority: 'high',
      timeToLive: 14400 // 4 hours
    }
  }
});

5. Chatbot Webhooks (Discord & Teams)

Both channels use a simplified HTTP-based webhook architecture, perfect for server alerts and team collaboration.

Discord Features

  • Supports rich embeds and custom avatars per message.
  • Configuration: DISCORD_WEBHOOK_URL.

MS Teams Features

  • Supports adaptive cards and actionable messages.
  • Configuration: TEAMS_WEBHOOK_URL.

Technical Appendix: Complete Redis API Reference

This specification documents every method available in the RedisService, providing developers with a complete catalog of atomic data operations.

1. Key Lifecycle (O(1))

get<T>(key: string): Promise<T | null>

  • Returns: The parsed JSON object or raw string.
  • Error Cases: Returns null if key does not exist.

set(key: string, value: any, ttl?: number): Promise<void>

  • Params: ttl defaults to 3600 seconds.
  • Serialization: Automatic JSON.stringify for objects.

del(key: string): Promise<void>

  • Purpose: Permanent removal of a key.

exists(key: string): Promise<boolean>

  • Returns: true if key exists, false otherwise.

expire(key: string, seconds: number): Promise<boolean>

  • Purpose: Update the TTL of an existing key.

ttl(key: string): Promise<number>

  • Returns: Remaining seconds (-1 for infinite, -2 for not found).

2. Atomic Counters (O(1))

incr(key: string): Promise<number>

  • Purpose: Thread-safe increment of an integer key.

decr(key: string): Promise<number>

  • Purpose: Thread-safe decrement.

incrBy(key: string, value: number): Promise<number>

  • Purpose: Increment by a specific amount.

decrBy(key: string, value: number): Promise<number>

  • Purpose: Decrement by a specific amount.

3. Hash Objects (O(N) for Multiple Fields)

hSet(key: string, field: string, value: any): Promise<number>

  • Purpose: Store a value in a hash field.

hGet<T>(key: string, field: string): Promise<T | null>

  • Purpose: Retrieve value from a specific hash field.

hDel(key: string, ...fields: string[]): Promise<number>

  • Purpose: Remove one or more fields from a hash.

hGetAll<T>(key: string): Promise<T>

  • Purpose: Retrieve the entire hash object.

hKeys(key: string): Promise<string[]>

  • Purpose: List all field names in a hash.

Technical Appendix: Global Configuration Schema (Exhaustive)

Every environment variable that influences @bts-soft/core is documented below.

| Variable Name | Purpose | Example Value | | :--- | :--- | :--- | | NODE_ENV | Runtime environment | production | development | | REDIS_HOST | Cache endpoint | 127.0.0.1 | | REDIS_PORT | Cache port | 6379 | | REDIS_PASS | Cache password | StrongSecret123! | | EMAIL_SERVICE | Nodemailer provider | gmail | outlook | | EMAIL_USER | Sender address | [email protected] | | EMAIL_PASS | App-specific password | abcd-efgh-ijkl-mnop | | CLOUDINARY_NAME | Media cloud name | bts-soft-cloud | | CLOUDINARY_API_KEY| Media API key | 123456789012345 | | CLOUDINARY_SECRET | Media API secret | _shhh_secrets_ | | TWILIO_SID | SMS account SID | AC123... | | TWILIO_TOKEN | SMS auth token | auth_tok_... | | TELEGRAM_TOKEN | Bot API token | 123456:ABC-DEF... |


Technical Appendix: Architectural Blueprint & Decision Records (ADR)

ADR 001: Choosing ULID over UUID

Decision: Standardize on ULID for primary keys. Rationale: ULIDs are lexicographical (sortable by time) while maintaining the collision resistance of UUIDs. This significantly improves database index performance for time-series data like Orders and Audits.

ADR 002: Command Pattern for Uploads

Decision: Use the Command & Strategy pattern for the Upload module. Rationale: This allows us to add new storage providers (S3, Azure Blob) without changing the UploadService signature, ensuring future-proof extensibility.

ADR 003: BullMQ for Notifications

Decision: Mandatory queueing for all notification channels. Rationale: Third-party APIs (Twilio, Firebase) are inherently unreliable. A persistent queue ensures that momentary network glitches do not result in dropped user notifications.


Technical Appendix: Ecosystem Roadmap 2026-2027

As we continue to evolve the @bts-soft/core framework hub, we have planned a series of major enhancements to maintain our lead in the enterprise NestJS space.

v2.3.0: The "Observability" Update (Q2 2026)

  • OpenTelemetry Native Support: Built-in tracing for Redis, TypeORM, and Notification dispatch.
  • Service Mesh Helpers: Pre-configured sidecar patterns for Istio and Linkerd.
  • Log Masking: Automated PII detection and redaction in the GeneralResponseInterceptor.

v2.4.0: The "Intelligence" Update (Q4 2026)

  • AI-Logic Validation: New decorators like @SentimentField and @SpamScanner using local LLMs or external APIs.
  • Predictive Caching: Using Redis TimeSeries to predict key expiration and warm the cache proactively.
  • Notification Sentiment Analysis: Automated scoring of incoming (if implemented) or outgoing message tones.

v3.0.0: The "Micro-Kernel" Revolution (2027)

  • Zero-Dependency Core: Moving heavy providers (Twilio, Firebase) into optional peer-dependencies.
  • WebAssembly Transformers: High-performance data cleaning using WASM-compiled Rust modules.
  • Native Bun/Deno Support: Ensuring full compatibility with next-gen JavaScript runtimes.

Technical Appendix: Developer Onboarding Checklist

New to the BTS Soft ecosystem? Follow this step-by-step onboarding guide.

Week 1: Foundational Setup

  1. [ ] Install the BTS Soft VS Code Extension Pack.
  2. [ ] Clone the Core Samples Repository.
  3. [ ] Complete the "Hello World" tutorial for the Validation module.
  4. [ ] Successfully send a test email via the Notification service.

Week 2: Intermediate Patterns

  1. [ ] Implement a Cache-Aside logic for a database entity.
  2. [ ] Create a custom Upload Observer for audit logging.
  3. [ ] Build a GraphQL Input Type using the core decorators.

Week 3: Production Readiness

  1. [ ] Perform a SQL Injection Simulation on your local API.
  2. [ ] Run a Load Test (1,000 RPS) using k6.
  3. [ ] Configure Redis Persistence (AOF) on your staging server.

Technical Appendix: The Global Developer Credits

A special thank you to all the engineers who have contributed to the @bts-soft codebase.

| Contributor | Area of Expertise | Version Contribution | | :--- | :--- | :--- | | Omar Sabry | Lead Architect, Security | v1.0.0 - Present |


Epilogue: The BTS Soft Legacy

The @bts-soft/core package represents years of combined engineering experience in the Middle Eastern tech market. It is built to solve the unique challenges of local connectivity, right-to-left language support, and high-availability requirements.


Credits & License

Created and maintained by Omar Sabry for BTS Soft. Licensed under the MIT License. © 2026 BTS Soft. All Rights Reserved.

Enterprise Media Management Deep Dive

The @bts-soft/upload package is built to handle the entire lifecycle of a file—from the moment it arrives as a stream to its eventual deletion or transformation in the cloud.


The Command Pattern Architecture

Instead of one giant service with 50 methods, we encapsulate file-specific logic into "Commands." This keeps the codebase clean and allows for easy unit testing of upload logic.

Available Commands

  • UploadImageCommand: Handles image-specific validation and format conversion.
  • UploadVideoCommand: Manages large file streams and chunked uploads.
  • UploadAudioCommand: Processes sound files with audio metadata extraction.
  • UploadFileCommand: Transparently handles documents and raw binary data.
  • DeleteImageCommand: Safely removes assets and cleans up the Cloudinary cache.

Strategic Media Lifecycles

Every file upload follows a 5-step lifecycle:

  1. Validation Stage: The UploadService checks the file extension and size against the protocol limits (e.g., 5MB for images).
  2. Command Preparation: A specific command is instantiated (e.g., UploadImageCommand) with the incoming stream and target folder.
  3. Execution (Strategy): The command calls the IUploadStrategy.upload() method. By default, this pipes the stream directly to Cloudinary's secure servers.
  4. Observer Notification: On success or failure, the LoggingObserver (or your custom observer) is notified to log the event or trigger a DB update.
  5. Response Sanitization: High-level metadata (URL, public_id, size) is returned to the caller in a standardized object.

Advanced Media Transformations

Since we use Cloudinary by default, you can leverage their massive transformation CDN directly through channelOptions.

// Example: Generate a square avatar with rounded corners
const result = await uploadService.uploadImageCore(file, 'users', {
  transformation: [
    { width: 500, height: 500, crop: "fill", gravity: "face" },
    { radius: "max" },
    { effect: "sepia" }
  ]
});

Global Troubleshooting Matrix

This comprehensive guide covers common errors and their solutions across all core modules.

Redis Connectivity

| Error Message | Possible Root Cause | Solution | | :--- | :--- | :--- | | ECONNREFUSED | Redis server is not running or port is wrong. | Check REDIS_HOST and REDIS_PORT. | | NOAUTH | Redis requires a password but none was provided. | Set REDIS_PASSWORD in .env. | | OOM command not allowed | Redis has reached its memory limit. | Increase maxmemory in redis.conf or cleanup old keys. |

Notification Delivery

| Error Message | Possible Root Cause | Solution | | :--- | :--- | :--- | | EAUTH - Invalid credentials| SMTP user/password is incorrect. | Check EMAIL_USER and EMAIL_PASS. | | Invalid phone number | Input is not in E.164 format. | Ensure recipientId starts with + or use the auto-normalizer. | | 403 Forbidden (Telegram) | Bot has been blocked by the user. | User must restart the bot to receive messages. |

Media Upload

| Error Message | Possible Root Cause | Solution | | :--- | :--- | :--- | | Invalid image type | Extension is not in the allowed list. | Check the 'Media Type Specifications' table above. | | File too large | Stream size exceeds the module limit. | Use a separate command for large files or update limits. | | Must provide cloud_name | Cloudinary config is missing. | Verify CLOUDINARY_CLOUD_NAME is loaded in process.env. |


Enterprise Design Patterns & Architecture Deep Dive

The architecture of @bts-soft/core is influenced by high-scale enterprise systems. This section explains the internal design patterns that make the system robust and modular.


1. The Strategy Pattern (Provider Decoupling)

In the upload and notification modules, we decouple the "What to do" from "How to do it."

classDiagram
    class IUploadStrategy {
        <<interface>>
        +upload(stream, options)
    }
    class CloudinaryUploadStrategy {
        -cloudinaryClient
        +upload()
    }
    class S3UploadStrategy {
        -s3Client
        +upload()
    }
    IUploadStrategy <|.. CloudinaryUploadStrategy
    IUploadStrategy <|.. S3UploadStrategy
    UploadService --> IUploadStrategy

Why this matters:

  • Zero Lock-in: You can switch from Cloudinary to S3 by simply creating a new strategy class and updating the factory.
  • Improved Testing: You can inject a MockUploadStrategy in unit tests to avoid hitting real APIs.

2. The Command Pattern (Logic Encapsulation)

Every specific media operation is a discrete "Command" object.

sequenceDiagram
    participant App as Application Code
    participant Service as UploadService
    participant Command as UploadImageCommand
    participant Strategy as CloudinaryStrategy

    App->>Service: uploadImageCore(stream, options)
    Service->>Command: new UploadImageCommand(strategy, stream, opts)
    Service->>Command: execute()
    Command->>Strategy: upload(stream, opts)
    Strategy-->>Command: result
    Command-->>Service: result
    Service-->>App: result

Why this matters:

  • Single Responsibility: The UploadService doesn't need to know how to handle MP4 chunks or JPEG compression; it just knows how to execute commands.
  • Atomic Operations: Each command is an isolated unit of work.

3. The Observer Pattern (Reactive Events)

We use the Observer pattern to handle secondary effects like logging, analytics, and cache invalidation.

graph LR
    Upload[Upload Command Success] --> ObserverPool[Observer Registry]
    ObserverPool --> Log[LoggingObserver]
    ObserverPool --> Analytics[MetricsObserver]
    ObserverPool --> DB[DatabaseSyncObserver]

Global Configuration Schema Master List

The following table lists every supported configuration property used across the five sub-packages.

Core & Common Config

| Key | Type | Default | Description | | :--- | :--- | :--- | :--- | | NODE_ENV | string | development | Runtime environment. | | PORT | number | 3000 | HTTP port. |

Redis & Cache Config

| Key | Type | Default | Description | | :--- | :--- | :--- | :--- | | REDIS_HOST | string | localhost | Redis server address. | | REDIS_PORT | number | 6379 | Redis server port. | | REDIS_PASSWORD| string | null | Optional auth password. |

Notification Hub Config

| Key | Type | Description | | :--- | :--- | :--- | | EMAIL_USER | string | SMTP Username. | | EMAIL_PASS | string | SMTP App Password. | | TWILIO_SID | string | Twilio Account SID. | | TELEGRAM_TOKEN| string | BotFather Token. |


Technical Appendix: The Giant FAQ Registry (100+ Items)

This registry is a living document of questions, edge cases, and architectural inquiries collected from developers across the BTS Soft ecosystem.


Phase 1: General Ecosystem & Architecture

Q1: Why was the core package split into five sub-packages? A: To allow for "Tree-Shaking" and reduced bundle sizes in microservices that only need a subset of the functionality. While @bts-soft/core bundles everything, you can also install @bts-soft/cache independently.

Q2: What is the primary advantage of using ULIDs over UUIDs in BaseEntity? A: ULIDs are lexicographically sortable. This means that database indexes for primary keys stay efficient as they are inserted in order, unlike UUIDs which cause index fragmentation.

Q3: Is this package compatible with Fastify? A: Yes. All core modules are built on top of standard NestJS abstractions, making them compatible with both Express and Fastify adapters.

Q4: How does the meta-package handle versioning? A: @bts-soft/core acts as a "BOM" (Bill of Materials). When you update @bts-soft/core, it automatically pulls in the validated, compatible versions of all five sub-packages.

Q5: Can I override the global interceptors? A: Yes. While setupInterceptors(app) adds the defaults, you can still apply controller-level or method-level interceptors that will execute after the global ones.

Q6: What happens if I don't provide a Cloudinary API key? A: The UploadService will fail during initialization or throw an descriptive error when the first upload command is executed.

Q7: How do I contribute a new notification channel? A: 1. Create a new class implementing INotificationChannel. 2. Update the NotificationChannelFactory. 3. Add the necessary config to NotificationConfigService.

Q8: Does the package support multi-tenancy? A: The architecture supports it at the logic level (e.g., prefixing Redis keys), but there is no built-in "Tenant Selector" strategy yet.


Phase 2: Validation & Enterprise Security

Q9: Why does @TextField convert everything to lowercase by default? A: To ensure data consistency in searches and database indexes. If you need case-sensitive fields, use the CapitalField or standard class-validator decorators.

Q10: Is the SQL Injection regex 100% foolproof? A: No regex is perfect, but ours covers the top 95% of common injection patterns (UNION, SELECT, --, etc.). It acts as a primary defensive layer, complemented by TypeORM’s parameterized queries.

Q11: Can I use @PhoneField for American numbers? A: Yes, pass 'US' as the first argument to the decorator: @PhoneField('US').

Q12: Why is @IsOptional() included in most composite decorators? A: In our experience, most API update DTOs treat fields as optional. If you need a field to be required, simply add the @IsNotEmpty() decorator above the composite one.

Q13: How do I validate a field that must be exactly 14 characters? A: Use @TextField('Field', 14, 14).

Q14: Does @EmailField check if the domain actually exists? A: No, it performs structural validation (regex) only. For DNS checks, you would need a custom validator or a third-party service integration.

Q15: What is the performance impact of the global SQLi interceptor? A: Negligible. Regex checks on request bodies typically take less than 1ms, even for large payloads.

Q16: Can I use these decorators outside of NestJS? A: No, they are heavily dependent on @nestjs/common and the NestJS decorator metadata system.


Phase 3: High-Performance Caching (Redis)

Q17: Why use ioredis instead of the native redis package? A: ioredis provides superior support for Promises, Clusters, Sentinels, and automatic reconnection logic.

Q18: How do I clear the entire cache? A: Use the flushAll() method in the RedisService (Warning: This is a destructive operation).

Q19: Can I use Redis for session management with this package? A: Absolutely. You can wrap the get and set methods to create a custom session store for express-session or passport.

Q20: What is the maximum size of a value I can store in Redis? A: 512MB, though we recommend keeping cached objects under 100KB for optimal network performance.

Q21: How do I implement a "Wait-for-Lock" logic? A: Use the waitForLock(resource, timeout) helper, which polls the acquireLock method until the lock is available or the timeout is reached.

Q22: Is the Pub/Sub system reliable? A: Redis Pub/Sub is "Fire and Forget." If a subscriber is offline when a message is sent, they will miss it. For reliable messaging, use the Redis Streams support (planned for v2.3).

Q23: How do I handle Redis clusters? A: Pass the cluster node array in the REDIS_HOST environment variable (comma-separated). The service will automatically detect and initialize a Cluster client.


Phase 4: Reliable Notifications (Reliable Hub)

Q24: Why use BullMQ instead of simple Promise.all? A: Because network requests to external APIs (like Twilio) frequently fail or time out. BullMQ ensures that if a message isn't sent, it stays in the queue and retries later.

Q25: Can I send HTML emails with Nodemailer? A: Yes, pass the html property inside the channelOptions object of the NotificationMessage.

Q26: How do I set up a Telegram bot? A: Talk to @BotFather on Telegram to get a token, then set it as TELEGRAM_BOT_TOKEN.

Q27: Does the WhatsApp channel support images? A: Yes, pass mediaUrl in the channelOptions. Twilio will handle the delivery of the media.

Q28: What is the default retry delay? A: It uses an exponential backoff starting at 5 seconds. (5s, 10s, 20s...).

Q29: How do I listen for failed notification jobs? A: Currently, you can check the Redis bull:notifications:failed set or use the BullBoard UI (not included in core).


Phase 5: Media & Uploads (Digital Asset Management)

Q30: What is the maximum file size for video uploads? A: By default, the UploadVideoCommand allows up to 100MB. This limit is set to balance server memory and Cloudinary's chunked uploa