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:
- Protocol Agnostic: Every component is designed to work seamlessly with both REST (OpenAPI) and GraphQL (Apollo).
- Security by Default: Global interceptors and specialized decorators protect against common vulnerabilities like SQL Injection and XSS from the moment the application starts.
- 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:
- Non-Blocking: Your API returns a 200 OK immediately after the job is queued, without waiting for external APIs (like Twilio or Firebase).
- Strict Retries: If a provider is down, the system automatically retries with an exponential backoff policy.
- 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:
Strategy Pattern: The
IUploadStrategyinterface allows you to define how files are stored. The default implementation isCloudinaryUploadStrategy, but you can easily plug in Amazon S3 or Google Cloud Storage.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.
Observer Pattern: Successful and failed uploads trigger events that
IUploadObserverinstances 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_secretThe 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:
ClassSerializerInterceptor: Usesclass-transformerto filter out sensitive fields (marked with@Exclude()) and include computed properties (marked with@Expose()).SqlInjectionInterceptor: A global scanner that intercepts all incoming request payloads (Body, Query, Params) and checks every string against theSQL_INJECTION_REGEX. If a violation is caught, it automatically throws a400 Bad Requestbefore the controller logic is executed.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
createdAtandupdatedAttimestamps. - Lifecycle Hooks: Includes pre-configured
AfterInsert,AfterUpdate, andBeforeRemovelogging 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/configwith built-in validation.ThrottlingModule: Pre-configured rate limiting to prevent Brute-Force and DDoS attacks.TranslationModule: Integratednestjs-i18nsupport 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.
Input Validation: The
RegisterDtouses@EmailField,@NameField, and@PasswordField. The input is automatically cleaned (SQLi protection), name is capitalized ("john doe" -> "John Doe"), and email is lowercased.Duplicate Check (Cache): Before hitting the database, the service checks Redis using
redisService.exists('registration:lock:' + email)to prevent rapid-fire duplicate registrations (Idempotency).Persistence (Common): The
Userentity extendsBaseEntity. It is saved with a auto-generated ULID.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.Response Formatting (Common): The
GeneralResponseInterceptorcatches 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.
Image Upload: The controller receives a stream.
uploadService.uploadImageCoreprocesses it using theCloudinaryUploadStrategyand notifies theLoggingObserver.Metadata Search: The search query is validated via
@TextField('Query', 3, 50).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, andDescriptionFieldin the validation module includes aMatches(SQL_INJECTION_REGEX)check. This ensures that potentially malicious strings are caught before they ever reach your service. - Infrastructure Level: The
SqlInjectionInterceptorprovides 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|testPORT: Default 3000
Redis Configuration
REDIS_HOST: e.g.,localhostREDIS_PORT: Default6379REDIS_PASSWORD: Optional
Notifications Configuration
EMAIL_HOST,EMAIL_PORT,EMAIL_USER,EMAIL_PASS,EMAIL_SENDERTWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_SMS_NUMBER,TWILIO_WHATSAPP_NUMBERTELEGRAM_BOT_TOKENFIREBASE_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>. Returnsnullif 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:
- Trims whitespace and dashes.
- Handles
00prefix by converting to+. - Automatically detects Egyptian
01formats and prepends+20. - 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
nullif key does not exist.
set(key: string, value: any, ttl?: number): Promise<void>
- Params:
ttldefaults to 3600 seconds. - Serialization: Automatic
JSON.stringifyfor objects.
del(key: string): Promise<void>
- Purpose: Permanent removal of a key.
exists(key: string): Promise<boolean>
- Returns:
trueif key exists,falseotherwise.
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
@SentimentFieldand@SpamScannerusing 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
- [ ] Install the BTS Soft VS Code Extension Pack.
- [ ] Clone the Core Samples Repository.
- [ ] Complete the "Hello World" tutorial for the Validation module.
- [ ] Successfully send a test email via the Notification service.
Week 2: Intermediate Patterns
- [ ] Implement a Cache-Aside logic for a database entity.
- [ ] Create a custom Upload Observer for audit logging.
- [ ] Build a GraphQL Input Type using the core decorators.
Week 3: Production Readiness
- [ ] Perform a SQL Injection Simulation on your local API.
- [ ] Run a Load Test (1,000 RPS) using
k6. - [ ] 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:
- Validation Stage: The
UploadServicechecks the file extension and size against the protocol limits (e.g., 5MB for images). - Command Preparation: A specific command is instantiated (e.g.,
UploadImageCommand) with the incoming stream and target folder. - Execution (Strategy): The command calls the
IUploadStrategy.upload()method. By default, this pipes the stream directly to Cloudinary's secure servers. - Observer Notification: On success or failure, the
LoggingObserver(or your custom observer) is notified to log the event or trigger a DB update. - 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 --> IUploadStrategyWhy 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
MockUploadStrategyin 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: resultWhy this matters:
- Single Responsibility: The
UploadServicedoesn'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
