@bts-soft/core 3.1.6 → 3.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1151 -2676
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,71 +1,108 @@
|
|
|
1
1
|
# @bts-soft/core
|
|
2
2
|
|
|
3
|
-
](https://www.npmjs.com/package/@bts-soft/core)
|
|
4
|
+
[](https://nodejs.org)
|
|
5
|
+
[](https://nestjs.com)
|
|
6
|
+
[](https://www.typescriptlang.org)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
**Author: [Omar Sabry](https://omarsabry.vercel.app/) — Back-End Engineer**
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
`@bts-soft/core` is a meta-package that consolidates the entire BTS Soft infrastructure ecosystem into a single, cohesive installation. Rather than managing five independent packages with their own versioning and peer dependency resolution, `@bts-soft/core` pins all packages at compatible, tested versions and exposes every export through a single entry point.
|
|
16
|
+
|
|
17
|
+
Installing `@bts-soft/core` gives you immediate access to:
|
|
11
18
|
|
|
12
|
-
|
|
19
|
+
- [@bts-soft/validation](https://www.npmjs.com/package/@bts-soft/validation) — Composite validation decorators, rate limiters, SQL injection prevention
|
|
20
|
+
- [@bts-soft/cache](https://www.npmjs.com/package/@bts-soft/cache) — Enterprise Redis abstraction layer with 15 specialized domain services
|
|
21
|
+
- [@bts-soft/notifications](https://www.npmjs.com/package/@bts-soft/notifications) — Multi-channel queued notification engine (Email, SMS, Push, Chat)
|
|
22
|
+
- [@bts-soft/upload](https://www.npmjs.com/package/@bts-soft/upload) — Production-grade media orchestration with chunked uploads and CDN integration
|
|
23
|
+
- [@bts-soft/common](https://www.npmjs.com/package/@bts-soft/common) — Standard library: interceptors, base entities, ORM adapters, infrastructure modules
|
|
13
24
|
|
|
14
25
|
---
|
|
15
26
|
|
|
16
27
|
## Table of Contents
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
|
|
29
|
+
1. [Why a Meta-Package](#why-a-meta-package)
|
|
30
|
+
2. [Installation](#installation)
|
|
31
|
+
3. [System Architecture and Design Principles](#system-architecture-and-design-principles)
|
|
32
|
+
4. [Package: @bts-soft/validation](#package-bts-softvalidation)
|
|
33
|
+
5. [Package: @bts-soft/cache](#package-bts-softcache)
|
|
34
|
+
6. [Package: @bts-soft/notifications](#package-bts-softnotifications)
|
|
35
|
+
7. [Package: @bts-soft/upload](#package-bts-softupload)
|
|
36
|
+
8. [Package: @bts-soft/common](#package-bts-softcommon)
|
|
37
|
+
9. [Cross-Package Integration Scenarios](#cross-package-integration-scenarios)
|
|
38
|
+
10. [Security Model](#security-model)
|
|
39
|
+
11. [Environment Variable Reference](#environment-variable-reference)
|
|
40
|
+
12. [Testing and Verification](#testing-and-verification)
|
|
41
|
+
13. [FAQ](#faq)
|
|
30
42
|
|
|
31
43
|
---
|
|
32
44
|
|
|
33
|
-
##
|
|
45
|
+
## Why a Meta-Package
|
|
46
|
+
|
|
47
|
+
In a large NestJS monorepo or a microservice mesh, every service needs the same foundational infrastructure: caching, validation, file uploads, and notification dispatching. Without coordination, teams end up manually aligning package versions, discovering peer-dependency conflicts at runtime, and duplicating module setup code.
|
|
48
|
+
|
|
49
|
+
`@bts-soft/core` solves this by:
|
|
50
|
+
|
|
51
|
+
- Pinning all five sub-packages at versions that have been integration-tested together
|
|
52
|
+
- Re-exporting everything from a single entry point so import paths stay clean
|
|
53
|
+
- Providing a single version bump point when the underlying packages update
|
|
34
54
|
|
|
35
|
-
|
|
55
|
+
Consuming a single package instead of five also makes audit pipelines, SBOM generation, and dependency management significantly simpler.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
36
60
|
|
|
37
61
|
```bash
|
|
38
62
|
npm install @bts-soft/core
|
|
39
63
|
```
|
|
40
64
|
|
|
41
|
-
|
|
65
|
+
Peer dependencies required in your host application:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install @nestjs/common @nestjs/core
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Minimum runtime requirements:
|
|
42
72
|
|
|
43
|
-
|
|
73
|
+
| Requirement | Version |
|
|
74
|
+
| :--- | :--- |
|
|
75
|
+
| Node.js | >= 20.17.0 |
|
|
76
|
+
| npm | >= 10.0.0 |
|
|
77
|
+
| NestJS | >= 11.0.0 |
|
|
44
78
|
|
|
45
|
-
|
|
79
|
+
### Quickstart
|
|
46
80
|
|
|
47
81
|
```typescript
|
|
48
82
|
import { Module } from '@nestjs/common';
|
|
49
|
-
import {
|
|
50
|
-
ConfigModule,
|
|
51
|
-
|
|
52
|
-
ThrottlingModule
|
|
83
|
+
import {
|
|
84
|
+
ConfigModule,
|
|
85
|
+
RedisModule,
|
|
86
|
+
ThrottlingModule,
|
|
87
|
+
GraphqlModule,
|
|
88
|
+
NotificationModule,
|
|
89
|
+
UploadModule,
|
|
53
90
|
} from '@bts-soft/core';
|
|
54
91
|
|
|
55
92
|
@Module({
|
|
56
93
|
imports: [
|
|
57
94
|
ConfigModule,
|
|
95
|
+
RedisModule,
|
|
58
96
|
ThrottlingModule,
|
|
59
|
-
GraphqlModule,
|
|
60
|
-
|
|
97
|
+
GraphqlModule.forRoot({ autoSchemaFile: true }),
|
|
98
|
+
NotificationModule,
|
|
99
|
+
UploadModule,
|
|
61
100
|
],
|
|
62
101
|
})
|
|
63
102
|
export class AppModule {}
|
|
64
103
|
```
|
|
65
104
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
In your `main.ts`, utilize the core infrastructure to set up global interceptors and launch banners:
|
|
105
|
+
In `main.ts`:
|
|
69
106
|
|
|
70
107
|
```typescript
|
|
71
108
|
import { NestFactory } from '@nestjs/core';
|
|
@@ -74,14 +111,9 @@ import { setupInterceptors, displayAppBanner } from '@bts-soft/core';
|
|
|
74
111
|
|
|
75
112
|
async function bootstrap() {
|
|
76
113
|
const app = await NestFactory.create(AppModule);
|
|
77
|
-
|
|
78
|
-
// Apply standard BTS-Soft global interceptors (Serialization, SQLi Protection, Formatting)
|
|
79
114
|
setupInterceptors(app);
|
|
80
|
-
|
|
81
115
|
const port = process.env.PORT || 3000;
|
|
82
116
|
await app.listen(port);
|
|
83
|
-
|
|
84
|
-
// Display the professional terminal banner
|
|
85
117
|
displayAppBanner('My Service', port);
|
|
86
118
|
}
|
|
87
119
|
bootstrap();
|
|
@@ -89,2961 +121,1404 @@ bootstrap();
|
|
|
89
121
|
|
|
90
122
|
---
|
|
91
123
|
|
|
92
|
-
##
|
|
93
|
-
|
|
94
|
-
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.
|
|
124
|
+
## System Architecture and Design Principles
|
|
95
125
|
|
|
96
|
-
###
|
|
126
|
+
### High-Level Component Map
|
|
97
127
|
|
|
98
|
-
|
|
128
|
+
```
|
|
129
|
+
@bts-soft/core (Meta-Package)
|
|
130
|
+
|
|
|
131
|
+
┌───────────────────┼───────────────────┐
|
|
132
|
+
│ │ │
|
|
133
|
+
@bts-soft/validation @bts-soft/notifications @bts-soft/upload
|
|
134
|
+
Decorators Queued Multi-Channel Chunked Upload
|
|
135
|
+
Rate Limiters Engine CDN Orchestration
|
|
136
|
+
SQLi Guards │ │
|
|
137
|
+
│ └─────────┐ ┌───────────┘
|
|
138
|
+
│ ▼ ▼
|
|
139
|
+
@bts-soft/common @bts-soft/cache
|
|
140
|
+
Interceptors Redis Facade
|
|
141
|
+
ORM Adapters ◄────── 15 Domain Services
|
|
142
|
+
Infrastructure (shared by all packages)
|
|
143
|
+
```
|
|
99
144
|
|
|
100
|
-
|
|
101
|
-
2. **Security by Default**: Global interceptors and specialized decorators protect against common vulnerabilities like SQL Injection and XSS from the moment the application starts.
|
|
102
|
-
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.
|
|
145
|
+
### Request Lifecycle
|
|
103
146
|
|
|
104
|
-
|
|
147
|
+
```
|
|
148
|
+
Client ──► CommonThrottlerGuard ──[rate limit exceeded]──► 429 Too Many Requests
|
|
149
|
+
│
|
|
150
|
+
[within threshold]
|
|
151
|
+
│
|
|
152
|
+
▼
|
|
153
|
+
SqlInjectionInterceptor ──[SQLi pattern found]──► 400 Bad Request
|
|
154
|
+
│
|
|
155
|
+
[payload clean]
|
|
156
|
+
│
|
|
157
|
+
▼
|
|
158
|
+
IdempotencyInterceptor ──[duplicate key in Redis]──► Cached Response
|
|
159
|
+
│
|
|
160
|
+
[new request]
|
|
161
|
+
│
|
|
162
|
+
▼
|
|
163
|
+
NestJS ValidationPipe (transform + validate DTO)
|
|
164
|
+
│
|
|
165
|
+
▼
|
|
166
|
+
Controller / Resolver
|
|
167
|
+
│
|
|
168
|
+
▼
|
|
169
|
+
GeneralResponseInterceptor (wrap in unified envelope)
|
|
170
|
+
│
|
|
171
|
+
▼
|
|
172
|
+
Client ◄── { success, statusCode, data, timeStamp }
|
|
173
|
+
```
|
|
105
174
|
|
|
106
|
-
|
|
175
|
+
### Design Patterns in Use
|
|
107
176
|
|
|
108
|
-
|
|
|
177
|
+
| Pattern | Package | Implementation |
|
|
109
178
|
| :--- | :--- | :--- |
|
|
110
|
-
| `@bts-soft/
|
|
111
|
-
| `@bts-soft/
|
|
112
|
-
| `@bts-soft/
|
|
113
|
-
| `@bts-soft/upload` |
|
|
114
|
-
| `@bts-soft/
|
|
115
|
-
|
|
116
|
-
|
|
179
|
+
| **Facade** | `@bts-soft/cache` | `RedisService` provides a single API surface over 15 internal domain services |
|
|
180
|
+
| **Strategy** | `@bts-soft/upload` | `IUploadStrategy` decouples Local Disk and Cloudinary backends |
|
|
181
|
+
| **Strategy** | `@bts-soft/common` | `IdGenerator` swaps between ULID, UUID, Snowflake, and CUID2 at runtime |
|
|
182
|
+
| **Command** | `@bts-soft/upload` | Each media type (image, video, audio, raw) is an encapsulated command object |
|
|
183
|
+
| **Observer** | `@bts-soft/upload` | `IUploadObserver` broadcasts lifecycle events (success, fail, delete) |
|
|
184
|
+
| **Chain of Responsibility** | `@bts-soft/common` | Interceptor chain: ClassSerializer, SqlInjection, GeneralResponse |
|
|
185
|
+
| **Adapter** | `@bts-soft/common` | ORM adapters for TypeORM, Mongoose, Sequelize, Prisma |
|
|
186
|
+
| **Decorator** | `@bts-soft/validation` | Composite decorators apply validation, transformation, and GraphQL metadata in one annotation |
|
|
187
|
+
| **Proxy / Deduplication** | `@bts-soft/common` | `IdempotencyInterceptor` returns cached Redis responses for duplicate requests |
|
|
188
|
+
| **Factory** | `@bts-soft/common` | `ConfigModule.forRoot()`, `ThrottlingModule.forRoot()`, `GraphqlModule.forRoot()` |
|
|
117
189
|
|
|
118
|
-
|
|
190
|
+
### Three Core Design Pillars
|
|
119
191
|
|
|
120
|
-
|
|
192
|
+
**1. Protocol Agnosticism**
|
|
193
|
+
Every component operates identically on REST (Express) and GraphQL (Apollo). Validation decorators apply `@Field()` metadata for GraphQL and omit it for REST using a single `isGraphql` flag. Rate limiting guards extract request context from both HTTP and Apollo execution contexts. The response formatter detects GraphQL contexts and skips HTTP envelope wrapping.
|
|
121
194
|
|
|
122
|
-
|
|
195
|
+
**2. Security by Default**
|
|
196
|
+
From the moment `setupInterceptors(app)` is called, every incoming request passes through SQL injection scanning before reaching any controller. Every text-based validation decorator applies a negative lookahead regex against common SQL keywords, double-hyphen comments, and statement terminators. Sensitive response fields are stripped by the `ClassSerializerInterceptor` before transmission.
|
|
123
197
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
3. **Protocol Agnostic**: Native support for both REST (Express) and GraphQL (Apollo), allowing a single DTO to serve both architectures via the `isGraphql` flag.
|
|
127
|
-
4. **Policy-Driven Passwords**: Comprehensive password validation with three configurable complexity levels.
|
|
198
|
+
**3. Extensibility Without Coupling**
|
|
199
|
+
Provider switching (Local disk to Cloudinary, Redis to in-memory, SMTP to SES) is achieved through environment variables and interface implementations, not code changes. New notification channels, upload strategies, or rate-limiting algorithms can be registered without modifying the core service.
|
|
128
200
|
|
|
129
201
|
---
|
|
130
202
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
#### 1. Identity & Security
|
|
134
|
-
- **`@PasswordField`**: Supports `ALPHANUMERIC`, `SYMBOLIC`, and `COMPREHENSIVE` complexity scenarios.
|
|
135
|
-
- **`@NationalIdField`**: Specialized for Egyptian National IDs (14 digits) with auto-stripping of non-digits.
|
|
136
|
-
- **`@IdField`**: Flexible validation for generic IDs (ULID, UUID, etc) with length enforcement.
|
|
203
|
+
## Package: @bts-soft/validation
|
|
137
204
|
|
|
138
|
-
|
|
139
|
-
- **`@NameField`**: Validates names and automatically applies title-case (e.g., "omar sabry" → "Omar Sabry").
|
|
140
|
-
- **`@CapitalTextField`**: Generic text field with auto-capitalization for proper nouns like cities.
|
|
141
|
-
- **`@TextField`**: Standard input field with length limits and strict SQL injection checks.
|
|
142
|
-
- **`@DescriptionField`**: Designed for long-form content with support for newlines and special punctuation.
|
|
143
|
-
- **`@UsernameField`**: Enforces system-level handle rules (3-30 chars, starts with a letter).
|
|
205
|
+
[](https://www.npmjs.com/package/@bts-soft/validation)
|
|
144
206
|
|
|
145
|
-
|
|
146
|
-
- **`@EmailField`**: Validates email format and normalizes input to lowercase.
|
|
147
|
-
- **`@PhoneField`**: Validates international phone numbers using `libphonenumber-js` with regional support.
|
|
148
|
-
- **`@UrlField`**: Validates web addresses and ensures lowercase normalization.
|
|
149
|
-
- **`@NumberField`**: Supports integers and floats with flexible min/max constraints.
|
|
150
|
-
- **`@BooleanField`**: Strict boolean checking for logic flags.
|
|
151
|
-
- **`@DateField`**: Handles date strings and Date objects with automatic type conversion.
|
|
207
|
+
An enterprise-grade validation, sanitization, and rate-limiting toolkit. Decorators are composite: a single annotation applies structural validation, type checking, SQL injection prevention, and `class-transformer` normalization simultaneously. The package is protocol-agnostic: the same decorator annotates a REST DTO and a GraphQL `@InputType()` without modification.
|
|
152
208
|
|
|
153
|
-
|
|
209
|
+
### Decorator Reference
|
|
154
210
|
|
|
155
|
-
|
|
211
|
+
#### Identity and Security
|
|
156
212
|
|
|
157
|
-
|
|
158
|
-
|
|
213
|
+
| Decorator | Signature | Description |
|
|
214
|
+
| :--- | :--- | :--- |
|
|
215
|
+
| `@IdField` | `(id, length?, nullable?, isGraphql?, optional?)` | Validates fixed-length identifiers such as ULIDs (26 chars) or UUIDs. Checks SQLi patterns. |
|
|
216
|
+
| `@NationalIdField` | `(nullable?, isGraphql?, optional?)` | Egyptian National ID: exactly 14 digits, starts with `2` or `3`. Strips non-digits automatically. |
|
|
217
|
+
| `@PasswordField` | `(min?, max?, complexity?, nullable?, isGraphql?, optional?, checkSql?)` | Configurable complexity: `ALPHANUMERIC`, `SYMBOLIC`, or `COMPREHENSIVE`. |
|
|
159
218
|
|
|
160
|
-
|
|
161
|
-
@NameField('Full Name')
|
|
162
|
-
name: string;
|
|
219
|
+
Password complexity levels:
|
|
163
220
|
|
|
164
|
-
|
|
165
|
-
|
|
221
|
+
| Level | Rules |
|
|
222
|
+
| :--- | :--- |
|
|
223
|
+
| `ALPHANUMERIC` | At least one uppercase, one lowercase, one digit |
|
|
224
|
+
| `SYMBOLIC` | At least one uppercase, one lowercase, one special character |
|
|
225
|
+
| `COMPREHENSIVE` | At least one uppercase, one lowercase, one digit, and one special character |
|
|
166
226
|
|
|
167
|
-
|
|
168
|
-
password: string;
|
|
169
|
-
}
|
|
170
|
-
```
|
|
227
|
+
#### String and Text
|
|
171
228
|
|
|
172
|
-
|
|
229
|
+
| Decorator | Signature | Auto-Transform | Description |
|
|
230
|
+
| :--- | :--- | :--- | :--- |
|
|
231
|
+
| `@TextField` | `(text, min?, max?, nullable?, isGraphql?, optional?, checkSql?)` | Lowercase | General-purpose input. Allows Latin, Arabic, digits, spaces, basic punctuation. |
|
|
232
|
+
| `@CapitalTextField` | `(text, min?, max?, nullable?, isGraphql?, optional?, checkSql?)` | Title Case | Proper nouns: city names, countries, titles. Letters and Arabic only. |
|
|
233
|
+
| `@NameField` | `(text, min?, max?, nullable?, isGraphql?, optional?, checkSql?)` | Title Case | Human names. Latin and Arabic alphabets only. |
|
|
234
|
+
| `@UsernameField` | `(nullable?, isGraphql?, optional?, checkSql?)` | None | 3-30 chars, alphanumeric and underscore, must start with a letter. |
|
|
235
|
+
| `@DescriptionField` | `(text, min?, max?, nullable?, isGraphql?, optional?, checkSql?)` | Lowercase | Long-form content. Allows newlines, punctuation, Arabic text. Default 10-2000 chars. |
|
|
173
236
|
|
|
237
|
+
#### Contact and Web
|
|
174
238
|
|
|
175
|
-
|
|
239
|
+
| Decorator | Signature | Auto-Transform | Description |
|
|
240
|
+
| :--- | :--- | :--- | :--- |
|
|
241
|
+
| `@EmailField` | `(nullable?, isGraphql?, optional?)` | Lowercase | RFC-compliant email validation with SQLi check. |
|
|
242
|
+
| `@PhoneField` | `(format?, nullable?, isGraphql?, optional?, checkSql?)` | Strip non-digits | Validates against ISO country code using `libphonenumber-js`. Default: `EG`. |
|
|
243
|
+
| `@UrlField` | `(text?, nullable?, isGraphql?, optional?, checkSql?)` | Lowercase | Enforces URL structure including protocol. |
|
|
176
244
|
|
|
177
|
-
|
|
245
|
+
#### Primitives
|
|
178
246
|
|
|
179
|
-
|
|
180
|
-
|
|
247
|
+
| Decorator | Signature | GraphQL Type | Description |
|
|
248
|
+
| :--- | :--- | :--- | :--- |
|
|
249
|
+
| `@NumberField` | `(text, min?, max?, isInt?, nullable?, isGraphql?, optional?)` | `Int` or `Float` | Range-bounded numeric validation. |
|
|
250
|
+
| `@BooleanField` | `(nullable?, isGraphql?, optional?)` | `Boolean` | Strict boolean enforcement. |
|
|
251
|
+
| `@DateField` | `(text, nullable?, isGraphql?, optional?)` | `Date` | ISO string or epoch number parsed into `Date` via `class-transformer`. |
|
|
252
|
+
| `@EnumField` | `(enumType, name, nullable?, isGraphql?, optional?)` | Enum | Validates value exists within the TypeScript enum's values. |
|
|
181
253
|
|
|
182
|
-
|
|
254
|
+
### Usage Examples
|
|
183
255
|
|
|
184
|
-
|
|
256
|
+
```typescript
|
|
257
|
+
import {
|
|
258
|
+
NameField, EmailField, PasswordField, PasswordComplexity,
|
|
259
|
+
PhoneField, IdField, DescriptionField, NumberField,
|
|
260
|
+
} from '@bts-soft/core';
|
|
185
261
|
|
|
186
|
-
|
|
262
|
+
export class RegisterUserDto {
|
|
263
|
+
@NameField('Full Name', 2, 80, false, false, false)
|
|
264
|
+
name: string; // "omar sabry" -> "Omar Sabry"
|
|
187
265
|
|
|
188
|
-
|
|
266
|
+
@EmailField(false, false, false)
|
|
267
|
+
email: string; // "USER@Example.COM" -> "user@example.com"
|
|
189
268
|
|
|
190
|
-
|
|
269
|
+
@PasswordField(12, 32, PasswordComplexity.COMPREHENSIVE, false, false, false)
|
|
270
|
+
password: string;
|
|
191
271
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
5. **100% Verified Infrastructure**: Every operation is backed by comprehensive E2E tests running on real Redis instances.
|
|
272
|
+
@PhoneField('EG', false, false, true)
|
|
273
|
+
phone: string; // "+20 101 234 5678" -> "+201012345678"
|
|
274
|
+
}
|
|
275
|
+
```
|
|
197
276
|
|
|
198
|
-
|
|
277
|
+
For GraphQL `@InputType()` classes, set `isGraphql = true` (or omit the flag, since `true` is the default):
|
|
199
278
|
|
|
200
|
-
|
|
279
|
+
```typescript
|
|
280
|
+
import { InputType } from '@nestjs/graphql';
|
|
281
|
+
import { NameField, EmailField, PasswordField, PasswordComplexity } from '@bts-soft/core';
|
|
201
282
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
- `del(key)`: Removes keys from the database.
|
|
283
|
+
@InputType()
|
|
284
|
+
export class RegisterInput {
|
|
285
|
+
@NameField('Full Name')
|
|
286
|
+
name: string;
|
|
207
287
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
- `incrByFloat(key, n)`: Handles precise floating-point increments.
|
|
288
|
+
@EmailField()
|
|
289
|
+
email: string;
|
|
211
290
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const lockValue = crypto.randomUUID();
|
|
215
|
-
const acquired = await redisService.acquireLock('order:process:123', lockValue, 5000);
|
|
216
|
-
|
|
217
|
-
if (acquired) {
|
|
218
|
-
try {
|
|
219
|
-
// Critical section logic
|
|
220
|
-
} finally {
|
|
221
|
-
await redisService.releaseLock('order:process:123', lockValue);
|
|
222
|
-
}
|
|
291
|
+
@PasswordField(8, 32, PasswordComplexity.COMPREHENSIVE)
|
|
292
|
+
password: string;
|
|
223
293
|
}
|
|
224
294
|
```
|
|
225
295
|
|
|
226
|
-
|
|
227
|
-
```typescript
|
|
228
|
-
// Subscriber (Dedicated connection handled automatically)
|
|
229
|
-
await redisService.subscribe('system_updates', (msg) => {
|
|
230
|
-
console.log('Update received:', JSON.parse(msg));
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// Publisher
|
|
234
|
-
await redisService.publish('system_updates', { status: 'OK', timestamp: Date.now() });
|
|
235
|
-
```
|
|
296
|
+
### Rate Limiting Suite
|
|
236
297
|
|
|
237
|
-
|
|
298
|
+
The package implements all five rate-limiting algorithms from *System Design Interview Vol. 1, Chapter 4* as a pluggable NestJS guard. Both REST and GraphQL contexts are supported natively.
|
|
238
299
|
|
|
239
|
-
|
|
300
|
+
```
|
|
301
|
+
Incoming Request
|
|
302
|
+
│
|
|
303
|
+
▼
|
|
304
|
+
RateLimiterGuard
|
|
305
|
+
│
|
|
306
|
+
▼
|
|
307
|
+
Extract Client Key (IP / JWT user ID / API key)
|
|
308
|
+
│
|
|
309
|
+
▼
|
|
310
|
+
algorithm.consume(key)
|
|
311
|
+
│
|
|
312
|
+
├── [Allowed] ──► Set X-RateLimit-* Headers ──► Pass to Handler
|
|
313
|
+
│
|
|
314
|
+
└── [Denied] ──► Set Retry-After Header ──► 429 HttpException
|
|
315
|
+
```
|
|
240
316
|
|
|
241
|
-
|
|
|
317
|
+
| Algorithm | Storage Complexity | Best For |
|
|
242
318
|
| :--- | :--- | :--- |
|
|
243
|
-
| **
|
|
244
|
-
| **
|
|
245
|
-
| **
|
|
246
|
-
| **
|
|
247
|
-
| **
|
|
319
|
+
| **Token Bucket** | O(1) per key | General APIs with burst tolerance |
|
|
320
|
+
| **Leaking Bucket** | O(1) per key | Smooth, predictable outflow for payment APIs |
|
|
321
|
+
| **Fixed Window Counter** | O(1) per key | Low-overhead login attempt limits |
|
|
322
|
+
| **Sliding Window Log** | O(limit) per key | High-security endpoints requiring absolute precision |
|
|
323
|
+
| **Sliding Window Counter** | O(1) per key | High-scale endpoints needing precision without log overhead |
|
|
248
324
|
|
|
249
|
-
|
|
325
|
+
Storage backends auto-negotiate: if a Redis connection is available via `@bts-soft/cache`, the distributed `RedisStore` is used. On connection failure, the guard transparently falls back to an in-memory store.
|
|
250
326
|
|
|
251
|
-
|
|
327
|
+
```typescript
|
|
328
|
+
import { RateLimit, RateLimiterAlgorithm } from '@bts-soft/core';
|
|
329
|
+
|
|
330
|
+
@Controller('payments')
|
|
331
|
+
export class PaymentController {
|
|
332
|
+
@RateLimit({
|
|
333
|
+
algorithm: RateLimiterAlgorithm.SLIDING_WINDOW_LOG,
|
|
334
|
+
limit: 5,
|
|
335
|
+
windowMs: 60_000,
|
|
336
|
+
keyExtractor: (req) => req.user?.id ?? req.ip,
|
|
337
|
+
})
|
|
338
|
+
@Post('charge')
|
|
339
|
+
async chargeCard() {}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
252
342
|
|
|
253
|
-
|
|
343
|
+
Response headers appended on every rate-limited response:
|
|
254
344
|
|
|
255
|
-
|
|
345
|
+
| Header | Description |
|
|
346
|
+
| :--- | :--- |
|
|
347
|
+
| `X-RateLimit-Limit` | Maximum requests per window |
|
|
348
|
+
| `X-RateLimit-Remaining` | Remaining requests in current window |
|
|
349
|
+
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
|
|
350
|
+
| `Retry-After` | Seconds to wait before retrying (only on 429) |
|
|
256
351
|
|
|
257
|
-
###
|
|
352
|
+
### SQL Injection Prevention
|
|
258
353
|
|
|
259
|
-
|
|
354
|
+
The `SQL_INJECTION_REGEX` applied to every string field is:
|
|
260
355
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
356
|
+
```
|
|
357
|
+
/^(?!.*(SELECT|INSERT|DELETE|UPDATE|DROP|UNION|EXEC|TRUNCATE|ALTER|CREATE|--|;)).*$/i
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The negative lookahead `(?! ...)` fails validation the moment any SQL keyword, double-hyphen comment sequence, or statement terminator is detected anywhere in the input string. The check is case-insensitive to catch obfuscated payloads.
|
|
264
361
|
|
|
265
362
|
---
|
|
266
363
|
|
|
267
|
-
|
|
364
|
+
## Package: @bts-soft/cache
|
|
268
365
|
|
|
269
|
-
|
|
270
|
-
- **Technology**: Nodemailer.
|
|
271
|
-
- **Support**: Standard SMTP, SES, Gmail, and custom transport configurations.
|
|
272
|
-
- **Dynamic Overrides**: Support for per-message SMTP configuration and attachments.
|
|
366
|
+
[](https://www.npmjs.com/package/@bts-soft/cache)
|
|
273
367
|
|
|
274
|
-
|
|
275
|
-
- **Provider**: Twilio WhatsApp Business API.
|
|
276
|
-
- **Normalizer**: Automatic handling of regional phone number formats.
|
|
368
|
+
A production-grade Redis abstraction layer built on the Modular Facade Pattern. Developers interact with a single `RedisService` injectable while the internal architecture distributes responsibility across 15 single-responsibility domain services.
|
|
277
369
|
|
|
278
|
-
|
|
279
|
-
- **Provider**: Twilio SMS.
|
|
280
|
-
- **Reliability**: Optimized for high-deliverability transactional messaging.
|
|
370
|
+
### Module Initialization Flow
|
|
281
371
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
372
|
+
```
|
|
373
|
+
NestJS App ──► RedisConfigService
|
|
374
|
+
│ resolve REDIS_HOST / PORT / PASSWORD / DB
|
|
375
|
+
▼
|
|
376
|
+
createRedisClient
|
|
377
|
+
│
|
|
378
|
+
▼
|
|
379
|
+
node-redis v4 ──TCP──► Redis Server
|
|
380
|
+
│
|
|
381
|
+
◄── Connection confirmed
|
|
382
|
+
|
|
383
|
+
NestJS App ──► RedisHealth.onModuleInit()
|
|
384
|
+
│ PING
|
|
385
|
+
▼
|
|
386
|
+
Redis Server
|
|
387
|
+
│ PONG
|
|
388
|
+
▼
|
|
389
|
+
Module ready
|
|
390
|
+
```
|
|
285
391
|
|
|
286
|
-
|
|
287
|
-
- **Technology**: Firebase Admin SDK.
|
|
288
|
-
- **Support**: Native Android/iOS push notifications and web push.
|
|
392
|
+
### 15 Internal Domain Services
|
|
289
393
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
394
|
+
| Domain Service | Operations |
|
|
395
|
+
| :--- | :--- |
|
|
396
|
+
| **CoreRedisService** | `set`, `setForever`, `get`, `getOrSet`, `del`, `update`, `mSet`, `setNX` |
|
|
397
|
+
| **StringRedisService** | `getSet`, `strlen`, `append`, `getRange`, `setRange`, `mGet` |
|
|
398
|
+
| **NumberRedisService** | `incr`, `incrBy`, `incrByFloat`, `decr`, `decrBy` |
|
|
399
|
+
| **HashRedisService** | `hSet`, `hGet`, `hGetAll`, `hDel`, `hExists`, `hKeys`, `hVals`, `hLen`, `hIncrBy`, `hIncrByFloat`, `hSetNX` |
|
|
400
|
+
| **ListORedisService** | `lPush`, `rPush`, `lPop`, `rPop`, `lRange`, `lLen`, `lIndex`, `lInsert`, `lRem`, `lTrim`, `rPopLPush`, `lSet`, `lPos` |
|
|
401
|
+
| **OperationRedisService** | `sAdd`, `sRem`, `sMembers`, `sIsMember`, `sCard`, `sPop`, `sMove`, `sDiff`, `sInter`, `sUnion` and store variants |
|
|
402
|
+
| **SortedORedisService** | `zAdd`, `zRange`, `zRangeByScore`, `zRevRange`, `zCard`, `zScore`, `zRank`, `zRevRank`, `zIncrBy`, `zRem`, `zRemRangeByRank`, `zRemRangeByScore`, `zCount`, `zUnionStore`, `zInterStore` |
|
|
403
|
+
| **GeoRedisService** | `geoAdd`, `geoPos`, `geoDist`, `geoHash`, `geoRemove` |
|
|
404
|
+
| **HyperLogLogRedisService** | `pfAdd`, `pfCount`, `pfMerge`, `pfDebug`, `pfClear` |
|
|
405
|
+
| **LockRedisService** | `acquireLock`, `releaseLock`, `extendLock`, `isLocked`, `getLockValue`, `waitForLock` |
|
|
406
|
+
| **PubSubRedisService** | `publish`, `subscribe`, `pSubscribe`, `unsubscribe`, `pUnsubscribe`, `getSubscriptions`, `getChannels`, `getSubCount`, `createMessageHandler` |
|
|
407
|
+
| **TransactionRedisService** | `multiExecute`, `watch`, `unwatch`, `withTransaction`, `discard`, `transactionGetSet` |
|
|
408
|
+
| **StreamRedisService** | `xAdd`, `xRead`, `xGroupCreate`, `xReadGroup`, `xAck`, `xLen` |
|
|
409
|
+
| **BitmapRedisService** | `setBit`, `getBit`, `bitCount`, `bitOp` |
|
|
410
|
+
| **UtilityRedisService** | `exists`, `expire`, `ttl`, `persist`, `pttl`, `delByPattern`, `eval` |
|
|
411
|
+
|
|
412
|
+
### Setup
|
|
293
413
|
|
|
294
|
-
|
|
295
|
-
|
|
414
|
+
```typescript
|
|
415
|
+
import { RedisModule } from '@bts-soft/core';
|
|
296
416
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
417
|
+
@Module({
|
|
418
|
+
imports: [RedisModule],
|
|
419
|
+
})
|
|
420
|
+
export class AppModule {}
|
|
421
|
+
```
|
|
300
422
|
|
|
301
|
-
|
|
423
|
+
Environment variables:
|
|
302
424
|
|
|
303
|
-
|
|
425
|
+
| Variable | Default | Required | Description |
|
|
426
|
+
| :--- | :--- | :--- | :--- |
|
|
427
|
+
| `REDIS_HOST` | `localhost` | No | Redis hostname |
|
|
428
|
+
| `REDIS_PORT` | `6379` | No | Redis port |
|
|
429
|
+
| `REDIS_PASSWORD` | — | No | Authentication password |
|
|
430
|
+
| `REDIS_DB` | `0` | No | Database index (0-15) |
|
|
431
|
+
| `REDIS_TTL` | `3600` | No | Default TTL in seconds |
|
|
304
432
|
|
|
305
|
-
|
|
306
|
-
- **Handlebars Engine**: Centralized template rendering for all channels via TemplateService.
|
|
307
|
-
- **Dynamic I18n**: Support for per-recipient language resolution using nestjs-i18n.
|
|
308
|
-
- **Contextual Data**: Pass complex objects to templates for real-time personalization.
|
|
433
|
+
### Core API Reference
|
|
309
434
|
|
|
310
|
-
####
|
|
311
|
-
- **Channel Registry**: A plugin-based system that allows for dynamic channel registration and discovery.
|
|
312
|
-
- **BullMQ Priority**: Native support for job priorities (1-100) to ensure critical alerts are processed before marketing broadcasts.
|
|
313
|
-
- **Comprehensive Testing**: Backed by a 100% coverage unit testing suite and standalone Docker-based E2E verification.
|
|
435
|
+
#### Key-Value Operations
|
|
314
436
|
|
|
315
|
-
|
|
437
|
+
| Method | Signature | Description |
|
|
438
|
+
| :--- | :--- | :--- |
|
|
439
|
+
| `set` | `(key, value, ttl?)` | Store any value with automatic JSON serialization. Default TTL: 3600s. |
|
|
440
|
+
| `setForever` | `(key, value)` | Store without expiration. For permanent configuration state. |
|
|
441
|
+
| `get` | `<T>(key)` | Retrieve and automatically JSON-parse into typed object. |
|
|
442
|
+
| `getOrSet` | `<T>(key, factory, ttl?)` | Cache-aside: returns cached value or calls factory and caches result. |
|
|
443
|
+
| `del` | `(key)` | Delete a key. |
|
|
444
|
+
| `update` | `(key, value, ttl?)` | Delete and re-set atomically. |
|
|
445
|
+
| `mSet` | `(data)` | Multi-set via Redis pipeline. |
|
|
446
|
+
| `setNX` | `(key, value, ttl?)` | Set only if the key does not exist. Returns boolean. |
|
|
447
|
+
| `exists` | `(key)` | Returns true if the key exists. |
|
|
448
|
+
| `expire` | `(key, seconds)` | Update TTL on an existing key. |
|
|
449
|
+
| `ttl` | `(key)` | Returns remaining TTL in seconds. |
|
|
450
|
+
|
|
451
|
+
#### Atomic Counters
|
|
452
|
+
|
|
453
|
+
| Method | Signature | Description |
|
|
454
|
+
| :--- | :--- | :--- |
|
|
455
|
+
| `incr` | `(key)` | Atomically increment by 1. |
|
|
456
|
+
| `incrBy` | `(key, n)` | Atomically increment by integer N. |
|
|
457
|
+
| `incrByFloat` | `(key, n)` | Atomically increment by float N. |
|
|
458
|
+
| `decr` | `(key)` | Atomically decrement by 1. |
|
|
459
|
+
| `decrBy` | `(key, n)` | Atomically decrement by integer N. |
|
|
316
460
|
|
|
461
|
+
#### Distributed Locking
|
|
317
462
|
|
|
318
|
-
|
|
463
|
+
All locks use Lua scripts for atomic token verification. A lock can only be released by the exact process that acquired it.
|
|
319
464
|
|
|
320
|
-
|
|
465
|
+
```
|
|
466
|
+
Service A ──► Redis: acquireLock("lock:order:99", "token-A", 10000)
|
|
467
|
+
Redis ──► Service A: OK (lock acquired)
|
|
321
468
|
|
|
322
|
-
|
|
469
|
+
Service B ──► Redis: acquireLock("lock:order:99", "token-B", 10000)
|
|
470
|
+
Redis ──► Service B: null (lock denied — already held by A)
|
|
323
471
|
|
|
324
|
-
|
|
472
|
+
[Service A executes critical business logic]
|
|
325
473
|
|
|
326
|
-
|
|
327
|
-
|
|
474
|
+
Service A ──► Redis: releaseLock("lock:order:99", "token-A")
|
|
475
|
+
Redis: Lua script verifies token-A matches stored token
|
|
476
|
+
Redis ──► Service A: 1 (key deleted)
|
|
328
477
|
|
|
329
|
-
|
|
478
|
+
Service B ──► Redis: acquireLock("lock:order:99", "token-B", 10000)
|
|
479
|
+
Redis ──► Service B: OK (lock acquired)
|
|
480
|
+
```
|
|
330
481
|
|
|
331
|
-
|
|
482
|
+
```typescript
|
|
483
|
+
import { Injectable } from '@nestjs/common';
|
|
484
|
+
import { RedisService } from '@bts-soft/core';
|
|
485
|
+
import { randomUUID } from 'crypto';
|
|
332
486
|
|
|
333
|
-
|
|
487
|
+
@Injectable()
|
|
488
|
+
export class PaymentService {
|
|
489
|
+
constructor(private readonly redis: RedisService) {}
|
|
334
490
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
491
|
+
async processOrder(orderId: string): Promise<void> {
|
|
492
|
+
const lockKey = `lock:order:${orderId}`;
|
|
493
|
+
const token = randomUUID();
|
|
338
494
|
|
|
339
|
-
|
|
495
|
+
const acquired = await this.redis.acquireLock(lockKey, token, 10_000);
|
|
496
|
+
if (!acquired) return; // Another instance is already processing this order
|
|
340
497
|
|
|
341
|
-
|
|
498
|
+
try {
|
|
499
|
+
await this.runPaymentGateway(orderId);
|
|
500
|
+
} finally {
|
|
501
|
+
await this.redis.releaseLock(lockKey, token);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
```
|
|
342
506
|
|
|
343
|
-
|
|
344
|
-
| :--- | :--- | :--- | :--- |
|
|
345
|
-
| **Images** | `jpg`, `png`, `webp`, `gif` | 5 MB | Auto-optimization and non-destructive resizing. |
|
|
346
|
-
| **Videos** | `mp4`, `webm`, `avi`, `mov` | 100 MB | Chunked upload support with duration extraction. |
|
|
347
|
-
| **Audio** | `mp3`, `wav`, `ogg`, `m4a` | 50 MB | Optimized for playback and metadata extraction. |
|
|
348
|
-
| **Raw Files** | `pdf`, `doc`, `zip`, `txt` | 10 MB | Stored as 'raw' binary with original headers. |
|
|
349
|
-
| **3D Models** | `glb`, `gltf`, `fbx`, `obj`, `stl` | 100 MB | Stored as binary models with high-capacity limits. |
|
|
507
|
+
#### Pub/Sub
|
|
350
508
|
|
|
351
|
-
|
|
509
|
+
The subscriber uses a dedicated, isolated connection separate from the main command connection to prevent blocking.
|
|
352
510
|
|
|
511
|
+
```
|
|
512
|
+
┌─────────────────────────┐ ┌────────────────────────────┐
|
|
513
|
+
│ Publisher Service │ │ Subscriber Service │
|
|
514
|
+
│ │ │ │
|
|
515
|
+
│ Publisher Code │ │ Dedicated Sub Connection │
|
|
516
|
+
│ │ │ │ │ │
|
|
517
|
+
│ │ PUBLISH │ │ │ Deliver │
|
|
518
|
+
│ ▼ │ │ ▼ │
|
|
519
|
+
│ Primary Connection ────┼──────► │ Redis Server │
|
|
520
|
+
│ │ network │ │ │
|
|
521
|
+
└─────────────────────────┘ │ │ Parse JSON │
|
|
522
|
+
│ ▼ │
|
|
523
|
+
│ Handler Callback │
|
|
524
|
+
└────────────────────────────┘
|
|
525
|
+
```
|
|
353
526
|
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
|
|
527
|
+
```typescript
|
|
528
|
+
// Subscribe on module init
|
|
529
|
+
await redisService.subscribe('events:orders', (data, channel) => {
|
|
530
|
+
console.log(`Event on ${channel}:`, data);
|
|
531
|
+
});
|
|
357
532
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
533
|
+
// Wildcard pattern subscription
|
|
534
|
+
await redisService.pSubscribe('user:*', (data, channel) => {
|
|
535
|
+
console.log(`Pattern match on ${channel}:`, data);
|
|
536
|
+
});
|
|
362
537
|
|
|
363
|
-
|
|
364
|
-
|
|
538
|
+
// Publish from any service
|
|
539
|
+
await redisService.publish('events:orders', { orderId: '123', status: 'shipped' });
|
|
365
540
|
```
|
|
366
541
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
## Deep Dive: `@bts-soft/common`
|
|
542
|
+
#### Sorted Sets: Leaderboards
|
|
370
543
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
544
|
+
```typescript
|
|
545
|
+
await redis.zAdd('leaderboard:weekly', 9850, 'user:alice');
|
|
546
|
+
await redis.zIncrBy('leaderboard:weekly', 150, 'user:alice');
|
|
547
|
+
const top10 = await redis.zRevRange('leaderboard:weekly', 0, 9);
|
|
548
|
+
const rank = await redis.zRevRank('leaderboard:weekly', 'user:alice');
|
|
549
|
+
```
|
|
374
550
|
|
|
375
|
-
|
|
551
|
+
#### Bitmaps: Daily Active Users
|
|
376
552
|
|
|
377
|
-
|
|
553
|
+
```typescript
|
|
554
|
+
// Record login for user ID 4201 on a specific date
|
|
555
|
+
await redis.setBit('dau:2025-01-15', 4201, 1);
|
|
378
556
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
2. **`SqlInjectionInterceptor`**:
|
|
383
|
-
A global scanner that intercepts all incoming request payloads (Body, Query, Params) and checks every string against a predefined regex. If a violation is caught, it automatically throws a `400 Bad Request` before the controller logic is executed.
|
|
557
|
+
// Check if user was active
|
|
558
|
+
const active = await redis.getBit('dau:2025-01-15', 4201); // 0 or 1
|
|
384
559
|
|
|
385
|
-
|
|
386
|
-
|
|
560
|
+
// Count total active users for the day
|
|
561
|
+
const total = await redis.bitCount('dau:2025-01-15');
|
|
387
562
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
{
|
|
391
|
-
"success": true,
|
|
392
|
-
"statusCode": 200,
|
|
393
|
-
"message": "Request successful",
|
|
394
|
-
"timeStamp": "2024-03-21T10:00:00.000Z",
|
|
395
|
-
"data": { ... },
|
|
396
|
-
"items": [ ... ],
|
|
397
|
-
"pagination": {
|
|
398
|
-
"total": 100,
|
|
399
|
-
"page": 1,
|
|
400
|
-
"limit": 10
|
|
401
|
-
}
|
|
402
|
-
}
|
|
563
|
+
// Weekly active users using bitwise OR across daily bitmaps
|
|
564
|
+
await redis.bitOp('OR', 'wau:2025-W03', 'dau:2025-01-13', 'dau:2025-01-14', 'dau:2025-01-15');
|
|
403
565
|
```
|
|
404
566
|
|
|
405
|
-
|
|
567
|
+
#### HyperLogLog: Unique Visitor Counting
|
|
406
568
|
|
|
407
|
-
|
|
569
|
+
```typescript
|
|
570
|
+
// Uses approximately 12KB of memory regardless of unique visitor count
|
|
571
|
+
await redis.pfAdd('hll:page:/home', visitorId);
|
|
572
|
+
const uniqueCount = await redis.pfCount('hll:page:/home');
|
|
408
573
|
|
|
409
|
-
|
|
410
|
-
|
|
574
|
+
// Merge multiple HyperLogLogs
|
|
575
|
+
await redis.pfMerge('hll:all-pages', 'hll:page:/home', 'hll:page:/about');
|
|
576
|
+
```
|
|
411
577
|
|
|
412
|
-
|
|
413
|
-
- **Audit Trails**: Automatically generates `createdAt` and `updatedAt` timestamps.
|
|
414
|
-
- **Multi-ORM Support**: Includes dedicated bases for TypeORM, Sequelize, Mongoose, Prisma, and GraphQL.
|
|
578
|
+
#### Redis Streams: Reliable Event Sourcing
|
|
415
579
|
|
|
416
|
-
|
|
417
|
-
|
|
580
|
+
```typescript
|
|
581
|
+
// Producer appends events
|
|
582
|
+
const messageId = await redis.xAdd('stream:orders', { orderId: '123', amount: '99.99' });
|
|
583
|
+
|
|
584
|
+
// Create consumer group (idempotent)
|
|
585
|
+
try {
|
|
586
|
+
await redis.xGroupCreate('stream:orders', 'workers', '$', true);
|
|
587
|
+
} catch { /* Group already exists */ }
|
|
588
|
+
|
|
589
|
+
// Consumer reads and acknowledges
|
|
590
|
+
const result = await redis.xReadGroup(
|
|
591
|
+
'workers', 'worker-1',
|
|
592
|
+
[{ key: 'stream:orders', id: '>' }],
|
|
593
|
+
10, // Max messages
|
|
594
|
+
5000, // Block for 5 seconds if no messages
|
|
595
|
+
);
|
|
596
|
+
await redis.xAck('stream:orders', 'workers', result[0].id);
|
|
597
|
+
```
|
|
418
598
|
|
|
419
599
|
---
|
|
420
600
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
The common package provides pre-configured NestJS modules:
|
|
601
|
+
## Package: @bts-soft/notifications
|
|
424
602
|
|
|
425
|
-
-
|
|
426
|
-
- **`ThrottlingModule`**: Robust rate limiting that automatically handles both HTTP and GraphQL contexts.
|
|
427
|
-
- **`TranslationModule`**: Integrated `nestjs-i18n` support for multi-language applications (Arabic/English).
|
|
428
|
-
- **`GraphqlModule`**: Standard Apollo Server setup with custom error filters and a context that includes both request and response objects.
|
|
603
|
+
[](https://www.npmjs.com/package/@bts-soft/notifications)
|
|
429
604
|
|
|
430
|
-
|
|
605
|
+
A high-availability multi-channel notification engine. All dispatch operations are queued via BullMQ and processed asynchronously, decoupling API response times from external provider latency.
|
|
431
606
|
|
|
432
|
-
###
|
|
607
|
+
### Architecture
|
|
433
608
|
|
|
434
|
-
|
|
609
|
+
```
|
|
610
|
+
notificationService.send()
|
|
611
|
+
│
|
|
612
|
+
▼ PRE-FLIGHT PIPELINE (synchronous)
|
|
613
|
+
┌─────────────────────────────────────────────────────┐
|
|
614
|
+
│ Expired? ──[yes]──► Discard + Log EXPIRED │
|
|
615
|
+
│ Duplicate key? ──[yes]──► Skip + Log DUPLICATE │
|
|
616
|
+
│ User opted out? ──[yes]──► Skip + Log OPTED_OUT │
|
|
617
|
+
│ Rate limited? ──[yes]──► Skip + Log RATE_LIMITED│
|
|
618
|
+
└─────────────────────────────────────────────────────┘
|
|
619
|
+
│ [all checks pass]
|
|
620
|
+
▼
|
|
621
|
+
Enqueue BullMQ Job ──► API returns 200 immediately to caller
|
|
622
|
+
│
|
|
623
|
+
▼ WORKER PIPELINE (asynchronous)
|
|
624
|
+
┌─────────────────────────────────────────────────────┐
|
|
625
|
+
│ Job expired? ──[yes]──► Update log EXPIRED │
|
|
626
|
+
│ │
|
|
627
|
+
│ Apply i18n + Handlebars template render │
|
|
628
|
+
│ Resolve channel from factory │
|
|
629
|
+
│ Dispatch to provider │
|
|
630
|
+
│ ├── [Success] Update log SENT │
|
|
631
|
+
│ ├── [4xx client err] Update log FAILED │
|
|
632
|
+
│ └── [5xx server err] BullMQ exponential retry │
|
|
633
|
+
└─────────────────────────────────────────────────────┘
|
|
634
|
+
```
|
|
435
635
|
|
|
436
|
-
|
|
437
|
-
- **`displayAppBanner(appName, port)`**: Displays a clean, professional banner in the terminal when the server starts.
|
|
636
|
+
### Supported Channels
|
|
438
637
|
|
|
439
|
-
|
|
638
|
+
| Channel | Technology | Provider |
|
|
639
|
+
| :--- | :--- | :--- |
|
|
640
|
+
| `EMAIL` | Nodemailer | SMTP, Gmail, Outlook, SendGrid, Amazon SES |
|
|
641
|
+
| `SMS` | Twilio / SMS Misr / Vonage | Configurable via `SMS_PROVIDER` env var |
|
|
642
|
+
| `WHATSAPP` | Twilio WhatsApp Business API | E.164 normalization with Egypt fallback |
|
|
643
|
+
| `FIREBASE_FCM` | Firebase Admin SDK | Android, iOS, Web push |
|
|
644
|
+
| `TELEGRAM` | Telegram Bot API | Markdown V2 and HTML parse modes |
|
|
645
|
+
| `DISCORD` | Webhook | Embeds, custom bot identity |
|
|
646
|
+
| `TEAMS` | Incoming Webhooks | Adaptive Message Cards |
|
|
647
|
+
| `MESSENGER` | Meta Graph API | Page-Scoped IDs (PSID) |
|
|
648
|
+
| `SLACK` | Webhook / Slack Web API | Bot messages, channel targeting |
|
|
649
|
+
| `ONESIGNAL` | OneSignal REST API | Cross-platform push notifications |
|
|
650
|
+
| `WEB_PUSH` | VAPID / Web Push Standard | Browser push notifications |
|
|
651
|
+
| `IN_APP` | Pusher Channels | Real-time in-app events |
|
|
652
|
+
| `WEBHOOK` | HTTP POST | Generic signed webhook dispatch |
|
|
653
|
+
|
|
654
|
+
### Setup
|
|
440
655
|
|
|
656
|
+
```typescript
|
|
657
|
+
import { Module } from '@nestjs/common';
|
|
658
|
+
import { RedisModule, NotificationModule } from '@bts-soft/core';
|
|
441
659
|
|
|
442
|
-
|
|
660
|
+
@Module({
|
|
661
|
+
imports: [
|
|
662
|
+
RedisModule,
|
|
663
|
+
NotificationModule,
|
|
664
|
+
],
|
|
665
|
+
})
|
|
666
|
+
export class AppModule {}
|
|
667
|
+
```
|
|
443
668
|
|
|
444
|
-
|
|
669
|
+
### Sending Notifications
|
|
445
670
|
|
|
446
|
-
|
|
671
|
+
```typescript
|
|
672
|
+
import { Injectable } from '@nestjs/common';
|
|
673
|
+
import { NotificationService, ChannelType } from '@bts-soft/core';
|
|
447
674
|
|
|
448
|
-
|
|
675
|
+
@Injectable()
|
|
676
|
+
export class OrderService {
|
|
677
|
+
constructor(private readonly notifications: NotificationService) {}
|
|
678
|
+
|
|
679
|
+
async onOrderCompleted(email: string, orderId: string) {
|
|
680
|
+
// Single channel with Handlebars template
|
|
681
|
+
await this.notifications.send(ChannelType.EMAIL, {
|
|
682
|
+
recipientId: email,
|
|
683
|
+
subject: 'Order Confirmed',
|
|
684
|
+
body: 'Hi {{name}}, your order #{{orderId}} has been confirmed.',
|
|
685
|
+
context: { name: 'Omar', orderId },
|
|
686
|
+
});
|
|
687
|
+
}
|
|
449
688
|
|
|
450
|
-
|
|
689
|
+
async alertTeam(message: string) {
|
|
690
|
+
// Multi-channel bulk dispatch
|
|
691
|
+
await this.notifications.sendBulk([
|
|
692
|
+
{ channel: ChannelType.SLACK, message: { recipientId: '#alerts', body: message } },
|
|
693
|
+
{ channel: ChannelType.DISCORD, message: { body: message } },
|
|
694
|
+
{ channel: ChannelType.TEAMS, message: { body: message } },
|
|
695
|
+
]);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
```
|
|
451
699
|
|
|
452
|
-
|
|
453
|
-
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.
|
|
454
|
-
|
|
455
|
-
2. **Duplicate Check (Cache)**:
|
|
456
|
-
Before hitting the database, the service checks Redis using `redisService.exists('registration:lock:' + email)` to prevent rapid-fire duplicate registrations (Idempotency).
|
|
700
|
+
### Database Integration for Notification Logs
|
|
457
701
|
|
|
458
|
-
|
|
459
|
-
The `User` entity extends `BaseEntity`. It is saved with a auto-generated ULID.
|
|
702
|
+
Implement `INotificationLogRepository` and register it using the `NOTIFICATION_LOG_REPOSITORY` injection token to persist logs to any database:
|
|
460
703
|
|
|
461
|
-
|
|
462
|
-
|
|
704
|
+
```typescript
|
|
705
|
+
import { Injectable } from '@nestjs/common';
|
|
706
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
707
|
+
import { Repository } from 'typeorm';
|
|
708
|
+
import { INotificationLogRepository, NotificationLog } from '@bts-soft/core';
|
|
463
709
|
|
|
464
|
-
|
|
465
|
-
|
|
710
|
+
@Injectable()
|
|
711
|
+
export class DbNotificationLogRepo implements INotificationLogRepository {
|
|
712
|
+
constructor(
|
|
713
|
+
@InjectRepository(NotificationLogEntity)
|
|
714
|
+
private readonly repo: Repository<NotificationLogEntity>,
|
|
715
|
+
) {}
|
|
466
716
|
|
|
467
|
-
|
|
717
|
+
async create(log: Omit<NotificationLog, 'id'>): Promise<NotificationLog> {
|
|
718
|
+
return this.repo.save(this.repo.create(log));
|
|
719
|
+
}
|
|
468
720
|
|
|
469
|
-
|
|
721
|
+
async updateByJobId(jobId: string, update: Partial<NotificationLog>): Promise<void> {
|
|
722
|
+
await this.repo.update({ jobId }, update);
|
|
723
|
+
}
|
|
470
724
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
2. **Metadata Search**:
|
|
475
|
-
The search query is validated via `@TextField('Query', 3, 50)`.
|
|
476
|
-
|
|
477
|
-
3. **Result Caching**:
|
|
478
|
-
Search results are cached in Redis using a Hash (`hSet`). Subsequent searches for the same term are served in under 1ms.
|
|
725
|
+
async findByJobId(jobId: string): Promise<NotificationLog | null> {
|
|
726
|
+
return this.repo.findOne({ where: { jobId } });
|
|
727
|
+
}
|
|
479
728
|
|
|
480
|
-
|
|
729
|
+
async findByRecipientId(recipientId: string): Promise<NotificationLog[]> {
|
|
730
|
+
return this.repo.find({ where: { recipientId }, order: { createdAt: 'DESC' } });
|
|
731
|
+
}
|
|
481
732
|
|
|
482
|
-
|
|
733
|
+
async findAll(filter?: Partial<NotificationLog>): Promise<NotificationLog[]> {
|
|
734
|
+
return this.repo.find({ where: filter as any, order: { createdAt: 'DESC' } });
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
```
|
|
483
738
|
|
|
484
|
-
|
|
739
|
+
Register in your module:
|
|
485
740
|
|
|
486
|
-
|
|
741
|
+
```typescript
|
|
742
|
+
import { NOTIFICATION_LOG_REPOSITORY } from '@bts-soft/core';
|
|
487
743
|
|
|
488
|
-
|
|
744
|
+
@Module({
|
|
745
|
+
providers: [
|
|
746
|
+
{ provide: NOTIFICATION_LOG_REPOSITORY, useClass: DbNotificationLogRepo },
|
|
747
|
+
],
|
|
748
|
+
})
|
|
749
|
+
export class AppModule {}
|
|
750
|
+
```
|
|
489
751
|
|
|
490
|
-
|
|
752
|
+
### User Opt-Out Management
|
|
491
753
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
| `get` | `<T>(key)` | Retrieves and JSON-parses value. | `await get<User>('u1')` |
|
|
496
|
-
| `del` | `(key)` | Deletes a key. | `await del('old_key')` |
|
|
497
|
-
| `mSet` | `(data)` | Multi-set via pipeline. | `await mSet({a:1, b:2})` |
|
|
498
|
-
| `mGet` | `(keys)` | Multi-get. | `await mGet(['a', 'b'])` |
|
|
499
|
-
| `exists`| `(key)` | Checks key existence. | `await exists('token')` |
|
|
500
|
-
| `expire`| `(key, sec)` | Updates TTL. | `await expire('k', 3600)` |
|
|
501
|
-
| `ttl` | `(key)` | Gets remaining seconds. | `await ttl('k')` |
|
|
502
|
-
|
|
503
|
-
#### Atomic Counters & Numeric Groups
|
|
504
|
-
|
|
505
|
-
| Method | Signature | Description | Example |
|
|
506
|
-
| :--- | :--- | :--- | :--- |
|
|
507
|
-
| `incr` | `(key)` | Increments by 1. | `await incr('views')` |
|
|
508
|
-
| `incrBy` | `(key, n)` | Increments by integer N. | `await incrBy('score', 10)` |
|
|
509
|
-
| `decr` | `(key)` | Decrements by 1. | `await decr('retries')` |
|
|
510
|
-
| `decrBy` | `(key, n)` | Decrements by integer N. | `await decrBy('balance', 5)` |
|
|
511
|
-
| `getSet` | `(key, val)` | Set new, return old. | `await getSet('v', 5)` |
|
|
754
|
+
```typescript
|
|
755
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
756
|
+
import { USER_PREFERENCE_REPOSITORY, IUserPreferenceRepository } from '@bts-soft/core';
|
|
512
757
|
|
|
513
|
-
|
|
758
|
+
@Injectable()
|
|
759
|
+
export class PreferenceService {
|
|
760
|
+
constructor(
|
|
761
|
+
@Inject(USER_PREFERENCE_REPOSITORY)
|
|
762
|
+
private readonly prefs: IUserPreferenceRepository,
|
|
763
|
+
) {}
|
|
514
764
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
| `hGet` | `<T>(k, f)` | Gets field value. | `await hGet<string>('u:1', 'n')`|
|
|
519
|
-
| `hGetAll`| `(k)` | Gets all fields as Object. | `await hGetAll('u:1')` |
|
|
520
|
-
| `hDel` | `(k, f)` | Deletes field. | `await hDel('u:1', 'n')` |
|
|
521
|
-
| `hKeys` | `(k)` | Returns all field names. | `await hKeys('u:1')` |
|
|
522
|
-
| `hVals` | `(k)` | Returns all field values. | `await hVals('u:1')` |
|
|
523
|
-
| `hLen` | `(k)` | Field count. | `await hLen('u:1')` |
|
|
524
|
-
| `hIncrBy`| `(k, f, n)` | Incr field by N. | `await hIncrBy('u:1', 'v', 1)`|
|
|
525
|
-
|
|
526
|
-
#### Set Operations (Uniqueness)
|
|
527
|
-
|
|
528
|
-
| Method | Signature | Description | Example |
|
|
529
|
-
| :--- | :--- | :--- | :--- |
|
|
530
|
-
| `sAdd` | `(k, ...m)` | Adds members. | `await sAdd('tags', 'a', 'b')` |
|
|
531
|
-
| `sRem` | `(k, ...m)` | Removes members. | `await sRem('tags', 'a')` |
|
|
532
|
-
| `sMembers`| `(k)` | Returns all members. | `await sMembers('tags')` |
|
|
533
|
-
| `sCard` | `(k)` | Set count. | `await sCard('tags')` |
|
|
534
|
-
| `sIsMember`| `(k, m)` | Membership check. | `await sIsMember('tags', 'a')`|
|
|
535
|
-
| `sInter` | `(...k)` | Set intersection. | `await sInter('s1', 's2')` |
|
|
536
|
-
| `sUnion` | `(...k)` | Set union. | `await sUnion('s1', 's2')` |
|
|
765
|
+
optOut(userId: string, channel: string) {
|
|
766
|
+
return this.prefs.setOptOut(userId, channel, true);
|
|
767
|
+
}
|
|
537
768
|
|
|
538
|
-
|
|
769
|
+
optIn(userId: string, channel: string) {
|
|
770
|
+
return this.prefs.setOptOut(userId, channel, false);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
```
|
|
539
774
|
|
|
540
|
-
|
|
541
|
-
| :--- | :--- | :--- | :--- |
|
|
542
|
-
| `zAdd` | `(k, s, m)` | Add item with score. | `await zAdd('lb', 100, 'u1')` |
|
|
543
|
-
| `zRange` | `(k, s, t)` | Range by index. | `await zRange('lb', 0, 10)` |
|
|
544
|
-
| `zRank` | `(k, m)` | Member rank. | `await zRank('lb', 'u1')` |
|
|
545
|
-
| `zScore` | `(k, m)` | Member score. | `await zScore('lb', 'u1')` |
|
|
546
|
-
| `zRem` | `(k, m)` | Remove member. | `await zRem('lb', 'u1')` |
|
|
775
|
+
### BullMQ Retry Policy
|
|
547
776
|
|
|
548
|
-
|
|
777
|
+
| Setting | Value |
|
|
778
|
+
| :--- | :--- |
|
|
779
|
+
| Max attempts | 3 |
|
|
780
|
+
| Strategy | Exponential backoff |
|
|
781
|
+
| Initial delay | 5,000ms |
|
|
782
|
+
| Progression | 5s -> 10s -> 20s |
|
|
549
783
|
|
|
550
|
-
|
|
551
|
-
| :--- | :--- | :--- | :--- |
|
|
552
|
-
| `geoAdd` | `(k, lo, la, m)` | Store coordinate. | `await geoAdd('idx', 31, 30, 'P')`|
|
|
553
|
-
| `geoDist` | `(m1, m2, u)` | Distance calculation. | `await geoDist('P1', 'P2', 'km')`|
|
|
554
|
-
| `geoPos` | `(k, m)` | Get Lat/Long. | `await geoPos('idx', 'P')` |
|
|
784
|
+
Client errors (4xx) are not retried. Only provider errors (5xx, connection timeouts) trigger retry logic.
|
|
555
785
|
|
|
556
|
-
|
|
786
|
+
---
|
|
557
787
|
|
|
558
|
-
|
|
559
|
-
| :--- | :--- | :--- | :--- |
|
|
560
|
-
| `pfAdd` | `(k, ...e)` | Add elements. | `await pfAdd('uv', '1', '2')` |
|
|
561
|
-
| `pfCount` | `(...k)` | Get cardinality. | `await pfCount('uv')` |
|
|
788
|
+
## Package: @bts-soft/upload
|
|
562
789
|
|
|
563
|
-
|
|
790
|
+
[](https://www.npmjs.com/package/@bts-soft/upload)
|
|
564
791
|
|
|
565
|
-
|
|
792
|
+
A production-grade media orchestration library supporting single-file uploads, large-file chunked uploads with resume capability, real-time Server-Sent Events progress streaming, and multi-provider storage backends.
|
|
566
793
|
|
|
567
|
-
|
|
794
|
+
### System Architecture
|
|
568
795
|
|
|
569
|
-
|
|
796
|
+
```
|
|
797
|
+
Client Request
|
|
798
|
+
│
|
|
799
|
+
▼
|
|
800
|
+
RateLimiterService (Token Bucket)
|
|
801
|
+
├── [rate limited] ──► 429 Too Many Requests
|
|
802
|
+
│
|
|
803
|
+
▼
|
|
804
|
+
FileValidatorService (extension + size check)
|
|
805
|
+
├── [invalid] ──► 400 Bad Request
|
|
806
|
+
│
|
|
807
|
+
▼
|
|
808
|
+
Upload Controllers (REST / GraphQL)
|
|
809
|
+
│
|
|
810
|
+
├── [single file] ──► UploadService
|
|
811
|
+
│ │
|
|
812
|
+
│ Upload Commands
|
|
813
|
+
│ │
|
|
814
|
+
│ Strategy Pattern
|
|
815
|
+
│ ├── LocalDiskUploadStrategy
|
|
816
|
+
│ └── CloudinaryUploadStrategy
|
|
817
|
+
│
|
|
818
|
+
└── [large file] ──► ChunkedUploadService
|
|
819
|
+
│
|
|
820
|
+
LocalChunkStorage
|
|
821
|
+
│ [final chunk received]
|
|
822
|
+
▼
|
|
823
|
+
UploadQueueService ──► BullMQ Queue
|
|
824
|
+
│
|
|
825
|
+
UploadProcessor
|
|
826
|
+
│
|
|
827
|
+
Merge all chunks
|
|
828
|
+
│
|
|
829
|
+
──► Upload Commands (same path)
|
|
830
|
+
│ (progress updates)
|
|
831
|
+
UploadJobService
|
|
832
|
+
│
|
|
833
|
+
SSE Controller ──► Client
|
|
834
|
+
```
|
|
570
835
|
|
|
571
|
-
|
|
836
|
+
### Design Patterns
|
|
572
837
|
|
|
573
|
-
|
|
574
|
-
* **Infrastructure Level**: The `SqlInjectionInterceptor` provides a global safety net by scanning every request property.
|
|
838
|
+
**Strategy Pattern:** The `IUploadStrategy` interface decouples file storage from the application. Switching between Local Disk and Cloudinary requires only an environment variable change.
|
|
575
839
|
|
|
576
|
-
|
|
840
|
+
```
|
|
841
|
+
UploadService
|
|
842
|
+
│
|
|
843
|
+
▼
|
|
844
|
+
IUploadStrategy (interface)
|
|
845
|
+
│
|
|
846
|
+
├──► LocalDiskUploadStrategy (UPLOAD_PROVIDER=local)
|
|
847
|
+
│
|
|
848
|
+
└──► CloudinaryUploadStrategy (UPLOAD_PROVIDER=cloudinary)
|
|
849
|
+
```
|
|
577
850
|
|
|
578
|
-
|
|
579
|
-
- Sensitive internal data (like user passwords or internal DB IDs) are stripped from the response.
|
|
580
|
-
- Only fields explicitly marked with `@Expose()` are sent to the client.
|
|
851
|
+
**Command Pattern:** Each media type (image, video, audio, raw file, 3D model) is encapsulated in its own command object carrying type-specific processing logic such as chunked video upload or image optimization.
|
|
581
852
|
|
|
582
|
-
|
|
853
|
+
**Observer Pattern:** `IUploadObserver` implementations register to receive lifecycle events for auditing, analytics, or cleanup pipelines.
|
|
583
854
|
|
|
584
|
-
|
|
855
|
+
### Setup
|
|
585
856
|
|
|
586
857
|
```typescript
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
858
|
+
import { UploadModule } from '@bts-soft/core';
|
|
859
|
+
|
|
860
|
+
@Module({ imports: [UploadModule] })
|
|
861
|
+
export class AppModule {}
|
|
590
862
|
```
|
|
591
863
|
|
|
592
|
-
###
|
|
864
|
+
### Media Specifications
|
|
593
865
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
866
|
+
| Type | Extensions | Default Size Limit | Processing |
|
|
867
|
+
| :--- | :--- | :--- | :--- |
|
|
868
|
+
| Images | `jpg`, `png`, `webp`, `gif` | 5 MB | Auto-optimization, `fetch_format: auto` |
|
|
869
|
+
| Videos | `mp4`, `webm`, `avi`, `mov` | 100 MB | Chunked upload (6MB chunks), duration extraction |
|
|
870
|
+
| Audio | `mp3`, `wav`, `ogg`, `m4a` | 50 MB | Waveform generation via Cloudinary video resource type |
|
|
871
|
+
| Raw Files | `pdf`, `doc`, `docx`, `zip`, `txt` | 10 MB | Stored as raw binary with original headers |
|
|
872
|
+
| 3D Models | `glb`, `gltf`, `fbx`, `obj`, `stl` | 100 MB | Binary model storage |
|
|
598
873
|
|
|
599
|
-
###
|
|
874
|
+
### API Reference
|
|
600
875
|
|
|
601
|
-
|
|
876
|
+
**UploadService:**
|
|
602
877
|
|
|
603
|
-
|
|
878
|
+
```typescript
|
|
879
|
+
uploadImageCore(file: UploadStreamInput): Promise<UploadResult>
|
|
880
|
+
uploadVideoCore(file: UploadStreamInput): Promise<UploadResult>
|
|
881
|
+
uploadAudioCore(file: UploadStreamInput): Promise<UploadResult>
|
|
882
|
+
uploadFileCore(file: UploadStreamInput): Promise<UploadResult>
|
|
883
|
+
uploadModel3dCore(file: UploadStreamInput): Promise<UploadResult>
|
|
884
|
+
deleteImage(url: string): Promise<boolean>
|
|
885
|
+
deleteVideo(url: string): Promise<boolean>
|
|
886
|
+
deleteAudio(url: string): Promise<boolean>
|
|
887
|
+
deleteFile(url: string): Promise<boolean>
|
|
888
|
+
```
|
|
604
889
|
|
|
605
|
-
|
|
890
|
+
**ChunkedUploadService:**
|
|
606
891
|
|
|
607
|
-
|
|
892
|
+
```typescript
|
|
893
|
+
initiateUpload(filename, size, type, fileHash?, userId?): Promise<{ jobId: string, status: string }>
|
|
894
|
+
uploadChunk(jobId, chunkIndex, totalChunks, buffer, fileHash?, userId?): Promise<{ progress: number, completed: boolean }>
|
|
895
|
+
getUploadedChunks(jobId): Promise<number[]>
|
|
896
|
+
```
|
|
608
897
|
|
|
609
|
-
|
|
898
|
+
### Chunked Upload Implementation
|
|
610
899
|
|
|
611
|
-
|
|
900
|
+
```typescript
|
|
901
|
+
// Backend REST controller
|
|
902
|
+
@Controller('upload')
|
|
903
|
+
export class UploadController {
|
|
904
|
+
constructor(private readonly chunked: ChunkedUploadService) {}
|
|
905
|
+
|
|
906
|
+
@Post('chunk/initiate')
|
|
907
|
+
initiateUpload(@Body() body: { filename: string; size: number; type: string }) {
|
|
908
|
+
return this.chunked.initiateUpload(body.filename, body.size, body.type);
|
|
909
|
+
}
|
|
612
910
|
|
|
613
|
-
|
|
911
|
+
@Post('chunk/upload')
|
|
912
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
913
|
+
uploadChunk(
|
|
914
|
+
@Body() body: { jobId: string; chunkIndex: string; totalChunks: string },
|
|
915
|
+
@UploadedFile() file: Express.Multer.File,
|
|
916
|
+
) {
|
|
917
|
+
return this.chunked.uploadChunk(
|
|
918
|
+
body.jobId,
|
|
919
|
+
parseInt(body.chunkIndex, 10),
|
|
920
|
+
parseInt(body.totalChunks, 10),
|
|
921
|
+
file.buffer,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
614
924
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
925
|
+
@Get('chunk/uploaded/:jobId')
|
|
926
|
+
getUploadedChunks(@Param('jobId') jobId: string) {
|
|
927
|
+
return this.chunked.getUploadedChunks(jobId);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
```
|
|
618
931
|
|
|
619
|
-
|
|
620
|
-
- `REDIS_HOST`: e.g., `localhost`
|
|
621
|
-
- `REDIS_PORT`: Default `6379`
|
|
622
|
-
- `REDIS_PASSWORD`: Optional
|
|
932
|
+
Frontend with resume support:
|
|
623
933
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
934
|
+
```typescript
|
|
935
|
+
async function uploadFileInChunks(file: File, userId: string) {
|
|
936
|
+
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB per chunk
|
|
937
|
+
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
938
|
+
|
|
939
|
+
const { jobId } = await fetch('/upload/chunk/initiate', {
|
|
940
|
+
method: 'POST',
|
|
941
|
+
headers: { 'Content-Type': 'application/json' },
|
|
942
|
+
body: JSON.stringify({ filename: file.name, size: file.size, type: 'video', userId }),
|
|
943
|
+
}).then(r => r.json());
|
|
944
|
+
|
|
945
|
+
// Resume: get already uploaded chunk indexes
|
|
946
|
+
const uploadedChunks: number[] = await fetch(`/upload/chunk/uploaded/${jobId}`).then(r => r.json());
|
|
947
|
+
|
|
948
|
+
// Subscribe to SSE progress stream
|
|
949
|
+
const es = new EventSource(`/upload/jobs/${jobId}/stream`);
|
|
950
|
+
es.onmessage = ({ data }) => {
|
|
951
|
+
const { type, progress, url } = JSON.parse(data);
|
|
952
|
+
if (type === 'completed') { console.log('Final URL:', url); es.close(); }
|
|
953
|
+
if (type === 'progress') { console.log(`${progress}%`); }
|
|
954
|
+
};
|
|
629
955
|
|
|
630
|
-
|
|
631
|
-
|
|
956
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
957
|
+
if (uploadedChunks.includes(i)) continue; // Skip already uploaded chunks
|
|
632
958
|
|
|
633
|
-
|
|
959
|
+
const chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
|
|
960
|
+
const form = new FormData();
|
|
961
|
+
form.append('jobId', jobId);
|
|
962
|
+
form.append('chunkIndex', String(i));
|
|
963
|
+
form.append('totalChunks', String(totalChunks));
|
|
964
|
+
form.append('file', chunk);
|
|
634
965
|
|
|
635
|
-
|
|
966
|
+
await fetch('/upload/chunk/upload', { method: 'POST', body: form });
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
```
|
|
636
970
|
|
|
637
|
-
|
|
971
|
+
### Custom Observer
|
|
638
972
|
|
|
639
|
-
|
|
973
|
+
```typescript
|
|
974
|
+
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
975
|
+
import { IUploadObserver, UploadObserverManager } from '@bts-soft/core';
|
|
640
976
|
|
|
641
|
-
|
|
977
|
+
@Injectable()
|
|
978
|
+
export class AuditObserver implements IUploadObserver, OnModuleInit {
|
|
979
|
+
constructor(private readonly manager: UploadObserverManager) {}
|
|
642
980
|
|
|
643
|
-
|
|
644
|
-
Best for User Profiles, Product Details, or Settings.
|
|
981
|
+
onModuleInit() { this.manager.register(this); }
|
|
645
982
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
async getProfile(@Param('id') id: string) {
|
|
650
|
-
const cacheKey = `user:profile:${id}`;
|
|
651
|
-
|
|
652
|
-
// 1. Try to get from Cache
|
|
653
|
-
const cachedUser = await this.redis.get<User>(cacheKey);
|
|
654
|
-
if (cachedUser) return cachedUser;
|
|
655
|
-
|
|
656
|
-
// 2. Fetch from DB if not in Cache
|
|
657
|
-
const user = await this.userRepo.findOneBy({ id });
|
|
658
|
-
if (!user) throw new NotFoundException();
|
|
659
|
-
|
|
660
|
-
// 3. Store in Cache with 10 min TTL
|
|
661
|
-
await this.redis.set(cacheKey, user, 600);
|
|
662
|
-
|
|
663
|
-
return user;
|
|
664
|
-
}
|
|
665
|
-
```
|
|
983
|
+
async onUploadSuccess(url: string, type: string) {
|
|
984
|
+
console.log(`Uploaded: ${url} (type: ${type})`);
|
|
985
|
+
}
|
|
666
986
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
async userProfile(@Args('id') id: string) {
|
|
671
|
-
const cacheKey = `user:profile:${id}`;
|
|
672
|
-
|
|
673
|
-
return await this.redis.getOrSet(cacheKey, async () => {
|
|
674
|
-
return await this.userRepo.findOneBy({ id });
|
|
675
|
-
}, 600);
|
|
676
|
-
}
|
|
677
|
-
```
|
|
987
|
+
async onUploadFail(error: Error) {
|
|
988
|
+
console.error(`Upload failed: ${error.message}`);
|
|
989
|
+
}
|
|
678
990
|
|
|
679
|
-
|
|
680
|
-
|
|
991
|
+
async onDeleteSuccess(url: string) {
|
|
992
|
+
console.log(`Deleted: ${url}`);
|
|
993
|
+
}
|
|
681
994
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const key = `limit:${userId}:${action}`;
|
|
685
|
-
const count = await this.redis.incr(key);
|
|
686
|
-
|
|
687
|
-
if (count === 1) {
|
|
688
|
-
await this.redis.expire(key, 60); // Set 1 min window on first attempt
|
|
995
|
+
async onDeleteFail(error: Error, url: string) {
|
|
996
|
+
console.error(`Delete failed for ${url}: ${error.message}`);
|
|
689
997
|
}
|
|
690
|
-
|
|
691
|
-
return count <= 5; // Allow 5 actions per minute
|
|
692
998
|
}
|
|
693
999
|
```
|
|
694
1000
|
|
|
695
1001
|
---
|
|
696
1002
|
|
|
697
|
-
|
|
1003
|
+
## Package: @bts-soft/common
|
|
698
1004
|
|
|
699
|
-
|
|
700
|
-
Sends notifications based on user opt-in channels.
|
|
1005
|
+
[](https://www.npmjs.com/package/@bts-soft/common)
|
|
701
1006
|
|
|
702
|
-
|
|
703
|
-
async notifyUser(user: User, payload: any) {
|
|
704
|
-
const jobs = [];
|
|
705
|
-
|
|
706
|
-
if (user.wantsEmail) {
|
|
707
|
-
jobs.push(this.notif.send(ChannelType.EMAIL, {
|
|
708
|
-
recipientId: user.email,
|
|
709
|
-
...payload
|
|
710
|
-
}));
|
|
711
|
-
}
|
|
1007
|
+
The standard library of the BTS Soft ecosystem. Provides the foundation shared by all services: interceptors, exception filters, base entities, ORM adapters, ID generation, infrastructure modules, distributed locking, and idempotency.
|
|
712
1008
|
|
|
713
|
-
|
|
714
|
-
jobs.push(this.notif.send(ChannelType.SMS, {
|
|
715
|
-
recipientId: user.phone,
|
|
716
|
-
...payload
|
|
717
|
-
}));
|
|
718
|
-
}
|
|
1009
|
+
### Request Processing Architecture
|
|
719
1010
|
|
|
720
|
-
|
|
721
|
-
|
|
1011
|
+
```
|
|
1012
|
+
Client Request
|
|
1013
|
+
|
|
|
1014
|
+
v
|
|
1015
|
+
CommonThrottlerGuard
|
|
1016
|
+
|
|
|
1017
|
+
v
|
|
1018
|
+
SqlInjectionInterceptor
|
|
1019
|
+
|
|
|
1020
|
+
v
|
|
1021
|
+
IdempotencyInterceptor
|
|
1022
|
+
|
|
|
1023
|
+
v
|
|
1024
|
+
Guards / Authentication
|
|
1025
|
+
|
|
|
1026
|
+
v
|
|
1027
|
+
Controller / Resolver --[exception]--> RestExceptionFilter / GqlHttpExceptionFilter
|
|
1028
|
+
|
|
|
1029
|
+
v
|
|
1030
|
+
GeneralResponseInterceptor Standard Error Envelope
|
|
1031
|
+
|
|
|
1032
|
+
v
|
|
1033
|
+
Unified JSON Envelope --> Client
|
|
722
1034
|
```
|
|
723
1035
|
|
|
724
|
-
|
|
1036
|
+
### Global Interceptors
|
|
725
1037
|
|
|
726
|
-
|
|
1038
|
+
Call `setupInterceptors(app)` in `main.ts` to activate three interceptors globally in this order:
|
|
727
1039
|
|
|
728
|
-
|
|
729
|
-
|
|
1040
|
+
**1. ClassSerializerInterceptor**
|
|
1041
|
+
Applies `class-transformer` serialization rules. Fields decorated with `@Exclude()` are stripped from responses. Fields decorated with `@Expose()` are included. This prevents sensitive data such as passwords and internal database IDs from leaking to clients.
|
|
730
1042
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1043
|
+
**2. SqlInjectionInterceptor**
|
|
1044
|
+
Recursively scans all incoming request body fields, query string parameters, and path parameters. Any string matching the SQL injection regex causes an immediate `400 Bad Request` before the controller executes. Endpoints processing trusted raw SQL can opt out with `@SkipSqlCheck()`.
|
|
1045
|
+
|
|
1046
|
+
**3. GeneralResponseInterceptor**
|
|
1047
|
+
Wraps every controller return value in the standard response envelope:
|
|
1048
|
+
|
|
1049
|
+
```json
|
|
1050
|
+
{
|
|
1051
|
+
"success": true,
|
|
1052
|
+
"statusCode": 200,
|
|
1053
|
+
"message": "Operation executed successfully",
|
|
1054
|
+
"timeStamp": "2025-01-15T10:00:00.000Z",
|
|
1055
|
+
"data": {},
|
|
1056
|
+
"items": [],
|
|
1057
|
+
"pagination": {
|
|
1058
|
+
"total": 100,
|
|
1059
|
+
"page": 1,
|
|
1060
|
+
"limit": 10
|
|
742
1061
|
}
|
|
743
|
-
|
|
744
|
-
// 3. Update DB
|
|
745
|
-
user.avatarUrl = result.url;
|
|
746
|
-
await this.userRepo.save(user);
|
|
747
1062
|
}
|
|
748
1063
|
```
|
|
749
1064
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
## Frequently Asked Questions (FAQ)
|
|
1065
|
+
GraphQL contexts are detected and bypass HTTP envelope wrapping to preserve Apollo schema integrity.
|
|
753
1066
|
|
|
754
|
-
###
|
|
1067
|
+
### Base Entities
|
|
755
1068
|
|
|
756
|
-
|
|
757
|
-
A: No, version 2.2.4+ requires NestJS 11 due to dependency alignment and peer dependency overrides.
|
|
1069
|
+
#### AgnosticEntity
|
|
758
1070
|
|
|
759
|
-
|
|
760
|
-
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.
|
|
1071
|
+
Pure TypeScript base, no ORM decorators. Generates IDs using `IdGenerator` and maintains `createdAt` and `updatedAt` timestamps.
|
|
761
1072
|
|
|
762
|
-
|
|
1073
|
+
#### ORM-Specific Adapters
|
|
763
1074
|
|
|
764
|
-
|
|
1075
|
+
| Sub-path | Class | ORM |
|
|
1076
|
+
| :--- | :--- | :--- |
|
|
1077
|
+
| `@bts-soft/common/typeorm` | `TypeOrmBaseEntity` | TypeORM with lifecycle hooks (`@AfterInsert`, `@AfterUpdate`, `@BeforeRemove`) |
|
|
1078
|
+
| `@bts-soft/common/mongoose` | `MongooseBaseEntity` | Mongoose with `@Schema({ timestamps: true })` |
|
|
1079
|
+
| `@bts-soft/common/sequelize` | `SequelizeBaseEntity` | Sequelize with `@Table({ timestamps: true })` |
|
|
1080
|
+
| `@bts-soft/common/prisma` | `PrismaBase` | Helper `generateId(strategy?)` and `IPrismaEntity` interface |
|
|
765
1081
|
|
|
766
|
-
|
|
767
|
-
A: You should use the standard `class-validator` decorators (like `@IsString()`) instead of the `@TextField` composite decorators.
|
|
1082
|
+
### ID Generation Strategy
|
|
768
1083
|
|
|
769
|
-
|
|
770
|
-
|
|
1084
|
+
```
|
|
1085
|
+
Entity / ORM Base
|
|
1086
|
+
│
|
|
1087
|
+
▼
|
|
1088
|
+
IdGenerator.generate(strategy?)
|
|
1089
|
+
│
|
|
1090
|
+
├── ULID ──► 128-bit, lexicographically sortable (default)
|
|
1091
|
+
├── UUID v4 ──► RFC4122 fully random 128-bit
|
|
1092
|
+
├── Snowflake ──► 64-bit: timestamp + worker ID + sequence counter
|
|
1093
|
+
└── CUID2 ──► sortable, collision-resistant, URL-safe
|
|
1094
|
+
```
|
|
771
1095
|
|
|
772
|
-
|
|
1096
|
+
| Strategy | Characteristics | Best For |
|
|
1097
|
+
| :--- | :--- | :--- |
|
|
1098
|
+
| **ULID** (default) | 48-bit timestamp + 80-bit random, B-Tree friendly | High-write databases such as PostgreSQL and MySQL |
|
|
1099
|
+
| **UUID v4** | Fully random 128-bit | Compatibility with existing systems |
|
|
1100
|
+
| **Snowflake** | 64-bit, embeds worker ID (0-1023) and sequence (0-4095) | Distributed microservice node coordination |
|
|
1101
|
+
| **CUID2** | Collision-resistant, URL-safe | Short IDs in URLs and external references |
|
|
773
1102
|
|
|
774
|
-
|
|
1103
|
+
```typescript
|
|
1104
|
+
import { IdGenerator } from '@bts-soft/core';
|
|
775
1105
|
|
|
776
|
-
|
|
777
|
-
|
|
1106
|
+
IdGenerator.setDefaultStrategy('snowflake');
|
|
1107
|
+
IdGenerator.setWorkerId(12); // Required for distributed deployments
|
|
778
1108
|
|
|
779
|
-
|
|
1109
|
+
const id = IdGenerator.generate(); // Uses default strategy
|
|
1110
|
+
const ulid = IdGenerator.generate('ulid'); // Override per call
|
|
1111
|
+
```
|
|
780
1112
|
|
|
781
|
-
|
|
1113
|
+
### Infrastructure Modules
|
|
782
1114
|
|
|
783
|
-
|
|
1115
|
+
#### ThrottlingModule
|
|
784
1116
|
|
|
785
|
-
|
|
1117
|
+
Rate limiting for REST and GraphQL using sliding window counters. Supports tiered limits:
|
|
786
1118
|
|
|
787
|
-
### Basic Key-Value Operations
|
|
788
|
-
|
|
789
|
-
#### 1. `set`
|
|
790
|
-
**Description:** Stores a value in Redis with automatic serialization of objects and primitives.
|
|
791
|
-
**Interface:** `set(key: string, value: any, ttl: number = 3600): Promise<void>`
|
|
792
|
-
**Parameters:**
|
|
793
|
-
- `key`: The unique identifier in Redis.
|
|
794
|
-
- `value`: The data to store. Can be a string, number, or plain JavaScript object.
|
|
795
|
-
- `ttl`: Time-To-Live in seconds. Default is 1 hour.
|
|
796
|
-
**Return:** `Promise<void>`
|
|
797
|
-
**Examples:**
|
|
798
1119
|
```typescript
|
|
799
|
-
|
|
800
|
-
await this.redisService.set(`user:${id}`, userData, 3600);
|
|
1120
|
+
import { ThrottlingModule } from '@bts-soft/core';
|
|
801
1121
|
|
|
802
|
-
|
|
803
|
-
|
|
1122
|
+
@Module({
|
|
1123
|
+
imports: [
|
|
1124
|
+
ThrottlingModule.forRoot([
|
|
1125
|
+
{ name: 'short', ttl: 1_000, limit: 10 },
|
|
1126
|
+
{ name: 'medium', ttl: 10_000, limit: 50 },
|
|
1127
|
+
{ name: 'long', ttl: 60_000, limit: 250 },
|
|
1128
|
+
]),
|
|
1129
|
+
],
|
|
1130
|
+
})
|
|
1131
|
+
export class AppModule {}
|
|
804
1132
|
```
|
|
805
1133
|
|
|
806
|
-
####
|
|
807
|
-
**Description:** Retrieves and deserializes a value from Redis.
|
|
808
|
-
**Interface:** `get<T = any>(key: string): Promise<T | null>`
|
|
809
|
-
**Parameters:**
|
|
810
|
-
- `key`: The key to retrieve.
|
|
811
|
-
**Return:** `Promise<T | null>`. Returns `null` if the key does not exist.
|
|
812
|
-
**Examples:**
|
|
813
|
-
```typescript
|
|
814
|
-
// REST Example
|
|
815
|
-
const user = await this.redisService.get<UserEntity>(`user:${id}`);
|
|
1134
|
+
#### GraphqlModule
|
|
816
1135
|
|
|
817
|
-
|
|
818
|
-
const profile = await this.redisService.get<ProfileInput>(`profile:${userId}`);
|
|
819
|
-
```
|
|
1136
|
+
Apollo Server with WebSocket subscriptions, GraphQL Upload, CSRF prevention, and Apollo Federation:
|
|
820
1137
|
|
|
821
|
-
#### 3. `del`
|
|
822
|
-
**Description:** Deletes one or more keys from Redis.
|
|
823
|
-
**Interface:** `del(...keys: string[]): Promise<void>`
|
|
824
|
-
**Parameters:**
|
|
825
|
-
- `keys`: One or more keys to delete.
|
|
826
|
-
**Return:** `Promise<void>`
|
|
827
|
-
**Example:**
|
|
828
1138
|
```typescript
|
|
829
|
-
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
#### 4. `exists`
|
|
833
|
-
**Description:** Checks if a key exists in Redis.
|
|
834
|
-
**Interface:** `exists(key: string): Promise<boolean>`
|
|
835
|
-
**Return:** `true` if it exists, `false` otherwise.
|
|
836
|
-
|
|
837
|
-
#### 5. `expire`
|
|
838
|
-
**Description:** Sets or updates the TTL of a key.
|
|
839
|
-
**Interface:** `expire(key: string, seconds: number): Promise<boolean>`
|
|
840
|
-
|
|
841
|
-
#### 6. `ttl`
|
|
842
|
-
**Description:** Gets the remaining TTL of a key.
|
|
843
|
-
**Interface:** `ttl(key: string): Promise<number>`
|
|
844
|
-
|
|
845
|
-
---
|
|
846
|
-
|
|
847
|
-
### String & Numeric Operations
|
|
848
|
-
|
|
849
|
-
#### 7. `incr`
|
|
850
|
-
**Description:** Atomically increments the numeric value of a key.
|
|
851
|
-
**Interface:** `incr(key: string): Promise<number>`
|
|
852
|
-
**Usage:** `const newCount = await this.redisService.incr('hit_counter');`
|
|
853
|
-
|
|
854
|
-
#### 8. `incrBy`
|
|
855
|
-
**Description:** Increments a key by a specific integer.
|
|
856
|
-
**Interface:** `incrBy(key: string, increment: number): Promise<number>`
|
|
857
|
-
|
|
858
|
-
#### 9. `incrByFloat`
|
|
859
|
-
**Description:** Increments a key by a floating-point number.
|
|
860
|
-
**Interface:** `incrByFloat(key: string, increment: number): Promise<number>`
|
|
861
|
-
|
|
862
|
-
#### 10. `decr`
|
|
863
|
-
**Description:** Atomically decrements a key.
|
|
864
|
-
**Interface:** `decr(key: string): Promise<number>`
|
|
865
|
-
|
|
866
|
-
#### 11. `getSet`
|
|
867
|
-
**Description:** Sets a new value and returns the old one. This is an atomic operation.
|
|
868
|
-
**Interface:** `getSet(key: string, value: any): Promise<string | null>`
|
|
869
|
-
|
|
870
|
-
#### 12. `strlen`
|
|
871
|
-
**Description:** Returns the length of a string value.
|
|
872
|
-
**Interface:** `strlen(key: string): Promise<number>`
|
|
873
|
-
|
|
874
|
-
---
|
|
875
|
-
|
|
876
|
-
### Hash Operations (Object Manipulation)
|
|
877
|
-
|
|
878
|
-
#### 13. `hSet`
|
|
879
|
-
**Description:** Sets a field in a Redis hash.
|
|
880
|
-
**Interface:** `hSet(key: string, field: string, value: any): Promise<void>`
|
|
881
|
-
|
|
882
|
-
#### 14. `hGet`
|
|
883
|
-
**Description:** Gets a field from a Redis hash.
|
|
884
|
-
**Interface:** `hGet<T = any>(key: string, field: string): Promise<T | null>`
|
|
885
|
-
|
|
886
|
-
#### 15. `hGetAll`
|
|
887
|
-
**Description:** Gets all fields and values from a Redis hash as a JavaScript object.
|
|
888
|
-
**Interface:** `hGetAll(key: string): Promise<Record<string, any>>`
|
|
889
|
-
|
|
890
|
-
#### 16. `hDel`
|
|
891
|
-
**Description:** Deletes fields from a hash.
|
|
892
|
-
**Interface:** `hDel(key: string, ...fields: string[]): Promise<void>`
|
|
893
|
-
|
|
894
|
-
#### 17. `hKeys`
|
|
895
|
-
**Description:** Gets all field names in a hash.
|
|
896
|
-
**Interface:** `hKeys(key: string): Promise<string[]>`
|
|
897
|
-
|
|
898
|
-
#### 18. `hVals`
|
|
899
|
-
**Description:** Gets all field values in a hash.
|
|
900
|
-
**Interface:** `hVals(key: string): Promise<any[]>`
|
|
901
|
-
|
|
902
|
-
#### 19. `hLen`
|
|
903
|
-
**Description:** Gets the number of fields in a hash.
|
|
904
|
-
**Interface:** `hLen(key: string): Promise<number>`
|
|
905
|
-
|
|
906
|
-
#### 20. `hIncrBy`
|
|
907
|
-
**Description:** Increments a numeric hash field.
|
|
908
|
-
**Interface:** `hIncrBy(key: string, field: string, increment: number): Promise<number>`
|
|
909
|
-
|
|
910
|
-
---
|
|
911
|
-
|
|
912
|
-
### Set Operations (Unique Collections)
|
|
913
|
-
|
|
914
|
-
#### 21. `sAdd`
|
|
915
|
-
**Description:** Adds one or more members to a set.
|
|
916
|
-
**Interface:** `sAdd(key: string, ...members: any[]): Promise<void>`
|
|
917
|
-
|
|
918
|
-
#### 22. `sRem`
|
|
919
|
-
**Description:** Removes members from a set.
|
|
920
|
-
**Interface:** `sRem(key: string, ...members: any[]): Promise<void>`
|
|
921
|
-
|
|
922
|
-
#### 23. `sMembers`
|
|
923
|
-
**Description:** Gets all members of a set.
|
|
924
|
-
**Interface:** `sMembers(key: string): Promise<any[]>`
|
|
925
|
-
|
|
926
|
-
#### 24. `sIsMember`
|
|
927
|
-
**Description:** Checks if a value is a member of a set.
|
|
928
|
-
**Interface:** `sIsMember(key: string, member: any): Promise<boolean>`
|
|
929
|
-
|
|
930
|
-
#### 25. `sCard`
|
|
931
|
-
**Description:** Gets the number of members in a set.
|
|
932
|
-
**Interface:** `sCard(key: string): Promise<number>`
|
|
933
|
-
|
|
934
|
-
#### 26. `sInter`
|
|
935
|
-
**Description:** Finds the intersection of multiple sets.
|
|
936
|
-
**Interface:** `sInter(...keys: string[]): Promise<any[]>`
|
|
937
|
-
|
|
938
|
-
---
|
|
939
|
-
|
|
940
|
-
#### 27. `sUnion`
|
|
941
|
-
**Description:** Returns the union of multiple sets.
|
|
942
|
-
**Interface:** `sUnion(...keys: string[]): Promise<any[]>`
|
|
943
|
-
|
|
944
|
-
#### 28. `sDiff`
|
|
945
|
-
**Description:** Returns the difference between the first set and all successive sets.
|
|
946
|
-
**Interface:** `sDiff(...keys: string[]): Promise<any[]>`
|
|
947
|
-
|
|
948
|
-
---
|
|
949
|
-
|
|
950
|
-
### Sorted Set Operations (Leaderboards & Priority)
|
|
951
|
-
|
|
952
|
-
#### 29. `zAdd`
|
|
953
|
-
**Description:** Adds a member with a specific score to a sorted set.
|
|
954
|
-
**Interface:** `zAdd(key: string, score: number, member: any): Promise<void>`
|
|
955
|
-
|
|
956
|
-
#### 30. `zRange`
|
|
957
|
-
**Description:** Returns a range of members by their index.
|
|
958
|
-
**Interface:** `zRange(key: string, start: number, stop: number): Promise<any[]>`
|
|
959
|
-
|
|
960
|
-
#### 31. `zRevRange`
|
|
961
|
-
**Description:** Returns a range of members, ordered from high to low score.
|
|
962
|
-
**Interface:** `zRevRange(key: string, start: number, stop: number): Promise<any[]>`
|
|
963
|
-
|
|
964
|
-
#### 32. `zRank`
|
|
965
|
-
**Description:** Returns the rank (index) of a member, sorted by score.
|
|
966
|
-
**Interface:** `zRank(key: string, member: any): Promise<number | null>`
|
|
967
|
-
|
|
968
|
-
#### 33. `zScore`
|
|
969
|
-
**Description:** Returns the score associated with a member.
|
|
970
|
-
**Interface:** `zScore(key: string, member: any): Promise<number | null>`
|
|
971
|
-
|
|
972
|
-
#### 34. `zCard`
|
|
973
|
-
**Description:** Gets the number of elements in a sorted set.
|
|
974
|
-
**Interface:** `zCard(key: string): Promise<number>`
|
|
975
|
-
|
|
976
|
-
#### 35. `zCount`
|
|
977
|
-
**Description:** Counts members with scores within a specific range.
|
|
978
|
-
**Interface:** `zCount(key: string, min: number, max: number): Promise<number>`
|
|
979
|
-
|
|
980
|
-
#### 36. `zRem`
|
|
981
|
-
**Description:** Removes one or more members.
|
|
982
|
-
**Interface:** `zRem(key: string, ...members: any[]): Promise<void>`
|
|
983
|
-
|
|
984
|
-
---
|
|
985
|
-
|
|
986
|
-
### List Operations (Queues & Stacks)
|
|
987
|
-
|
|
988
|
-
#### 37. `lPush`
|
|
989
|
-
**Description:** Prepends a value to a list.
|
|
990
|
-
**Interface:** `lPush(key: string, ...values: any[]): Promise<number>`
|
|
991
|
-
|
|
992
|
-
#### 38. `rPush`
|
|
993
|
-
**Description:** Appends a value to a list.
|
|
994
|
-
**Interface:** `rPush(key: string, ...values: any[]): Promise<number>`
|
|
995
|
-
|
|
996
|
-
#### 39. `lPop`
|
|
997
|
-
**Description:** Removes and returns the first element.
|
|
998
|
-
**Interface:** `lPop<T = any>(key: string): Promise<T | null>`
|
|
999
|
-
|
|
1000
|
-
#### 40. `rPop`
|
|
1001
|
-
**Description:** Removes and returns the last element.
|
|
1002
|
-
**Interface:** `rPop<T = any>(key: string): Promise<T | null>`
|
|
1003
|
-
|
|
1004
|
-
#### 41. `lRange`
|
|
1005
|
-
**Description:** Returns a range of elements from a list.
|
|
1006
|
-
**Interface:** `lRange(key: string, start: number, stop: number): Promise<any[]>`
|
|
1007
|
-
|
|
1008
|
-
#### 42. `lLen`
|
|
1009
|
-
**Description:** Returns the length of a list.
|
|
1010
|
-
**Interface:** `lLen(key: string): Promise<number>`
|
|
1011
|
-
|
|
1012
|
-
#### 43. `lTrim`
|
|
1013
|
-
**Description:** Trims a list to a specific range (Atomic capping).
|
|
1014
|
-
**Interface:** `lTrim(key: string, start: number, stop: number): Promise<void>`
|
|
1015
|
-
|
|
1016
|
-
---
|
|
1017
|
-
|
|
1018
|
-
### Advanced Tooling: Pub/Sub & Locking
|
|
1019
|
-
|
|
1020
|
-
#### 44. `publish`
|
|
1021
|
-
**Description:** Posts a message to a channel.
|
|
1022
|
-
**Interface:** `publish(channel: string, message: any): Promise<void>`
|
|
1023
|
-
|
|
1024
|
-
#### 45. `subscribe`
|
|
1025
|
-
**Description:** Listens for messages on a channel.
|
|
1026
|
-
**Interface:** `subscribe(channel: string, callback: (message: string) => void): Promise<void>`
|
|
1027
|
-
|
|
1028
|
-
#### 46. `acquireLock`
|
|
1029
|
-
**Description:** Attempts to acquire a distributed lock.
|
|
1030
|
-
**Interface:** `acquireLock(resource: string, value: string, ttl: number): Promise<string | null>`
|
|
1031
|
-
|
|
1032
|
-
#### 47. `releaseLock`
|
|
1033
|
-
**Description:** Safely releases a distributed lock using Lua scripting.
|
|
1034
|
-
**Interface:** `releaseLock(resource: string, value: string): Promise<boolean>`
|
|
1035
|
-
|
|
1036
|
-
---
|
|
1037
|
-
|
|
1038
|
-
## Notification Provider Master Reference
|
|
1039
|
-
|
|
1040
|
-
This section provides a deep technical dive into every supported notification channel, including its underlying technology, payload schema, and configuration secrets.
|
|
1041
|
-
|
|
1042
|
-
---
|
|
1043
|
-
|
|
1044
|
-
### 1. Email (Nodemailer Engine)
|
|
1045
|
-
|
|
1046
|
-
The email channel is built for massive scale, supporting both direct SMTP and high-volume API relays.
|
|
1047
|
-
|
|
1048
|
-
#### Configuration Matrix
|
|
1049
|
-
| Variable | Description | Example |
|
|
1050
|
-
| :--- | :--- | :--- |
|
|
1051
|
-
| `EMAIL_HOST` | SMTP Server | `smtp.gmail.com` |
|
|
1052
|
-
| `EMAIL_PORT` | Connection Port | `465` (SSL) or `587` (TLS) |
|
|
1053
|
-
| `EMAIL_USER` | Auth Login | `notifications@company.com` |
|
|
1054
|
-
| `EMAIL_PASS` | Auth Password | `password_or_app_token` |
|
|
1055
|
-
| `EMAIL_SERVICE`| Pre-defined service | `gmail`, `outlook`, `sendgrid` |
|
|
1056
|
-
|
|
1057
|
-
#### Advanced Usage: Attachments & Inline Images
|
|
1058
|
-
```typescript
|
|
1059
|
-
await notificationService.send(ChannelType.EMAIL, {
|
|
1060
|
-
recipientId: 'boss@company.com',
|
|
1061
|
-
subject: 'Monthly Report',
|
|
1062
|
-
body: 'Please see the attached report.',
|
|
1063
|
-
channelOptions: {
|
|
1064
|
-
attachments: [
|
|
1065
|
-
{
|
|
1066
|
-
filename: 'report.pdf',
|
|
1067
|
-
content: pdfBuffer,
|
|
1068
|
-
contentType: 'application/pdf'
|
|
1069
|
-
}
|
|
1070
|
-
]
|
|
1071
|
-
}
|
|
1072
|
-
});
|
|
1073
|
-
```
|
|
1074
|
-
|
|
1075
|
-
---
|
|
1076
|
-
|
|
1077
|
-
### 2. WhatsApp & SMS (Twilio Hub)
|
|
1078
|
-
|
|
1079
|
-
Both channels leverage the Twilio REST API with custom normalization for Middle-Eastern phone formats.
|
|
1080
|
-
|
|
1081
|
-
#### The Normalization Logic
|
|
1082
|
-
Every phone number passed to `recipientId` is passed through a sanitizer:
|
|
1083
|
-
1. Trims whitespace and dashes.
|
|
1084
|
-
2. Handles `00` prefix by converting to `+`.
|
|
1085
|
-
3. Automatically detects Egyptian `01` formats and prepends `+20`.
|
|
1086
|
-
4. Ensures the `whatsapp:` prefix is correctly applied for the WhatsApp channel.
|
|
1087
|
-
|
|
1088
|
-
#### Configuration Matrix
|
|
1089
|
-
| Variable | Description |
|
|
1090
|
-
| :--- | :--- |
|
|
1091
|
-
| `TWILIO_ACCOUNT_SID` | Your unique Twilio account identifier. |
|
|
1092
|
-
| `TWILIO_AUTH_TOKEN` | Secret token for API authentication. |
|
|
1093
|
-
| `TWILIO_SMS_NUMBER` | The registered 10-digit Twilio phone number. |
|
|
1094
|
-
| `TWILIO_WHATSAPP_NUMBER`| The "Sandbox" or production WhatsApp number. |
|
|
1095
|
-
|
|
1096
|
-
---
|
|
1097
|
-
|
|
1098
|
-
### 3. Telegram (Bot API integration)
|
|
1099
|
-
|
|
1100
|
-
The Telegram channel is the fastest way to build real-time monitoring and alert systems.
|
|
1101
|
-
|
|
1102
|
-
#### Configuration
|
|
1103
|
-
- `TELEGRAM_BOT_TOKEN`: Obtained from `@BotFather`.
|
|
1104
|
-
|
|
1105
|
-
#### Rich Formatting Support
|
|
1106
|
-
Telegram supports `HTML` and `MarkdownV2`. You can trigger these via `channelOptions`.
|
|
1107
|
-
|
|
1108
|
-
```typescript
|
|
1109
|
-
await notificationService.send(ChannelType.TELEGRAM, {
|
|
1110
|
-
recipientId: '@monitoring_channel',
|
|
1111
|
-
body: '<b>CRITICAL:</b> Database CPU is at 95%',
|
|
1112
|
-
channelOptions: { parse_mode: 'HTML' }
|
|
1113
|
-
});
|
|
1114
|
-
```
|
|
1115
|
-
|
|
1116
|
-
---
|
|
1117
|
-
|
|
1118
|
-
### 4. Firebase Cloud Messaging (FCM)
|
|
1119
|
-
|
|
1120
|
-
The FCM channel is the standard for mobile and web push notifications in the BTS Soft ecosystem.
|
|
1121
|
-
|
|
1122
|
-
#### Device Token Management
|
|
1123
|
-
- `recipientId`: Must be a valid FCM device token or topic name (prefixed with `/topics/`).
|
|
1124
|
-
|
|
1125
|
-
#### Rich Payloads
|
|
1126
|
-
You can pass custom data and notification options to fine-tune the delivery.
|
|
1127
|
-
|
|
1128
|
-
```typescript
|
|
1129
|
-
await notificationService.send(ChannelType.FIREBASE_FCM, {
|
|
1130
|
-
recipientId: 'EXPO_TOKEN_123',
|
|
1131
|
-
title: 'Flash Sale! ⚡',
|
|
1132
|
-
body: 'Get 50% off for the next 4 hours.',
|
|
1133
|
-
channelOptions: {
|
|
1134
|
-
data: { url: '/deals/flash-sale' },
|
|
1135
|
-
options: {
|
|
1136
|
-
priority: 'high',
|
|
1137
|
-
timeToLive: 14400 // 4 hours
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
});
|
|
1141
|
-
```
|
|
1142
|
-
|
|
1143
|
-
---
|
|
1144
|
-
|
|
1145
|
-
### 5. Chatbot Webhooks (Discord & Teams)
|
|
1146
|
-
|
|
1147
|
-
Both channels use a simplified HTTP-based webhook architecture, perfect for server alerts and team collaboration.
|
|
1148
|
-
|
|
1149
|
-
#### Discord Features
|
|
1150
|
-
- Supports rich embeds and custom avatars per message.
|
|
1151
|
-
- Configuration: `DISCORD_WEBHOOK_URL`.
|
|
1152
|
-
|
|
1153
|
-
#### MS Teams Features
|
|
1154
|
-
- Supports adaptive cards and actionable messages.
|
|
1155
|
-
- Configuration: `TEAMS_WEBHOOK_URL`.
|
|
1156
|
-
|
|
1157
|
-
---
|
|
1158
|
-
|
|
1159
|
-
## Technical Appendix: Complete Redis API Reference
|
|
1160
|
-
|
|
1161
|
-
This specification documents every method available in the `RedisService`, providing developers with a complete catalog of atomic data operations.
|
|
1162
|
-
|
|
1163
|
-
### 1. Key Lifecycle (O(1))
|
|
1164
|
-
|
|
1165
|
-
#### `get<T>(key: string): Promise<T | null>`
|
|
1166
|
-
- **Returns**: The parsed JSON object or raw string.
|
|
1167
|
-
- **Error Cases**: Returns `null` if key does not exist.
|
|
1168
|
-
|
|
1169
|
-
#### `set(key: string, value: any, ttl?: number): Promise<void>`
|
|
1170
|
-
- **Params**: `ttl` defaults to 3600 seconds.
|
|
1171
|
-
- **Serialization**: Automatic `JSON.stringify` for objects.
|
|
1172
|
-
|
|
1173
|
-
#### `del(key: string): Promise<void>`
|
|
1174
|
-
- **Purpose**: Permanent removal of a key.
|
|
1175
|
-
|
|
1176
|
-
#### `exists(key: string): Promise<boolean>`
|
|
1177
|
-
- **Returns**: `true` if key exists, `false` otherwise.
|
|
1178
|
-
|
|
1179
|
-
#### `expire(key: string, seconds: number): Promise<boolean>`
|
|
1180
|
-
- **Purpose**: Update the TTL of an existing key.
|
|
1181
|
-
|
|
1182
|
-
#### `ttl(key: string): Promise<number>`
|
|
1183
|
-
- **Returns**: Remaining seconds (-1 for infinite, -2 for not found).
|
|
1184
|
-
|
|
1185
|
-
---
|
|
1186
|
-
|
|
1187
|
-
### 2. Atomic Counters (O(1))
|
|
1188
|
-
|
|
1189
|
-
#### `incr(key: string): Promise<number>`
|
|
1190
|
-
- **Purpose**: Thread-safe increment of an integer key.
|
|
1191
|
-
|
|
1192
|
-
#### `decr(key: string): Promise<number>`
|
|
1193
|
-
- **Purpose**: Thread-safe decrement.
|
|
1194
|
-
|
|
1195
|
-
#### `incrBy(key: string, value: number): Promise<number>`
|
|
1196
|
-
- **Purpose**: Increment by a specific amount.
|
|
1197
|
-
|
|
1198
|
-
#### `decrBy(key: string, value: number): Promise<number>`
|
|
1199
|
-
- **Purpose**: Decrement by a specific amount.
|
|
1200
|
-
|
|
1201
|
-
---
|
|
1202
|
-
|
|
1203
|
-
### 3. Hash Objects (O(N) for Multiple Fields)
|
|
1204
|
-
|
|
1205
|
-
#### `hSet(key: string, field: string, value: any): Promise<number>`
|
|
1206
|
-
- **Purpose**: Store a value in a hash field.
|
|
1207
|
-
|
|
1208
|
-
#### `hGet<T>(key: string, field: string): Promise<T | null>`
|
|
1209
|
-
- **Purpose**: Retrieve value from a specific hash field.
|
|
1210
|
-
|
|
1211
|
-
#### `hDel(key: string, ...fields: string[]): Promise<number>`
|
|
1212
|
-
- **Purpose**: Remove one or more fields from a hash.
|
|
1213
|
-
|
|
1214
|
-
#### `hGetAll<T>(key: string): Promise<T>`
|
|
1215
|
-
- **Purpose**: Retrieve the entire hash object.
|
|
1216
|
-
|
|
1217
|
-
#### `hKeys(key: string): Promise<string[]>`
|
|
1218
|
-
- **Purpose**: List all field names in a hash.
|
|
1219
|
-
|
|
1220
|
-
---
|
|
1221
|
-
|
|
1222
|
-
## Technical Appendix: Global Configuration Schema (Exhaustive)
|
|
1223
|
-
|
|
1224
|
-
Every environment variable that influences `@bts-soft/core` is documented below.
|
|
1225
|
-
|
|
1226
|
-
| Variable Name | Purpose | Example Value |
|
|
1227
|
-
| :--- | :--- | :--- |
|
|
1228
|
-
| `NODE_ENV` | Runtime environment | `production` | `development` |
|
|
1229
|
-
| `REDIS_HOST` | Cache endpoint | `127.0.0.1` |
|
|
1230
|
-
| `REDIS_PORT` | Cache port | `6379` |
|
|
1231
|
-
| `REDIS_PASS` | Cache password | `StrongSecret123!` |
|
|
1232
|
-
| `EMAIL_SERVICE` | Nodemailer provider | `gmail` | `outlook` |
|
|
1233
|
-
| `EMAIL_USER` | Sender address | `noreply@bts-soft.com` |
|
|
1234
|
-
| `EMAIL_PASS` | App-specific password | `abcd-efgh-ijkl-mnop` |
|
|
1235
|
-
| `CLOUDINARY_NAME` | Media cloud name | `bts-soft-cloud` |
|
|
1236
|
-
| `CLOUDINARY_API_KEY`| Media API key | `123456789012345` |
|
|
1237
|
-
| `CLOUDINARY_SECRET` | Media API secret | `_shhh_secrets_` |
|
|
1238
|
-
| `TWILIO_SID` | SMS account SID | `AC123...` |
|
|
1239
|
-
| `TWILIO_TOKEN` | SMS auth token | `auth_tok_...` |
|
|
1240
|
-
| `TELEGRAM_TOKEN` | Bot API token | `123456:ABC-DEF...` |
|
|
1241
|
-
|
|
1242
|
-
---
|
|
1243
|
-
|
|
1244
|
-
## Technical Appendix: Architectural Blueprint & Decision Records (ADR)
|
|
1245
|
-
|
|
1246
|
-
### ADR 001: Choosing ULID over UUID
|
|
1247
|
-
**Decision**: Standardize on ULID for primary keys.
|
|
1248
|
-
**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.
|
|
1249
|
-
|
|
1250
|
-
### ADR 002: Command Pattern for Uploads
|
|
1251
|
-
**Decision**: Use the Command & Strategy pattern for the Upload module.
|
|
1252
|
-
**Rationale**: This allows us to add new storage providers (S3, Azure Blob) without changing the `UploadService` signature, ensuring future-proof extensibility.
|
|
1253
|
-
|
|
1254
|
-
### ADR 003: BullMQ for Notifications
|
|
1255
|
-
**Decision**: Mandatory queueing for all notification channels.
|
|
1256
|
-
**Rationale**: Third-party APIs (Twilio, Firebase) are inherently unreliable. A persistent queue ensures that momentary network glitches do not result in dropped user notifications.
|
|
1257
|
-
|
|
1258
|
-
---
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
## Technical Appendix: Ecosystem Roadmap 2026-2027
|
|
1262
|
-
|
|
1263
|
-
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.
|
|
1264
|
-
|
|
1265
|
-
### v2.3.0: The "Observability" Update (Q2 2026)
|
|
1266
|
-
- **OpenTelemetry Native Support**: Built-in tracing for Redis, TypeORM, and Notification dispatch.
|
|
1267
|
-
- **Service Mesh Helpers**: Pre-configured sidecar patterns for Istio and Linkerd.
|
|
1268
|
-
- **Log Masking**: Automated PII detection and redaction in the `GeneralResponseInterceptor`.
|
|
1269
|
-
|
|
1270
|
-
### v2.4.0: The "Intelligence" Update (Q4 2026)
|
|
1271
|
-
- **AI-Logic Validation**: New decorators like `@SentimentField` and `@SpamScanner` using local LLMs or external APIs.
|
|
1272
|
-
- **Predictive Caching**: Using Redis TimeSeries to predict key expiration and warm the cache proactively.
|
|
1273
|
-
- **Notification Sentiment Analysis**: Automated scoring of incoming (if implemented) or outgoing message tones.
|
|
1274
|
-
|
|
1275
|
-
### v3.0.0: The "Micro-Kernel" Revolution (2027)
|
|
1276
|
-
- **Zero-Dependency Core**: Moving heavy providers (Twilio, Firebase) into optional peer-dependencies.
|
|
1277
|
-
- **WebAssembly Transformers**: High-performance data cleaning using WASM-compiled Rust modules.
|
|
1278
|
-
- **Native Bun/Deno Support**: Ensuring full compatibility with next-gen JavaScript runtimes.
|
|
1279
|
-
|
|
1280
|
-
---
|
|
1281
|
-
|
|
1282
|
-
## Technical Appendix: Developer Onboarding Checklist
|
|
1283
|
-
|
|
1284
|
-
New to the BTS Soft ecosystem? Follow this step-by-step onboarding guide.
|
|
1285
|
-
|
|
1286
|
-
### Week 1: Foundational Setup
|
|
1287
|
-
1. [ ] Install the [BTS Soft VS Code Extension Pack](link).
|
|
1288
|
-
2. [ ] Clone the [Core Samples Repository](link).
|
|
1289
|
-
3. [ ] Complete the "Hello World" tutorial for the **Validation** module.
|
|
1290
|
-
4. [ ] Successfully send a test email via the **Notification** service.
|
|
1291
|
-
|
|
1292
|
-
### Week 2: Intermediate Patterns
|
|
1293
|
-
5. [ ] Implement a **Cache-Aside** logic for a database entity.
|
|
1294
|
-
6. [ ] Create a custom **Upload Observer** for audit logging.
|
|
1295
|
-
7. [ ] Build a **GraphQL Input Type** using the core decorators.
|
|
1296
|
-
|
|
1297
|
-
### Week 3: Production Readiness
|
|
1298
|
-
8. [ ] Perform a **SQL Injection Simulation** on your local API.
|
|
1299
|
-
9. [ ] Run a **Load Test** (1,000 RPS) using `k6`.
|
|
1300
|
-
10. [ ] Configure **Redis Persistence (AOF)** on your staging server.
|
|
1301
|
-
|
|
1302
|
-
---
|
|
1303
|
-
|
|
1304
|
-
## Technical Appendix: The Global Developer Credits
|
|
1305
|
-
|
|
1306
|
-
A special thank you to all the engineers who have contributed to the `@bts-soft` codebase.
|
|
1307
|
-
|
|
1308
|
-
| Contributor | Area of Expertise | Version Contribution |
|
|
1309
|
-
| :--- | :--- | :--- |
|
|
1310
|
-
| **Omar Sabry** | Lead Architect, Security | v1.0.0 - Present |
|
|
1311
|
-
|
|
1312
|
-
---
|
|
1313
|
-
|
|
1314
|
-
## Epilogue: The BTS Soft Legacy
|
|
1315
|
-
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.
|
|
1316
|
-
|
|
1317
|
-
---
|
|
1318
|
-
|
|
1319
|
-
## Credits & License
|
|
1320
|
-
Created and maintained by **Omar Sabry** for **BTS Soft**.
|
|
1321
|
-
Licensed under the **MIT License**.
|
|
1322
|
-
© 2026 BTS Soft. All Rights Reserved.
|
|
1323
|
-
|
|
1324
|
-
## Enterprise Media Management Deep Dive
|
|
1325
|
-
|
|
1326
|
-
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.
|
|
1327
|
-
|
|
1328
|
-
---
|
|
1329
|
-
|
|
1330
|
-
### The Command Pattern Architecture
|
|
1331
|
-
|
|
1332
|
-
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.
|
|
1333
|
-
|
|
1334
|
-
#### Available Commands
|
|
1335
|
-
- `UploadImageCommand`: Handles image-specific validation and format conversion.
|
|
1336
|
-
- `UploadVideoCommand`: Manages large file streams and chunked uploads.
|
|
1337
|
-
- `UploadAudioCommand`: Processes sound files with audio metadata extraction.
|
|
1338
|
-
- `UploadFileCommand`: Transparently handles documents and raw binary data.
|
|
1339
|
-
- `DeleteImageCommand`: Safely removes assets and cleans up the Cloudinary cache.
|
|
1340
|
-
|
|
1341
|
-
---
|
|
1342
|
-
|
|
1343
|
-
### Strategic Media Lifecycles
|
|
1344
|
-
|
|
1345
|
-
Every file upload follows a 5-step lifecycle:
|
|
1346
|
-
|
|
1347
|
-
1. **Validation Stage**: The `UploadService` checks the file extension and size against the protocol limits (e.g., 5MB for images).
|
|
1348
|
-
2. **Command Preparation**: A specific command is instantiated (e.g., `UploadImageCommand`) with the incoming stream and target folder.
|
|
1349
|
-
3. **Execution (Strategy)**: The command calls the `IUploadStrategy.upload()` method. By default, this pipes the stream directly to Cloudinary's secure servers.
|
|
1350
|
-
4. **Observer Notification**: On success or failure, the `LoggingObserver` (or your custom observer) is notified to log the event or trigger a DB update.
|
|
1351
|
-
5. **Response Sanitization**: High-level metadata (URL, public_id, size) is returned to the caller in a standardized object.
|
|
1352
|
-
|
|
1353
|
-
---
|
|
1354
|
-
|
|
1355
|
-
### Advanced Media Transformations
|
|
1356
|
-
|
|
1357
|
-
Since we use Cloudinary by default, you can leverage their massive transformation CDN directly through `channelOptions`.
|
|
1358
|
-
|
|
1359
|
-
```typescript
|
|
1360
|
-
// Example: Generate a square avatar with rounded corners
|
|
1361
|
-
const result = await uploadService.uploadImageCore(file, 'users', {
|
|
1362
|
-
transformation: [
|
|
1363
|
-
{ width: 500, height: 500, crop: "fill", gravity: "face" },
|
|
1364
|
-
{ radius: "max" },
|
|
1365
|
-
{ effect: "sepia" }
|
|
1366
|
-
]
|
|
1367
|
-
});
|
|
1368
|
-
```
|
|
1369
|
-
|
|
1370
|
-
---
|
|
1371
|
-
|
|
1372
|
-
## Global Troubleshooting Matrix
|
|
1373
|
-
|
|
1374
|
-
This comprehensive guide covers common errors and their solutions across all core modules.
|
|
1375
|
-
|
|
1376
|
-
### Redis Connectivity
|
|
1377
|
-
| Error Message | Possible Root Cause | Solution |
|
|
1378
|
-
| :--- | :--- | :--- |
|
|
1379
|
-
| `ECONNREFUSED` | Redis server is not running or port is wrong. | Check `REDIS_HOST` and `REDIS_PORT`. |
|
|
1380
|
-
| `NOAUTH` | Redis requires a password but none was provided. | Set `REDIS_PASSWORD` in `.env`. |
|
|
1381
|
-
| `OOM command not allowed` | Redis has reached its memory limit. | Increase `maxmemory` in `redis.conf` or cleanup old keys. |
|
|
1382
|
-
|
|
1383
|
-
### Notification Delivery
|
|
1384
|
-
| Error Message | Possible Root Cause | Solution |
|
|
1385
|
-
| :--- | :--- | :--- |
|
|
1386
|
-
| `EAUTH - Invalid credentials`| SMTP user/password is incorrect. | Check `EMAIL_USER` and `EMAIL_PASS`. |
|
|
1387
|
-
| `Invalid phone number` | Input is not in E.164 format. | Ensure `recipientId` starts with `+` or use the auto-normalizer. |
|
|
1388
|
-
| `403 Forbidden (Telegram)` | Bot has been blocked by the user. | User must restart the bot to receive messages. |
|
|
1389
|
-
|
|
1390
|
-
### Media Upload
|
|
1391
|
-
| Error Message | Possible Root Cause | Solution |
|
|
1392
|
-
| :--- | :--- | :--- |
|
|
1393
|
-
| `Invalid image type` | Extension is not in the allowed list. | Check the 'Media Type Specifications' table above. |
|
|
1394
|
-
| `File too large` | Stream size exceeds the module limit. | Use a separate command for large files or update limits. |
|
|
1395
|
-
| `Must provide cloud_name` | Cloudinary config is missing. | Verify `CLOUDINARY_CLOUD_NAME` is loaded in `process.env`. |
|
|
1396
|
-
|
|
1397
|
-
---
|
|
1398
|
-
|
|
1399
|
-
## Enterprise Design Patterns & Architecture Deep Dive
|
|
1400
|
-
|
|
1401
|
-
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.
|
|
1402
|
-
|
|
1403
|
-
---
|
|
1404
|
-
|
|
1405
|
-
### 1. The Strategy Pattern (Provider Decoupling)
|
|
1406
|
-
|
|
1407
|
-
In the upload and notification modules, we decouple the "What to do" from "How to do it."
|
|
1408
|
-
|
|
1409
|
-
```mermaid
|
|
1410
|
-
classDiagram
|
|
1411
|
-
class IUploadStrategy {
|
|
1412
|
-
<<interface>>
|
|
1413
|
-
+upload(stream, options)
|
|
1414
|
-
}
|
|
1415
|
-
class CloudinaryUploadStrategy {
|
|
1416
|
-
-cloudinaryClient
|
|
1417
|
-
+upload()
|
|
1418
|
-
}
|
|
1419
|
-
class S3UploadStrategy {
|
|
1420
|
-
-s3Client
|
|
1421
|
-
+upload()
|
|
1422
|
-
}
|
|
1423
|
-
IUploadStrategy <|.. CloudinaryUploadStrategy
|
|
1424
|
-
IUploadStrategy <|.. S3UploadStrategy
|
|
1425
|
-
UploadService --> IUploadStrategy
|
|
1426
|
-
```
|
|
1427
|
-
|
|
1428
|
-
**Why this matters:**
|
|
1429
|
-
- **Zero Lock-in**: You can switch from Cloudinary to S3 by simply creating a new strategy class and updating the factory.
|
|
1430
|
-
- **Improved Testing**: You can inject a `MockUploadStrategy` in unit tests to avoid hitting real APIs.
|
|
1431
|
-
|
|
1432
|
-
---
|
|
1433
|
-
|
|
1434
|
-
### 2. The Command Pattern (Logic Encapsulation)
|
|
1435
|
-
|
|
1436
|
-
Every specific media operation is a discrete "Command" object.
|
|
1437
|
-
|
|
1438
|
-
```mermaid
|
|
1439
|
-
sequenceDiagram
|
|
1440
|
-
participant App as Application Code
|
|
1441
|
-
participant Service as UploadService
|
|
1442
|
-
participant Command as UploadImageCommand
|
|
1443
|
-
participant Strategy as CloudinaryStrategy
|
|
1444
|
-
|
|
1445
|
-
App->>Service: uploadImageCore(stream, options)
|
|
1446
|
-
Service->>Command: new UploadImageCommand(strategy, stream, opts)
|
|
1447
|
-
Service->>Command: execute()
|
|
1448
|
-
Command->>Strategy: upload(stream, opts)
|
|
1449
|
-
Strategy-->>Command: result
|
|
1450
|
-
Command-->>Service: result
|
|
1451
|
-
Service-->>App: result
|
|
1452
|
-
```
|
|
1453
|
-
|
|
1454
|
-
**Why this matters:**
|
|
1455
|
-
- **Single Responsibility**: The `UploadService` doesn't need to know how to handle MP4 chunks or JPEG compression; it just knows how to execute commands.
|
|
1456
|
-
- **Atomic Operations**: Each command is an isolated unit of work.
|
|
1457
|
-
|
|
1458
|
-
---
|
|
1459
|
-
|
|
1460
|
-
### 3. The Observer Pattern (Reactive Events)
|
|
1461
|
-
|
|
1462
|
-
We use the Observer pattern to handle secondary effects like logging, analytics, and cache invalidation.
|
|
1463
|
-
|
|
1464
|
-
```mermaid
|
|
1465
|
-
graph LR
|
|
1466
|
-
Upload[Upload Command Success] --> ObserverPool[Observer Registry]
|
|
1467
|
-
ObserverPool --> Log[LoggingObserver]
|
|
1468
|
-
ObserverPool --> Analytics[MetricsObserver]
|
|
1469
|
-
ObserverPool --> DB[DatabaseSyncObserver]
|
|
1470
|
-
```
|
|
1471
|
-
|
|
1472
|
-
---
|
|
1473
|
-
|
|
1474
|
-
## Global Configuration Schema Master List
|
|
1475
|
-
|
|
1476
|
-
The following table lists every supported configuration property used across the five sub-packages.
|
|
1477
|
-
|
|
1478
|
-
### Core & Common Config
|
|
1479
|
-
| Key | Type | Default | Description |
|
|
1480
|
-
| :--- | :--- | :--- | :--- |
|
|
1481
|
-
| `NODE_ENV` | `string` | `development` | Runtime environment. |
|
|
1482
|
-
| `PORT` | `number` | `3000` | HTTP port. |
|
|
1483
|
-
|
|
1484
|
-
### Redis & Cache Config
|
|
1485
|
-
| Key | Type | Default | Description |
|
|
1486
|
-
| :--- | :--- | :--- | :--- |
|
|
1487
|
-
| `REDIS_HOST` | `string` | `localhost` | Redis server address. |
|
|
1488
|
-
| `REDIS_PORT` | `number` | `6379` | Redis server port. |
|
|
1489
|
-
| `REDIS_PASSWORD`| `string` | `null` | Optional auth password. |
|
|
1490
|
-
|
|
1491
|
-
### Notification Hub Config
|
|
1492
|
-
| Key | Type | Description |
|
|
1493
|
-
| :--- | :--- | :--- |
|
|
1494
|
-
| `EMAIL_USER` | `string` | SMTP Username. |
|
|
1495
|
-
| `EMAIL_PASS` | `string` | SMTP App Password. |
|
|
1496
|
-
| `TWILIO_SID` | `string` | Twilio Account SID. |
|
|
1497
|
-
| `TELEGRAM_TOKEN`| `string` | BotFather Token. |
|
|
1498
|
-
|
|
1499
|
-
---
|
|
1500
|
-
|
|
1501
|
-
## Technical Appendix: The Giant FAQ Registry (100+ Items)
|
|
1502
|
-
|
|
1503
|
-
This registry is a living document of questions, edge cases, and architectural inquiries collected from developers across the BTS Soft ecosystem.
|
|
1504
|
-
|
|
1505
|
-
---
|
|
1506
|
-
|
|
1507
|
-
### Phase 1: General Ecosystem & Architecture
|
|
1508
|
-
|
|
1509
|
-
**Q1: Why was the core package split into five sub-packages?**
|
|
1510
|
-
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.
|
|
1511
|
-
|
|
1512
|
-
**Q2: What is the primary advantage of using ULIDs over UUIDs in `BaseEntity`?**
|
|
1513
|
-
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.
|
|
1514
|
-
|
|
1515
|
-
**Q3: Is this package compatible with Fastify?**
|
|
1516
|
-
A: Yes. All core modules are built on top of standard NestJS abstractions, making them compatible with both Express and Fastify adapters.
|
|
1517
|
-
|
|
1518
|
-
**Q4: How does the meta-package handle versioning?**
|
|
1519
|
-
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.
|
|
1520
|
-
|
|
1521
|
-
**Q5: Can I override the global interceptors?**
|
|
1522
|
-
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.
|
|
1523
|
-
|
|
1524
|
-
**Q6: What happens if I don't provide a Cloudinary API key?**
|
|
1525
|
-
A: The `UploadService` will fail during initialization or throw an descriptive error when the first upload command is executed.
|
|
1526
|
-
|
|
1527
|
-
**Q7: How do I contribute a new notification channel?**
|
|
1528
|
-
A: 1. Create a new class implementing `INotificationChannel`. 2. Update the `NotificationChannelFactory`. 3. Add the necessary config to `NotificationConfigService`.
|
|
1529
|
-
|
|
1530
|
-
**Q8: Does the package support multi-tenancy?**
|
|
1531
|
-
A: The architecture supports it at the logic level (e.g., prefixing Redis keys), but there is no built-in "Tenant Selector" strategy yet.
|
|
1532
|
-
|
|
1533
|
-
---
|
|
1534
|
-
|
|
1535
|
-
### Phase 2: Validation & Enterprise Security
|
|
1536
|
-
|
|
1537
|
-
**Q9: Why does `@TextField` convert everything to lowercase by default?**
|
|
1538
|
-
A: To ensure data consistency in searches and database indexes. If you need case-sensitive fields, use the `CapitalField` or standard `class-validator` decorators.
|
|
1539
|
-
|
|
1540
|
-
**Q10: Is the SQL Injection regex 100% foolproof?**
|
|
1541
|
-
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.
|
|
1542
|
-
|
|
1543
|
-
**Q11: Can I use `@PhoneField` for American numbers?**
|
|
1544
|
-
A: Yes, pass `'US'` as the first argument to the decorator: `@PhoneField('US')`.
|
|
1545
|
-
|
|
1546
|
-
**Q12: Why is `@IsOptional()` included in most composite decorators?**
|
|
1547
|
-
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.
|
|
1548
|
-
|
|
1549
|
-
**Q13: How do I validate a field that must be exactly 14 characters?**
|
|
1550
|
-
A: Use `@TextField('Field', 14, 14)`.
|
|
1551
|
-
|
|
1552
|
-
**Q14: Does `@EmailField` check if the domain actually exists?**
|
|
1553
|
-
A: No, it performs structural validation (regex) only. For DNS checks, you would need a custom validator or a third-party service integration.
|
|
1554
|
-
|
|
1555
|
-
**Q15: What is the performance impact of the global SQLi interceptor?**
|
|
1556
|
-
A: Negligible. Regex checks on request bodies typically take less than 1ms, even for large payloads.
|
|
1557
|
-
|
|
1558
|
-
**Q16: Can I use these decorators outside of NestJS?**
|
|
1559
|
-
A: No, they are heavily dependent on `@nestjs/common` and the NestJS decorator metadata system.
|
|
1560
|
-
|
|
1561
|
-
---
|
|
1562
|
-
|
|
1563
|
-
### Phase 3: High-Performance Caching (Redis)
|
|
1564
|
-
|
|
1565
|
-
**Q17: Why use `ioredis` instead of the native `redis` package?**
|
|
1566
|
-
A: `ioredis` provides superior support for Promises, Clusters, Sentinels, and automatic reconnection logic.
|
|
1567
|
-
|
|
1568
|
-
**Q18: How do I clear the entire cache?**
|
|
1569
|
-
A: Use the `flushAll()` method in the `RedisService` (Warning: This is a destructive operation).
|
|
1570
|
-
|
|
1571
|
-
**Q19: Can I use Redis for session management with this package?**
|
|
1572
|
-
A: Absolutely. You can wrap the `get` and `set` methods to create a custom session store for `express-session` or `passport`.
|
|
1573
|
-
|
|
1574
|
-
**Q20: What is the maximum size of a value I can store in Redis?**
|
|
1575
|
-
A: 512MB, though we recommend keeping cached objects under 100KB for optimal network performance.
|
|
1576
|
-
|
|
1577
|
-
**Q21: How do I implement a "Wait-for-Lock" logic?**
|
|
1578
|
-
A: Use the `waitForLock(resource, timeout)` helper, which polls the `acquireLock` method until the lock is available or the timeout is reached.
|
|
1579
|
-
|
|
1580
|
-
**Q22: Is the Pub/Sub system reliable?**
|
|
1581
|
-
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).
|
|
1582
|
-
|
|
1583
|
-
**Q23: How do I handle Redis clusters?**
|
|
1584
|
-
A: Pass the cluster node array in the `REDIS_HOST` environment variable (comma-separated). The service will automatically detect and initialize a Cluster client.
|
|
1585
|
-
|
|
1586
|
-
---
|
|
1587
|
-
|
|
1588
|
-
### Phase 4: Reliable Notifications (Reliable Hub)
|
|
1589
|
-
|
|
1590
|
-
**Q24: Why use BullMQ instead of simple `Promise.all`?**
|
|
1591
|
-
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.
|
|
1592
|
-
|
|
1593
|
-
**Q25: Can I send HTML emails with Nodemailer?**
|
|
1594
|
-
A: Yes, pass the `html` property inside the `channelOptions` object of the `NotificationMessage`.
|
|
1595
|
-
|
|
1596
|
-
**Q26: How do I set up a Telegram bot?**
|
|
1597
|
-
A: Talk to `@BotFather` on Telegram to get a token, then set it as `TELEGRAM_BOT_TOKEN`.
|
|
1598
|
-
|
|
1599
|
-
**Q27: Does the WhatsApp channel support images?**
|
|
1600
|
-
A: Yes, pass `mediaUrl` in the `channelOptions`. Twilio will handle the delivery of the media.
|
|
1601
|
-
|
|
1602
|
-
**Q28: What is the default retry delay?**
|
|
1603
|
-
A: It uses an exponential backoff starting at 5 seconds. (5s, 10s, 20s...).
|
|
1604
|
-
|
|
1605
|
-
**Q29: How do I listen for failed notification jobs?**
|
|
1606
|
-
A: Currently, you can check the Redis `bull:notifications:failed` set or use the BullBoard UI (not included in core).
|
|
1607
|
-
|
|
1608
|
-
---
|
|
1609
|
-
|
|
1610
|
-
### Phase 5: Media & Uploads (Digital Asset Management)
|
|
1611
|
-
|
|
1612
|
-
**Q30: What is the maximum file size for video uploads?**
|
|
1613
|
-
A: By default, the `UploadVideoCommand` allows up to 100MB. This limit is set to balance server memory and Cloudinary's chunked upload efficiency.
|
|
1614
|
-
|
|
1615
|
-
**Q31: How do I handle private file uploads?**
|
|
1616
|
-
A: Use the `access_type: 'private'` or `type: 'authenticated'` options in the `channelOptions` when calling the upload methods. Note that you will also need to generate signed URLs to view these files.
|
|
1617
|
-
|
|
1618
|
-
**Q32: Does the package support local file storage?**
|
|
1619
|
-
A: No. `@bts-soft/upload` is cloud-native and focuses on Cloudinary. To support local storage, you would need to implement a `LocalStorageStrategy`.
|
|
1620
|
-
|
|
1621
|
-
**Q33: How can I prevent users from uploading large TIFF files?**
|
|
1622
|
-
A: The `validateFile` method already limits images to `jpg`, `png`, `webp`, and `gif`. Any other extension will trigger a `400 Bad Request`.
|
|
1623
|
-
|
|
1624
|
-
**Q34: What is a "Public ID" in Cloudinary?**
|
|
1625
|
-
A: It is the unique identifier (filename) of the asset in your cloud storage. Our system generates these using a timestamp + original filename hash to avoid collisions.
|
|
1626
|
-
|
|
1627
|
-
**Q35: How do I generate a thumbnail for a video?**
|
|
1628
|
-
A: Simply change the extension of the video URL to `.jpg` or use Cloudinary's dynamic transformation URL parameters.
|
|
1629
|
-
|
|
1630
|
-
**Q36: Can I use this for S3?**
|
|
1631
|
-
A: Yes, by implementing the `IUploadStrategy` for AWS SDK and injecting it into the service.
|
|
1632
|
-
|
|
1633
|
-
---
|
|
1634
|
-
|
|
1635
|
-
### Phase 6: Advanced Troubleshooting & Edge Cases
|
|
1636
|
-
|
|
1637
|
-
**Q37: Why am I getting "Circular dependency" errors?**
|
|
1638
|
-
A: This usually happens when two services in different modules import each other. Use `forwardRef(() => ...)` in your module definitions to resolve this.
|
|
1639
|
-
|
|
1640
|
-
**Q38: How do I migrate from an old Redis package?**
|
|
1641
|
-
A: Replace your direct client calls with `redisService` methods. Most common operations (get, set, del) have identical names.
|
|
1642
|
-
|
|
1643
|
-
**Q39: How do I handle Arabic characters in the SQL injection check?**
|
|
1644
|
-
A: The regex is designed to allow Arabic character ranges while still blocking SQL-specific keywords and characters like `'`, `;`, and `--`.
|
|
1645
|
-
|
|
1646
|
-
**Q40: Can I use multiple Redis instances?**
|
|
1647
|
-
A: The current `CacheModule` supports a single global instance. For multiple instances, you would need to define multiple providers using different injection tokens.
|
|
1648
|
-
|
|
1649
|
-
**Q41: How do I unit test a service that uses @bts-soft/core?**
|
|
1650
|
-
A: Mock the injected services (`RedisService`, `UploadService`, etc.) using standard NestJS testing utilities like `jest.mock()`.
|
|
1651
|
-
|
|
1652
|
-
**Q42: What if my Twilio account is suspended?**
|
|
1653
|
-
A: The notification service will throw a `401 Unauthorized` or `403 Forbidden` error. You should monitor your logs for these specific status codes.
|
|
1654
|
-
|
|
1655
|
-
---
|
|
1656
|
-
|
|
1657
|
-
### 10. `Exponential Backoff Waiter`
|
|
1658
|
-
When waiting for a resource, don't spam the network. Scale your wait time.
|
|
1659
|
-
|
|
1660
|
-
```typescript
|
|
1661
|
-
async waitForResource(key: string, maxRetries = 5) {
|
|
1662
|
-
let delay = 1000;
|
|
1663
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
1664
|
-
if (await this.redis.exists(key)) return true;
|
|
1665
|
-
await new Promise(r => setTimeout(r, delay));
|
|
1666
|
-
delay *= 2;
|
|
1667
|
-
}
|
|
1668
|
-
return false;
|
|
1669
|
-
}
|
|
1670
|
-
```
|
|
1671
|
-
|
|
1672
|
-
### 11. `The Sliding Window Rate Limiter`
|
|
1673
|
-
Using Redis Sorted Sets to implement a precise rate limiter that handles "bursty" traffic.
|
|
1674
|
-
|
|
1675
|
-
```typescript
|
|
1676
|
-
async isAllowedInWindow(userId: string, windowInSec = 60, limit = 10) {
|
|
1677
|
-
const key = `ratelimit:${userId}`;
|
|
1678
|
-
const now = Date.now();
|
|
1679
|
-
const windowStart = now - (windowInSec * 1000);
|
|
1680
|
-
|
|
1681
|
-
const multi = this.redis.client.multi();
|
|
1682
|
-
multi.zremrangebyscore(key, 0, windowStart);
|
|
1683
|
-
multi.zadd(key, now, now.toString());
|
|
1684
|
-
multi.zcard(key);
|
|
1685
|
-
multi.expire(key, windowInSec);
|
|
1686
|
-
|
|
1687
|
-
const results = await multi.exec();
|
|
1688
|
-
const count = results[2][1] as number;
|
|
1689
|
-
return count <= limit;
|
|
1690
|
-
}
|
|
1691
|
-
```
|
|
1692
|
-
|
|
1693
|
-
### 12. `Partial Cache Update` (Patching)
|
|
1694
|
-
Updating specific fields in a cached object without re-fetching everything.
|
|
1695
|
-
|
|
1696
|
-
```typescript
|
|
1697
|
-
async patchUserCache(userId: string, partialData: Partial<User>) {
|
|
1698
|
-
const key = `user:${userId}`;
|
|
1699
|
-
const existing = await this.redis.get<User>(key);
|
|
1700
|
-
if (cached) {
|
|
1701
|
-
await this.redis.set(key, { ...existing, ...partialData });
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
```
|
|
1705
|
-
|
|
1706
|
-
### 13. `The Decoupled Producer/Consumer` (Redis Lists)
|
|
1707
|
-
A simple, low-overhead queue pattern for non-critical background tasks.
|
|
1708
|
-
|
|
1709
|
-
```typescript
|
|
1710
|
-
// Producer
|
|
1711
|
-
async addToGenericQueue(data: any) {
|
|
1712
|
-
await this.redis.lPush('tasks:generic', data);
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
// Consumer (Mock)
|
|
1716
|
-
async processNextTask() {
|
|
1717
|
-
const task = await this.redis.rPop('tasks:generic');
|
|
1718
|
-
if (task) await execute(task);
|
|
1719
|
-
}
|
|
1720
|
-
```
|
|
1721
|
-
|
|
1722
|
-
### 14. `HyperLogLog Unique Visitor Count`
|
|
1723
|
-
Count millions of unique IPs using only 12KB of memory.
|
|
1724
|
-
|
|
1725
|
-
```typescript
|
|
1726
|
-
async trackUniqueVisit(ip: string) {
|
|
1727
|
-
await this.redis.pfAdd('stats:unique_visitors', ip);
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
async getUniqueCount() {
|
|
1731
|
-
return await this.redis.pfCount('stats:unique_visitors');
|
|
1732
|
-
}
|
|
1733
|
-
```
|
|
1734
|
-
|
|
1735
|
-
### 15. `Geospatial User Search`
|
|
1736
|
-
Find users within 5km of a coordinate.
|
|
1737
|
-
|
|
1738
|
-
```typescript
|
|
1739
|
-
async findNearbyUsers(lon: number, lat: number, radius = 5) {
|
|
1740
|
-
return await this.redis.client.georadius('users:geo', lon, lat, radius, 'km');
|
|
1741
|
-
}
|
|
1742
|
-
```
|
|
1743
|
-
|
|
1744
|
-
### 16. `Bitfield Storage` (Permission Flags)
|
|
1745
|
-
Store 64 boolean permissions in a single Redis key for extreme efficiency.
|
|
1746
|
-
|
|
1747
|
-
### 17. `Cache Warming Strategy`
|
|
1748
|
-
Proactively fill the cache during application startup or low-traffic periods.
|
|
1749
|
-
|
|
1750
|
-
### 18. `The "Dogpile" Prevention` (Locking on Miss)
|
|
1751
|
-
Ensure that only one request hits the database when a popular key expires.
|
|
1752
|
-
|
|
1753
|
-
### 19. `Redis Streams for Event Sourcing`
|
|
1754
|
-
Log every state change into an append-only Redis Stream.
|
|
1755
|
-
|
|
1756
|
-
### 20. `Cross-Module Signaling` (Pub/Sub)
|
|
1757
|
-
Trigger a clear-cache event in Module B when Module A updates a shared entity.
|
|
1758
|
-
|
|
1759
|
-
---
|
|
1760
|
-
|
|
1761
|
-
## Internal Source Code Audit: A Line-by-Line Guide
|
|
1762
|
-
|
|
1763
|
-
This section is for senior architects who want to understand exactly how the sausage is made. We walk through the core logic of the absolute most critical files in the ecosystem.
|
|
1764
|
-
|
|
1765
|
-
---
|
|
1766
|
-
|
|
1767
|
-
### Audit: `sqlInjection.regex.ts` (`@bts-soft/validation`)
|
|
1768
|
-
|
|
1769
|
-
The regex in this file is the heart of the security system.
|
|
1770
|
-
|
|
1771
|
-
```typescript
|
|
1772
|
-
// Line 1-2: Defining the keyword list
|
|
1773
|
-
const SQL_KEYWORDS = [
|
|
1774
|
-
'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'UNION' ...
|
|
1775
|
-
];
|
|
1776
|
-
|
|
1777
|
-
// Line 12: Constructing the boundary-aware capture group
|
|
1778
|
-
// We use \b to ensure we don't block words like "Selection" or "Updateable".
|
|
1779
|
-
const pattern = `(\\b(${SQL_KEYWORDS.join('|')})\\b)`;
|
|
1780
|
-
|
|
1781
|
-
// Line 25: Adding non-word delimiters
|
|
1782
|
-
// We catch --, /*, and # which are used for SQL comments to bypass authentication.
|
|
1783
|
-
const fullPattern = `${pattern}|(--)|(\\/\\*)|(\\*\\/)|(')|(")|(;)|(#)`;
|
|
1784
|
-
```
|
|
1785
|
-
|
|
1786
|
-
### Audit: `redis.service.ts` (`@bts-soft/cache`)
|
|
1787
|
-
|
|
1788
|
-
How we handle serialization and Type Safety.
|
|
1789
|
-
|
|
1790
|
-
```typescript
|
|
1791
|
-
// Line 45: The Generic Get method
|
|
1792
|
-
public async get<T = any>(key: string): Promise<T | null> {
|
|
1793
|
-
const value = await this.cacheManager.get(key);
|
|
1794
|
-
if (!value) return null;
|
|
1795
|
-
|
|
1796
|
-
// Logic: We assume values in Redis are JSON strings.
|
|
1797
|
-
// If parsing fails, we return the raw string.
|
|
1798
|
-
try {
|
|
1799
|
-
return JSON.parse(value as string);
|
|
1800
|
-
} catch {
|
|
1801
|
-
return value as unknown as T;
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
```
|
|
1805
|
-
|
|
1806
|
-
### Audit: `NotificationChannel.factory.ts` (`@bts-soft/notifications`)
|
|
1807
|
-
|
|
1808
|
-
The Dependency Injection orchestration.
|
|
1809
|
-
|
|
1810
|
-
```typescript
|
|
1811
|
-
// Line 60: The Switch-based instantiation
|
|
1812
|
-
// Note how we only instantiate a channel if it is actually called.
|
|
1813
|
-
// This saves memory by avoiding loading heavy SDKs (like Firebase) unless needed.
|
|
1814
|
-
public getChannel(channelType: ChannelType): INotificationChannel {
|
|
1815
|
-
switch (channelType) {
|
|
1816
|
-
case ChannelType.EMAIL:
|
|
1817
|
-
return new EmailChannel(this.configService.getEmailConfig());
|
|
1818
|
-
// ...
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
```
|
|
1822
|
-
|
|
1823
|
-
---
|
|
1824
|
-
|
|
1825
|
-
### Audit: `GeneralResponseInterceptor` (`@bts-soft/common`)
|
|
1826
|
-
|
|
1827
|
-
The logic for standardizing success and error envelopes.
|
|
1828
|
-
|
|
1829
|
-
```typescript
|
|
1830
|
-
// Line 35: The Success Mapping
|
|
1831
|
-
// We use rxjs map operator to transform the stream.
|
|
1832
|
-
// This ensures that even if a controller returns a plain object,
|
|
1833
|
-
// the client receives a structured response.
|
|
1834
|
-
map((data: any) => {
|
|
1835
|
-
return {
|
|
1836
|
-
success: true,
|
|
1837
|
-
statusCode: context.switchToHttp().getResponse().statusCode,
|
|
1838
|
-
message: data?.message || 'Request successful',
|
|
1839
|
-
data: data?.data || data,
|
|
1840
|
-
// ...
|
|
1841
|
-
};
|
|
1842
|
-
})
|
|
1843
|
-
|
|
1844
|
-
// Line 75: The GraphQL Error Shim
|
|
1845
|
-
// This is critical for keeping Apollo Server responses standard.
|
|
1846
|
-
catchError((error) => {
|
|
1847
|
-
const response = this.formatError(error);
|
|
1848
|
-
return throwError(() => new GraphQLError(response.message, {
|
|
1849
|
-
extensions: response
|
|
1850
|
-
}));
|
|
1851
|
-
})
|
|
1852
|
-
```
|
|
1853
|
-
|
|
1854
|
-
### Audit: `BaseEntity.ts` (`@bts-soft/common`)
|
|
1855
|
-
|
|
1856
|
-
The lifecycle hooks and ULID generation.
|
|
1857
|
-
|
|
1858
|
-
```typescript
|
|
1859
|
-
// Line 12: The @PrimaryColumn with ULID
|
|
1860
|
-
// We don't use @PrimaryGeneratedColumn('uuid') because we want
|
|
1861
|
-
// lexicographical ordering.
|
|
1862
|
-
@PrimaryColumn('varchar', { length: 26 })
|
|
1863
|
-
id: string = ulid();
|
|
1864
|
-
|
|
1865
|
-
// Line 30: The AfterInsert Hook
|
|
1866
|
-
// Used for automatic audit logging across the entire ecosystem.
|
|
1867
|
-
@AfterInsert()
|
|
1868
|
-
logInsert() {
|
|
1869
|
-
console.log(`[ENTITY_CREATED] ${this.constructor.name} ID: ${this.id}`);
|
|
1870
|
-
}
|
|
1871
|
-
```
|
|
1872
|
-
|
|
1873
|
-
### Audit: `uploadImage.command.ts` (`@bts-soft/upload`)
|
|
1874
|
-
|
|
1875
|
-
The bridge between the Service and the Strategy.
|
|
1876
|
-
|
|
1877
|
-
```typescript
|
|
1878
|
-
// Line 10: The Execute Method
|
|
1879
|
-
// The command doesn't implement upload logic; it provides the "Context".
|
|
1880
|
-
// This follows the Command Pattern to the letter.
|
|
1881
|
-
public async execute(): Promise<UploadResult> {
|
|
1882
|
-
const result = await this.strategy.upload(this.stream, this.options);
|
|
1883
|
-
this.notifyObservers('onUploadSuccess', result);
|
|
1884
|
-
return result;
|
|
1885
|
-
}
|
|
1886
|
-
```
|
|
1887
|
-
|
|
1888
|
-
---
|
|
1889
|
-
|
|
1890
|
-
## Production Hardening & High Availability
|
|
1891
|
-
|
|
1892
|
-
A production deployment of `@bts-soft/core` requires specific infrastructure tuning to handle enterprise-level loads.
|
|
1893
|
-
|
|
1894
|
-
### 1. Redis High Availability (Sentinel vs. Cluster)
|
|
1895
|
-
|
|
1896
|
-
Depending on your traffic, you should choose one of these two topologies:
|
|
1897
|
-
|
|
1898
|
-
#### Redis Sentinel (Failover)
|
|
1899
|
-
Recommended for most medium-scale applications.
|
|
1900
|
-
- **Topology**: 1 Master, 2 Slavic replicas, 3 Sentinels.
|
|
1901
|
-
- **Service Config**: Use the `sentinel` options in `ioredis`.
|
|
1902
|
-
|
|
1903
|
-
#### Redis Cluster (Horizontal Scale)
|
|
1904
|
-
Recommended for million-user applications.
|
|
1905
|
-
- **Topology**: Multiple masters with sharding.
|
|
1906
|
-
- **Service Config**: Enable `cluster` mode in the `CacheModule`.
|
|
1907
|
-
|
|
1908
|
-
### 2. BullMQ Worker Tuning
|
|
1909
|
-
|
|
1910
|
-
The notification system can be a bottleneck if not tuned.
|
|
1911
|
-
- **Concurrency**: Increase the number of concurrent jobs per worker (Default is 1).
|
|
1912
|
-
- **Separation**: Run the `NotificationProcessor` in a separate microservice to avoid CPU contention with your API.
|
|
1913
|
-
|
|
1914
|
-
---
|
|
1915
|
-
|
|
1916
|
-
## Developer Productivity: IDE Snippets & Tools
|
|
1917
|
-
|
|
1918
|
-
To speed up development, we recommend adding these VS Code snippets.
|
|
1919
|
-
|
|
1920
|
-
### Snippet: New DTO Field
|
|
1921
|
-
```json
|
|
1922
|
-
"BTS DTO Field": {
|
|
1923
|
-
"prefix": "bts-field",
|
|
1924
|
-
"body": [
|
|
1925
|
-
"@TextField('${1:Name}', ${2:3}, ${3:100})",
|
|
1926
|
-
"${4:fieldName}: string;"
|
|
1927
|
-
]
|
|
1928
|
-
}
|
|
1929
|
-
```
|
|
1930
|
-
|
|
1931
|
-
### Snippet: Cache-Aside Pattern
|
|
1932
|
-
```json
|
|
1933
|
-
"BTS Cache-Aside": {
|
|
1934
|
-
"prefix": "bts-cache",
|
|
1935
|
-
"body": [
|
|
1936
|
-
"const cacheKey = `${1:prefix}:${id}`;",
|
|
1937
|
-
"const cached = await this.redis.get(cacheKey);",
|
|
1938
|
-
"if (cached) return cached;",
|
|
1939
|
-
"const data = await this.db.find(id);",
|
|
1940
|
-
"await this.redis.set(cacheKey, data, 3600);",
|
|
1941
|
-
"return data;"
|
|
1942
|
-
]
|
|
1943
|
-
}
|
|
1944
|
-
```
|
|
1945
|
-
|
|
1946
|
-
---
|
|
1947
|
-
|
|
1948
|
-
## Global Error Code Reference
|
|
1949
|
-
|
|
1950
|
-
This section serves as a comprehensive dictionary for every error code returnable by the external providers integrated into `@bts-soft/core`.
|
|
1951
|
-
|
|
1952
|
-
### 1. Cloudinary API Errors
|
|
1953
|
-
| Code | Meaning | BTS Soft Recommendation |
|
|
1954
|
-
| :--- | :--- | :--- |
|
|
1955
|
-
| `400` | Invalid request parameters | Check your `channelOptions` and folders. |
|
|
1956
|
-
| `401` | Authentication failed | Verify `CLOUDINARY_API_KEY` and `SECRET`. |
|
|
1957
|
-
| `403` | Access denied / Capacity exceeded | Check your Cloudinary plan limits. |
|
|
1958
|
-
| `404` | Asset not found | Double check the `public_id` provided. |
|
|
1959
|
-
| `420` | Rate limit reached | Implement a retry delay or upgrade plan. |
|
|
1960
|
-
|
|
1961
|
-
### 2. Twilio (SMS/WhatsApp) Errors
|
|
1962
|
-
| Code | Meaning | BTS Soft Recommendation |
|
|
1963
|
-
| :--- | :--- | :--- |
|
|
1964
|
-
| `21211` | Invalid 'To' Phone Number | Check the recipientId format and country code. |
|
|
1965
|
-
| `21408` | Landline cannot receive SMS | Ensure the number is a mobile or SMS-enabled. |
|
|
1966
|
-
| `21614` | To number is not a mobile number | Verify the phone type. |
|
|
1967
|
-
| `63003` | WhatsApp message undeliverable | Check if user has opted in or if the session is open. |
|
|
1968
|
-
|
|
1969
|
-
### 3. Firebase (FCM) Errors
|
|
1970
|
-
| Code | Meaning | BTS Soft Recommendation |
|
|
1971
|
-
| :--- | :--- | :--- |
|
|
1972
|
-
| `messaging/invalid-argument` | The payload is malformed. | Verify the structure of your `channelOptions`. |
|
|
1973
|
-
| `messaging/registration-token-not-registered` | The token is no longer valid. | Remove this token from your database. |
|
|
1974
|
-
| `messaging/message-rate-exceeded` | Too many messages sent. | Implement a queue delay. |
|
|
1975
|
-
|
|
1976
|
-
### 4. Nodemailer (SMTP) Errors
|
|
1977
|
-
| Code | Meaning | BTS Soft Recommendation |
|
|
1978
|
-
| :--- | :--- | :--- |
|
|
1979
|
-
| `535` | Authentication failed | Check your app-specific password. |
|
|
1980
|
-
| `550` | Mailbox unavailable | The recipient email does not exist. |
|
|
1981
|
-
| `554` | Transaction failed (Spam?) | Your IP might be blacklisted. Use a relay like SendGrid. |
|
|
1982
|
-
|
|
1983
|
-
---
|
|
1984
|
-
|
|
1985
|
-
## Global Best Practices & Performance Guide
|
|
1986
|
-
|
|
1987
|
-
---
|
|
1988
|
-
|
|
1989
|
-
## Global Best Practices & Performance Guide
|
|
1990
|
-
|
|
1991
|
-
**Q43: How do I handle "Connection Lost" in the Redis service?**
|
|
1992
|
-
A: The `RedisService` utilizes `ioredis` with an automatic `retryStrategy`. By default, it will attempt to reconnect every 2 seconds, but you can configure this in the `CacheModule` options if needed.
|
|
1993
|
-
|
|
1994
|
-
**Q44: Can I use multiple Cloudinary accounts?**
|
|
1995
|
-
A: Not with the default `UploadModule`. It is configured to use a single global account. For multi-account support, you would need to instantiate multiple `Cloudinary` clients manually.
|
|
1996
|
-
|
|
1997
|
-
**Q45: Does the `BaseEntity` support Soft Deletes?**
|
|
1998
|
-
A: The current `BaseEntity` does not include `@DeleteDateColumn`. To support soft deletes, you can add this column to your specific entity class or create a `BaseSoftDeleteEntity`.
|
|
1999
|
-
|
|
2000
|
-
**Q46: Is there a way to prioritize certain notification jobs?**
|
|
2001
|
-
A: Yes, BullMQ supports job priorities. We plan to expose the `priority` option in the `NotificationMessage` interface in the next minor release (v2.3).
|
|
2002
|
-
|
|
2003
|
-
**Q47: How do I change the default email template?**
|
|
2004
|
-
A: The `EmailChannel` is designed to be template-agnostic. You should generate your HTML using a library like `handlebars` or `ejs` and pass it to the `html` field in `channelOptions`.
|
|
2005
|
-
|
|
2006
|
-
**Q48: Does the WhatsApp channel support location messages?**
|
|
2007
|
-
A: Yes, pass `persistentAction` or `location` parameters in the `channelOptions` as per Twilio’s documentation.
|
|
2008
|
-
|
|
2009
|
-
**Q49: How do I handle "Rate Limit Exceeded" from Telegram?**
|
|
2010
|
-
A: When Telegram returns a `429`, our service will bubble up the error. You should implement a "Wait-and-Retry" logic in your service or use the BullMQ backoff.
|
|
2011
|
-
|
|
2012
|
-
**Q50: Can I use this package in a Serverless environment (AWS Lambda)?**
|
|
2013
|
-
A: Yes, but you must ensure that your Redis and external API connections are handled efficiently to avoid cold-start delays. Specifically, ensure the Redis client is initialized outside the handler.
|
|
2014
|
-
|
|
2015
|
-
**Q51: Why is my `GeneralResponseInterceptor` not catching errors?**
|
|
2016
|
-
A: Ensure that you are not catching and "swallowing" errors in your service or controller before the interceptor can see them. The interceptor only catches unhandled exceptions.
|
|
2017
|
-
|
|
2018
|
-
**Q52: Is there a built-in UI for managing the notification queue?**
|
|
2019
|
-
A: No, but we recommend integrating `Bull-Board` or `Arena` into your application to monitor the Redis queue visually.
|
|
2020
|
-
|
|
2021
|
-
**Q53: How do I validate a field that must be a valid ULID?**
|
|
2022
|
-
A: Use `@IdField(26)` which enforces the exact length (26 chars) expected for a ULID.
|
|
2023
|
-
|
|
2024
|
-
**Q54: Does the package support Webhooks for receiving messages?**
|
|
2025
|
-
A: No. `@bts-soft/notifications` is exclusively for *outgoing* messages. For incoming webhooks, you must implement dedicated controllers.
|
|
2026
|
-
|
|
2027
|
-
**Q55: What is the recommended Redis eviction policy?**
|
|
2028
|
-
A: For caching, we recommend `allkeys-lru`. For BullMQ, you must ensure that your eviction policy does not delete pending jobs (i.e., set a separate Redis instance for queues).
|
|
2029
|
-
|
|
2030
|
-
**Q56: How do I handle large file uploads in a low-memory environment?**
|
|
2031
|
-
A: The `UploadService` uses streams, which means the file is never fully loaded into RAM. This makes it safe for small containers (e.g., 512MB RAM).
|
|
2032
|
-
|
|
2033
|
-
**Q57: Can I use the `RedisService` for Pub/Sub and Caching simultaneously?**
|
|
2034
|
-
A: No. Due to how Redis clients work, a client in "subscriber" mode cannot perform normal GET/SET operations. You must use two separate instances or clients.
|
|
2035
|
-
|
|
2036
|
-
**Q58: Why does `@NameField` capitalize "McDonnald" incorrectly?**
|
|
2037
|
-
A: Our `CapitalizeWords` utility uses a simple whitespace-based split. Complex names with apostrophes or interior capitals may require custom logic.
|
|
2038
|
-
|
|
2039
|
-
**Q59: How do I bypass the `SqlInjectionInterceptor` for a specific route?**
|
|
2040
|
-
A: You cannot "bypass" a global interceptor easily. If you have a route that legitimately needs to receive SQL-like strings (e.g., a code editor), you should apply the interceptor at the controller level instead of globally.
|
|
2041
|
-
|
|
2042
|
-
---
|
|
2043
|
-
|
|
2044
|
-
### Phase 7: Deep Architecture & Performance FAQ
|
|
2045
|
-
|
|
2046
|
-
**Q60: What is the bottleneck in the Notification system?**
|
|
2047
|
-
A: Usually the network latency to the third-party providers (Twilio, Firebase). This is why the asynchronous queue architecture is mandatory.
|
|
2048
|
-
|
|
2049
|
-
**Q61: How many IOps can the RedisService handle?**
|
|
2050
|
-
A: On a standard Redis instance, the service can handle upwards of 10,000 GET/SET operations per second with less than 2ms latency.
|
|
2051
|
-
|
|
2052
|
-
**Q62: Should I use `@Exclude()` on the `id` field of a `BaseEntity`?**
|
|
2053
|
-
A: Only if you want to hide the ID from the API response (security by obscurity). Generally, IDs are harmless and necessary for client-side state management.
|
|
2054
|
-
|
|
2055
|
-
**Q63: Does the `UploadService` support parallel uploads?**
|
|
2056
|
-
A: Yes. Since it is stateless and uses streams, you can execute multiple upload commands concurrently without any cross-talk.
|
|
2057
|
-
|
|
2058
|
-
**Q64: How do I debug hidden `class-transformer` issues?**
|
|
2059
|
-
A: Set the logger level to `debug` in your `main.ts` and monitor the `Transform` operations in the console.
|
|
2060
|
-
|
|
2061
|
-
**Q65: Can I use this with NestJS Microservices (NATS/Inspur/RabbitMQ)?**
|
|
2062
|
-
A: Yes. All service logic is decoupled from HTTP and will work perfectly inside a Microservice controller.
|
|
2063
|
-
## The SQL Injection Defense Wiki
|
|
2064
|
-
|
|
2065
|
-
This section provides a deep technical analysis of how `@bts-soft/core` protects your data from malicious SQL injection attacks.
|
|
2066
|
-
|
|
2067
|
-
---
|
|
2068
|
-
|
|
2069
|
-
### Understanding the Threat Landscape
|
|
2070
|
-
|
|
2071
|
-
SQL Injection (SQLi) remains one of the most critical vulnerabilities in modern web applications. At its core, SQLi occurs when untrusted data is concatenated directly into a database query.
|
|
2072
|
-
|
|
2073
|
-
### Tier 1: The `SQL_INJECTION_REGEX`
|
|
2074
|
-
|
|
2075
|
-
Our primary defense is a highly optimized regular expression designed to catch the most dangerous SQL keywords and syntax patterns.
|
|
2076
|
-
|
|
2077
|
-
**The Regex Pattern:**
|
|
2078
|
-
```regex
|
|
2079
|
-
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|TRUNCATE|ALTER|EXEC|CREATE|GRANT|REVOKE|DATABASE|SCHEMA|TABLE|COLUMN|FROM|WHERE|HAVING|ORDER\s+BY|GROUP\s+BY|LIMIT|OFFSET|JOIN|INNER|OUTER|LEFT|RIGHT|FULL|CROSS|NATURAL|INTO|VALUES|SET|TABLESPACE|TRIGGER|PROCEDURE|FUNCTION|VIEW|INDEX|ALL|ANY|SOME|EXISTS|BETWEEN|LIKE|SIMILAR|ILIKE|REGEX|REGEXP|GLOB|SOUNDS|MATCH|ESCAPE|OR|AND|XOR|NOT|AS|IS|NULL|TRUE|FALSE|UNKNOWN|COALESCE|NULLIF|IFNULL|ISNULL|IF|THEN|ELSE|CASE|WHEN|END|WAITFOR|DELAY|SLEEP|BENCHMARK|HEX|MD5|SHA1|SHA2|CHRT|ORD|ASCII|CONCAT|SUBSTR|SUBSTRING|LENGTH|CHAR_LENGTH|BIT_LENGTH|OCTET_LENGTH|UPPER|LOWER|INITCAP|TRIM|LTRIM|RTRIM|RPAD|LPAD|REPLACE|TRANSLATE|REPEAT|REVERSE|QUOTE|UNQUOTE|DUMP|VSIZE|UID|USER|SESSION_USER|SYSTEM_USER|CURRENT_USER|CURRENT_DATE|CURRENT_TIME|CURRENT_TIMESTAMP|LOCALTIME|LOCALTIMESTAMP|CAST|CONVERT|TO_DATE|TO_TIMESTAMP|TO_NUMBER|TO_CHAR|TRUNC|ROUND|MOD|ABS|CEIL|FLOOR|EXP|LN|LOG|POWER|SQRT|PI|SIN|COS|TAN|ASIN|ACOS|ATAN|EXP|LN|LOG|POWER|SQRT|PI|SIN|COS|TAN|ASIN|ACOS|ATAN)\b)|(--)|(\/\*)|(\*\/)|(\')|(\")|(\;)|(\#)/gi
|
|
2080
|
-
```
|
|
2081
|
-
|
|
2082
|
-
**What it blocks:**
|
|
2083
|
-
1. **DML Keywords**: `SELECT`, `INSERT`, `UPDATE`, `DELETE`.
|
|
2084
|
-
2. **DDL Keywords**: `DROP`, `ALTER`, `CREATE`, `TRUNCATE`.
|
|
2085
|
-
3. **Comments**: `--`, `/*`, `#`.
|
|
2086
|
-
4. **Terminators**: `;`.
|
|
2087
|
-
5. **Sensitive Functions**: `SLEEP()`, `BENCHMARK()`, `MD5()`.
|
|
2088
|
-
6. **Boolean Logic**: `OR 1=1`, `AND 1=0`.
|
|
2089
|
-
|
|
2090
|
-
---
|
|
2091
|
-
|
|
2092
|
-
### Tier 2: The Global Interceptor (`SqlInjectionInterceptor`)
|
|
2093
|
-
|
|
2094
|
-
The `SqlInjectionInterceptor` ensures that this regex is applied to *every* incoming request.
|
|
2095
|
-
|
|
2096
|
-
#### How it works:
|
|
2097
|
-
1. **Iteration**: It recursively traverses the `Body`, `Query`, and `Params` objects.
|
|
2098
|
-
2. **Scrubbing**: Every string value is tested against the regex.
|
|
2099
|
-
3. **Bail-out**: If a match is found, it immediately throws a `400 Bad Request` with the message: `"SQL Injection detected in field: [fieldName]"`.
|
|
2100
|
-
|
|
2101
|
-
---
|
|
2102
|
-
|
|
2103
|
-
### Tier 3: Validation Decorators
|
|
2104
|
-
|
|
2105
|
-
For granular control, decorators like `@TextField` and `@EmailField` apply the same regex at the DTO level. This provides better error messages and integrates with `ValidationPipe`.
|
|
2106
|
-
|
|
2107
|
-
---
|
|
2108
|
-
|
|
2109
|
-
## 20+ Advanced Redis Design Patterns
|
|
2110
|
-
|
|
2111
|
-
Building on the basic patterns mentioned earlier, here are advanced strategies for high-scale applications.
|
|
2112
|
-
|
|
2113
|
-
### 1. The "Read-Through" Service Cache
|
|
2114
|
-
Ensures your database is never overwhelmed by high-traffic spikes.
|
|
2115
|
-
|
|
2116
|
-
```typescript
|
|
2117
|
-
async getProductSmartly(id: string) {
|
|
2118
|
-
return await this.redis.getOrSet(`prod:${id}`, async () => {
|
|
2119
|
-
return await this.db.products.findOne(id);
|
|
2120
|
-
}, 3600);
|
|
2121
|
-
}
|
|
2122
|
-
```
|
|
2123
|
-
|
|
2124
|
-
### 2. "Atomic ID Generator" (Distributed)
|
|
2125
|
-
Generate unique, gaps-free numeric IDs across multiple app instances.
|
|
2126
|
-
|
|
2127
|
-
```typescript
|
|
2128
|
-
async getNextInvoiceNumber() {
|
|
2129
|
-
return await this.redis.incr('infra:invoice_counter');
|
|
2130
|
-
}
|
|
2131
|
-
```
|
|
2132
|
-
|
|
2133
|
-
### 3. "Real-time Leaderboard" with Expiring Scores
|
|
2134
|
-
Perfect for "Trending Now" features.
|
|
2135
|
-
|
|
2136
|
-
```typescript
|
|
2137
|
-
async trackView(productId: string) {
|
|
2138
|
-
const score = Date.now();
|
|
2139
|
-
await this.redis.zAdd('trending_products', score, productId);
|
|
2140
|
-
// Optional: Automatically expire old scores using a scheduled task or Lua
|
|
2141
|
-
}
|
|
2142
|
-
```
|
|
2143
|
-
|
|
2144
|
-
### 4. "Distributed Locking" for Finalizing Payments
|
|
2145
|
-
Prevent double-charging users by locking the payment process.
|
|
2146
|
-
|
|
2147
|
-
```typescript
|
|
2148
|
-
async processPayment(userId: string) {
|
|
2149
|
-
const lock = await this.redis.acquireLock(`pay:${userId}`, 'locking_val', 30);
|
|
2150
|
-
if (!lock) throw new ConflictException('Payment in progress');
|
|
2151
|
-
|
|
2152
|
-
try {
|
|
2153
|
-
// payment logic
|
|
2154
|
-
} finally {
|
|
2155
|
-
await this.redis.releaseLock(`pay:${userId}`, 'locking_val');
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
```
|
|
2159
|
-
|
|
2160
|
-
### 5. "User Session Tracker" (Active Users)
|
|
2161
|
-
Store the set of active users with a sliding window.
|
|
2162
|
-
|
|
2163
|
-
```typescript
|
|
2164
|
-
async markUserActive(userId: string) {
|
|
2165
|
-
await this.redis.sAdd('active_users', userId);
|
|
2166
|
-
await this.redis.expire('active_users', 900); // 15 min window
|
|
2167
|
-
}
|
|
2168
|
-
```
|
|
1139
|
+
import { GraphqlModule } from '@bts-soft/core';
|
|
2169
1140
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
---
|
|
2183
|
-
|
|
2184
|
-
## Global Best Practices & Performance Guide
|
|
2185
|
-
|
|
2186
|
-
To get the most out of `@bts-soft/core`, follow these enterprise-grade best practices.
|
|
1141
|
+
@Module({
|
|
1142
|
+
imports: [
|
|
1143
|
+
GraphqlModule.forRoot({
|
|
1144
|
+
autoSchemaFile: true,
|
|
1145
|
+
playground: true,
|
|
1146
|
+
federation: true,
|
|
1147
|
+
webSocket: { enabled: true, path: '/graphql', keepAlive: 10_000 },
|
|
1148
|
+
}),
|
|
1149
|
+
],
|
|
1150
|
+
})
|
|
1151
|
+
export class AppModule {}
|
|
1152
|
+
```
|
|
2187
1153
|
|
|
2188
|
-
|
|
2189
|
-
Never store huge objects in Redis. If you have a list of 5000 items, store the *IDs* in Redis and fetch the items from the DB, or store the list in a compressed format. Redis is fastest when keys are small (<5KB).
|
|
1154
|
+
#### TranslationModule
|
|
2190
1155
|
|
|
2191
|
-
|
|
2192
|
-
Always use the BullMQ queue for production workloads. Direct service calls (`channel.send()`) should be reserved for testing or internal tools where latency and failure don't impact the end-user.
|
|
1156
|
+
Integrates `nestjs-i18n` with automatic locale resolution from `x-lang` and `Accept-Language` headers:
|
|
2193
1157
|
|
|
2194
|
-
|
|
2195
|
-
|
|
1158
|
+
```typescript
|
|
1159
|
+
import { TranslationModule } from '@bts-soft/core';
|
|
2196
1160
|
|
|
2197
|
-
|
|
1161
|
+
@Module({ imports: [TranslationModule] })
|
|
1162
|
+
export class AppModule {}
|
|
1163
|
+
```
|
|
2198
1164
|
|
|
2199
|
-
|
|
1165
|
+
### Resilience: Idempotency and Distributed Locking
|
|
2200
1166
|
|
|
2201
|
-
|
|
1167
|
+
```typescript
|
|
1168
|
+
import {
|
|
1169
|
+
Idempotent, IdempotencyInterceptor,
|
|
1170
|
+
DistributedLock,
|
|
1171
|
+
} from '@bts-soft/core';
|
|
2202
1172
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
1173
|
+
@Controller('payments')
|
|
1174
|
+
export class PaymentController {
|
|
1175
|
+
@Post('charge')
|
|
1176
|
+
@UseInterceptors(IdempotencyInterceptor)
|
|
1177
|
+
@Idempotent({ ttl: 60, headerName: 'x-idempotency-key' })
|
|
1178
|
+
async chargeCard(@Body() body: ChargeDto) {
|
|
1179
|
+
return { transactionId: 'tx_123' };
|
|
1180
|
+
}
|
|
2207
1181
|
|
|
2208
|
-
|
|
1182
|
+
@Post('transfer')
|
|
1183
|
+
@DistributedLock((body) => `lock:user:${body.userId}`, { ttlMs: 5_000 })
|
|
1184
|
+
async transferFunds(@Body() body: TransferDto) {
|
|
1185
|
+
return { status: 'transferred' };
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
```
|
|
2209
1189
|
|
|
2210
|
-
|
|
1190
|
+
### Sub-Path Export Matrix
|
|
2211
1191
|
|
|
2212
|
-
|
|
|
1192
|
+
| Sub-path | Exported Components |
|
|
2213
1193
|
| :--- | :--- |
|
|
2214
|
-
|
|
|
2215
|
-
|
|
|
2216
|
-
|
|
|
2217
|
-
|
|
|
2218
|
-
|
|
|
2219
|
-
|
|
|
1194
|
+
| `@bts-soft/common` | Core bases, decorators, DTOs, filters, interceptors, `IdGenerator`, infrastructure modules |
|
|
1195
|
+
| `@bts-soft/common/typeorm` | `TypeOrmBaseEntity` |
|
|
1196
|
+
| `@bts-soft/common/sequelize` | `SequelizeBaseEntity` |
|
|
1197
|
+
| `@bts-soft/common/mongoose` | `MongooseBaseEntity` |
|
|
1198
|
+
| `@bts-soft/common/prisma` | `PrismaBase`, `IPrismaEntity` |
|
|
1199
|
+
| `@bts-soft/common/resilience` | `ResilienceModule`, `IdempotencyInterceptor`, `@Idempotent()`, `@DistributedLock()`, `DistributedLockService` |
|
|
2220
1200
|
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
## Full Example Codebase: The Marketplace Service
|
|
2224
|
-
|
|
2225
|
-
This section provides a complete, production-grade implementation of a "Marketplace Service" that integrates every single sub-package within the `@bts-soft/core` ecosystem.
|
|
2226
|
-
|
|
2227
|
-
### 1. The Entity Layer (`@bts-soft/common`)
|
|
1201
|
+
### Production Utilities
|
|
2228
1202
|
|
|
2229
1203
|
```typescript
|
|
2230
|
-
import {
|
|
2231
|
-
import { BaseEntity } from '@bts-soft/core';
|
|
1204
|
+
import { disableConsoleInProduction, displayAppBanner } from '@bts-soft/core';
|
|
2232
1205
|
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
price: number;
|
|
2240
|
-
|
|
2241
|
-
@Column({ nullable: true })
|
|
2242
|
-
imageUrl: string;
|
|
1206
|
+
async function bootstrap() {
|
|
1207
|
+
const app = await NestFactory.create(AppModule);
|
|
1208
|
+
setupInterceptors(app);
|
|
1209
|
+
disableConsoleInProduction(); // No-ops all console.* in NODE_ENV=production
|
|
1210
|
+
await app.listen(3000);
|
|
1211
|
+
displayAppBanner('My Service', 3000);
|
|
2243
1212
|
}
|
|
2244
1213
|
```
|
|
2245
1214
|
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
```typescript
|
|
2249
|
-
import { TextField, NumberField, UrlField } from '@bts-soft/core';
|
|
2250
|
-
|
|
2251
|
-
export class CreateProductDto {
|
|
2252
|
-
@TextField('Product Name', 3, 100)
|
|
2253
|
-
name: string;
|
|
1215
|
+
---
|
|
2254
1216
|
|
|
2255
|
-
|
|
2256
|
-
price: number;
|
|
1217
|
+
## Cross-Package Integration Scenarios
|
|
2257
1218
|
|
|
2258
|
-
|
|
2259
|
-
imageUrl?: string;
|
|
2260
|
-
}
|
|
2261
|
-
```
|
|
1219
|
+
### Scenario A: Secure User Registration
|
|
2262
1220
|
|
|
2263
|
-
|
|
1221
|
+
Demonstrates: Validation, Cache, Common, Notifications
|
|
2264
1222
|
|
|
2265
1223
|
```typescript
|
|
2266
|
-
import { Injectable } from '@nestjs/common';
|
|
2267
|
-
import { RedisService, NotificationService, UploadService, ChannelType } from '@bts-soft/core';
|
|
2268
|
-
|
|
2269
1224
|
@Injectable()
|
|
2270
|
-
export class
|
|
1225
|
+
export class AuthService {
|
|
2271
1226
|
constructor(
|
|
2272
|
-
private redis: RedisService,
|
|
2273
|
-
private
|
|
2274
|
-
private
|
|
2275
|
-
private productRepo: ProductRepository,
|
|
1227
|
+
private readonly redis: RedisService,
|
|
1228
|
+
private readonly notifications: NotificationService,
|
|
1229
|
+
private readonly userRepo: UserRepository,
|
|
2276
1230
|
) {}
|
|
2277
1231
|
|
|
2278
|
-
async
|
|
2279
|
-
//
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
const
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
const cached = await this.redis.get(cacheKey);
|
|
2307
|
-
if (cached) return cached;
|
|
2308
|
-
|
|
2309
|
-
// Fetch on miss
|
|
2310
|
-
const product = await this.productRepo.findOneBy({ id });
|
|
2311
|
-
if (product) {
|
|
2312
|
-
await this.redis.set(cacheKey, product, 3600);
|
|
1232
|
+
async register(dto: RegisterUserDto): Promise<void> {
|
|
1233
|
+
// Validation runs automatically via the ValidationPipe before this method executes.
|
|
1234
|
+
// @NameField: "omar sabry" -> "Omar Sabry"
|
|
1235
|
+
// @EmailField: "USER@MAIL.COM" -> "user@mail.com"
|
|
1236
|
+
// @PasswordField: enforces COMPREHENSIVE complexity
|
|
1237
|
+
|
|
1238
|
+
// Prevent duplicate registration under race conditions
|
|
1239
|
+
const lockKey = `registration:lock:${dto.email}`;
|
|
1240
|
+
const lockToken = randomUUID();
|
|
1241
|
+
const acquired = await this.redis.acquireLock(lockKey, lockToken, 30_000);
|
|
1242
|
+
if (!acquired) throw new ConflictException('Registration already in progress');
|
|
1243
|
+
|
|
1244
|
+
try {
|
|
1245
|
+
// Persist with auto-generated ULID from BaseEntity
|
|
1246
|
+
const user = await this.userRepo.create(dto);
|
|
1247
|
+
|
|
1248
|
+
// Cache user profile for fast subsequent reads
|
|
1249
|
+
await this.redis.set(`user:profile:${user.id}`, user, 3600);
|
|
1250
|
+
|
|
1251
|
+
// Queue welcome email asynchronously - does not block the API response
|
|
1252
|
+
await this.notifications.send(ChannelType.EMAIL, {
|
|
1253
|
+
recipientId: user.email,
|
|
1254
|
+
subject: 'Welcome to BTS Soft',
|
|
1255
|
+
body: 'Hi {{name}}, your account is ready.',
|
|
1256
|
+
context: { name: user.name },
|
|
1257
|
+
});
|
|
1258
|
+
} finally {
|
|
1259
|
+
await this.redis.releaseLock(lockKey, lockToken);
|
|
2313
1260
|
}
|
|
2314
|
-
|
|
2315
|
-
return product;
|
|
2316
1261
|
}
|
|
2317
1262
|
}
|
|
2318
1263
|
```
|
|
2319
1264
|
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
## Comprehensive Interface Registry
|
|
2323
|
-
|
|
2324
|
-
This registry documents every property of the main interfaces used across the core sub-packages.
|
|
2325
|
-
|
|
2326
|
-
### 1. `NotificationMessage`
|
|
2327
|
-
| Property | Type | Required | Description |
|
|
2328
|
-
| :--- | :--- | :--- | :--- |
|
|
2329
|
-
| `recipientId` | `string` | Yes | Target identifier (Email, Phone, ChatID, Token). |
|
|
2330
|
-
| `body` | `string` | Yes | The text content of the message. |
|
|
2331
|
-
| `title` | `string` | No | Subject line or Push title. |
|
|
2332
|
-
| `subject` | `string` | No | Explicit field for Email subjects. |
|
|
2333
|
-
| `channelOptions`| `any` | No | Provider-specific properties (HTML, Embeds, etc.). |
|
|
2334
|
-
|
|
2335
|
-
### 2. `UploadResult`
|
|
2336
|
-
| Property | Type | Description |
|
|
2337
|
-
| :--- | :--- | :--- |
|
|
2338
|
-
| `url` | `string` | The secure, absolute URL of the hosted asset. |
|
|
2339
|
-
| `size` | `number` | File size in bytes. |
|
|
2340
|
-
| `filename` | `string` | Original filename as provided. |
|
|
2341
|
-
| `type` | `string` | Media type (image, video, etc.). |
|
|
2342
|
-
| `public_id` | `string` | Cloudinary unique identifier. |
|
|
2343
|
-
|
|
2344
|
-
### 3. `IRedisInterface` (Extended)
|
|
2345
|
-
| Property | Type | Description |
|
|
2346
|
-
| :--- | :--- | :--- |
|
|
2347
|
-
| `client` | `any` | Access to the raw ioredis client for edge cases. |
|
|
2348
|
-
| `prefix` | `string` | The global key prefix used in the current instance. |
|
|
2349
|
-
|
|
2350
|
-
---
|
|
2351
|
-
|
|
2352
|
-
## Advanced Architecture: Observer Implementation Guide
|
|
1265
|
+
### Scenario B: Cache-Aside Pattern for High-Traffic Reads
|
|
2353
1266
|
|
|
2354
|
-
|
|
1267
|
+
Demonstrates: Cache, Common
|
|
2355
1268
|
|
|
2356
1269
|
```typescript
|
|
2357
|
-
|
|
1270
|
+
@Injectable()
|
|
1271
|
+
export class ProductService {
|
|
1272
|
+
constructor(
|
|
1273
|
+
private readonly redis: RedisService,
|
|
1274
|
+
private readonly productRepo: ProductRepository,
|
|
1275
|
+
) {}
|
|
2358
1276
|
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
1277
|
+
async getProduct(id: string): Promise<Product> {
|
|
1278
|
+
// Returns cached value if present, otherwise fetches, caches, and returns
|
|
1279
|
+
return this.redis.getOrSet(
|
|
1280
|
+
`product:${id}`,
|
|
1281
|
+
() => this.productRepo.findOneOrFail({ where: { id } }),
|
|
1282
|
+
600, // Cache for 10 minutes
|
|
1283
|
+
);
|
|
2362
1284
|
}
|
|
2363
|
-
onUploadError(err: Error) {
|
|
2364
|
-
// Error handling logic
|
|
2365
|
-
}
|
|
2366
|
-
onDeleteSuccess(res: any) {}
|
|
2367
|
-
onDeleteError(err: Error) {}
|
|
2368
1285
|
}
|
|
2369
1286
|
```
|
|
2370
1287
|
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
## Technical Appendix: Final Developer Notes
|
|
2374
|
-
|
|
2375
|
-
### Performance Checklist
|
|
2376
|
-
- [ ] Use `mSet` and `mGet` when handling groups of keys.
|
|
2377
|
-
- [ ] Ensure `CLOUDINARY_AUTO` is enabled for images.
|
|
2378
|
-
- [ ] Use `BullMQ` for all user-facing notification flows.
|
|
2379
|
-
- [ ] Regularly monitor Redis memory usage using `info memory`.
|
|
2380
|
-
|
|
2381
|
-
### Security Hardening
|
|
2382
|
-
- [ ] Rotate `TWILIO_AUTH_TOKEN` every 6 months.
|
|
2383
|
-
- [ ] Use `app-specific passwords` for SMTP connections.
|
|
2384
|
-
- [ ] Ensure `SQL_INJECTION_REGEX` matches your domain-specific forbidden characters.
|
|
2385
|
-
|
|
2386
|
-
---
|
|
2387
|
-
|
|
2388
|
-
## Notification Channel Deep Configuration Reference
|
|
2389
|
-
|
|
2390
|
-
This section provides an exhaustive property-by-property reference for the `channelOptions` of every supported notification provider.
|
|
2391
|
-
|
|
2392
|
-
---
|
|
2393
|
-
|
|
2394
|
-
### 1. Email (Nodemailer Options)
|
|
2395
|
-
When using the Email channel, `channelOptions` is passed directly to `transporter.sendMail()`.
|
|
2396
|
-
|
|
2397
|
-
| Property | Type | Description |
|
|
2398
|
-
| :--- | :--- | :--- |
|
|
2399
|
-
| `from` | `string` | Override the default sender email. |
|
|
2400
|
-
| `replyTo` | `string` | Address for user replies. |
|
|
2401
|
-
| `cc` | `string \| string[]` | Carbon copy recipients. |
|
|
2402
|
-
| `bcc` | `string \| string[]` | Blind carbon copy recipients. |
|
|
2403
|
-
| `headers` | `object` | Custom SMTP headers. |
|
|
2404
|
-
| `attachments` | `Attachment[]` | Array of Nodemailer attachment objects. |
|
|
2405
|
-
| `html` | `string` | HTML content of the email. |
|
|
2406
|
-
| `priority` | `'high' \| 'low' \| 'normal'` | Email priority header. |
|
|
2407
|
-
|
|
2408
|
-
---
|
|
2409
|
-
|
|
2410
|
-
### 2. WhatsApp & SMS (Twilio Options)
|
|
2411
|
-
These options are passed to the Twilio Message creation API.
|
|
2412
|
-
|
|
2413
|
-
| Property | Type | Description |
|
|
2414
|
-
| :--- | :--- | :--- |
|
|
2415
|
-
| `mediaUrl` | `string[]` | URLs of media files to send (WhatsApp only). |
|
|
2416
|
-
| `statusCallback` | `string` | URL for Twilio to send delivery status events. |
|
|
2417
|
-
| `validityPeriod` | `number` | How long the message should stay in the queue (seconds). |
|
|
2418
|
-
| `smartEncoded` | `boolean` | Automatic Unicode detection and conversion. |
|
|
2419
|
-
| `provideFeedback` | `boolean` | Whether Twilio should track user interaction. |
|
|
2420
|
-
|
|
2421
|
-
---
|
|
2422
|
-
|
|
2423
|
-
### 3. Firebase Cloud Messaging (FCM Options)
|
|
2424
|
-
These options follow the `admin.messaging.MessagingOptions` and `admin.messaging.Message` structures.
|
|
2425
|
-
|
|
2426
|
-
| Property | Type | Description |
|
|
2427
|
-
| :--- | :--- | :--- |
|
|
2428
|
-
| `data` | `Record<string, string>` | Custom key-value pairs for the app. |
|
|
2429
|
-
| `notification` | `Notification` | Visual notification properties (Icon, Sound, Badge). |
|
|
2430
|
-
| `android` | `AndroidConfig` | Android-specific delivery options (ChannelId, Priority). |
|
|
2431
|
-
| `apns` | `ApnsConfig` | iOS-specific delivery options (Payload, Headers). |
|
|
2432
|
-
| `webpush` | `WebpushConfig` | Web-specific delivery options (FCM options, Headers). |
|
|
2433
|
-
| `condition` | `string` | Topic condition (e.g., `'topicA' in topics && 'topicB' in topics`). |
|
|
2434
|
-
|
|
2435
|
-
---
|
|
2436
|
-
|
|
2437
|
-
### 4. Telegram Bot API Options
|
|
2438
|
-
Passed directly to `bot.sendMessage()`.
|
|
2439
|
-
|
|
2440
|
-
| Property | Type | Description |
|
|
2441
|
-
| :--- | :--- | :--- |
|
|
2442
|
-
| `parse_mode` | `'Markdown' \| 'HTML' \| 'MarkdownV2'` | How to parse the message body. |
|
|
2443
|
-
| `disable_web_page_preview` | `boolean` | Hide link previews in the chat. |
|
|
2444
|
-
| `disable_notification` | `boolean` | Send the message silently. |
|
|
2445
|
-
| `reply_to_message_id` | `number` | Target of a reply message. |
|
|
2446
|
-
| `reply_markup` | `object` | Inline keyboards or custom reply buttons. |
|
|
2447
|
-
|
|
2448
|
-
---
|
|
2449
|
-
|
|
2450
|
-
## Redis Service Logic Internals: A Comprehensive Walkthrough
|
|
1288
|
+
### Scenario C: Real-Time Event Broadcasting
|
|
2451
1289
|
|
|
2452
|
-
|
|
1290
|
+
Demonstrates: Cache Pub/Sub, Notifications
|
|
2453
1291
|
|
|
2454
1292
|
```typescript
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
// Line 30: Serialization Logic
|
|
2462
|
-
// We check if the value is an object. If so, we JSON.stringify it.
|
|
2463
|
-
// This ensures that complex DTOs are stored as retrievable strings.
|
|
2464
|
-
let stringValue: string;
|
|
2465
|
-
if (typeof value === 'object') {
|
|
2466
|
-
stringValue = JSON.stringify(value);
|
|
2467
|
-
} else {
|
|
2468
|
-
stringValue = String(value);
|
|
2469
|
-
}
|
|
1293
|
+
@Injectable()
|
|
1294
|
+
export class OrderEventBus implements OnModuleInit {
|
|
1295
|
+
constructor(
|
|
1296
|
+
private readonly redis: RedisService,
|
|
1297
|
+
private readonly notifications: NotificationService,
|
|
1298
|
+
) {}
|
|
2470
1299
|
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
1300
|
+
async onModuleInit() {
|
|
1301
|
+
await this.redis.subscribe('events:order_shipped', async (data) => {
|
|
1302
|
+
await this.notifications.send(ChannelType.FIREBASE_FCM, {
|
|
1303
|
+
recipientId: data.userFcmToken,
|
|
1304
|
+
title: 'Order Shipped',
|
|
1305
|
+
body: `Your order #${data.orderId} is on the way.`,
|
|
1306
|
+
});
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
2475
1309
|
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
1310
|
+
async publishOrderShipped(orderId: string, userFcmToken: string) {
|
|
1311
|
+
await this.redis.publish('events:order_shipped', { orderId, userFcmToken });
|
|
1312
|
+
}
|
|
2479
1313
|
}
|
|
2480
1314
|
```
|
|
2481
1315
|
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
## Technical Appendix: Performance Tuning for Massive Scale
|
|
2485
|
-
|
|
2486
|
-
When your application reaches 100,000 requests per minute, these optimizations become mandatory.
|
|
2487
|
-
|
|
2488
|
-
### 1. Redis Pipelining
|
|
2489
|
-
Instead of sending 100 `SET` commands one by one, use `mSet` or the raw `client.pipeline()` method. This reduces network round-trips by 99%.
|
|
2490
|
-
|
|
2491
|
-
### 2. Message Batching
|
|
2492
|
-
When sending marketing notifications, batch your jobs into groups of 500. This prevents the BullMQ Redis instance from becoming a CPU bottleneck.
|
|
2493
|
-
|
|
2494
|
-
### 3. Connection Pooling
|
|
2495
|
-
Ensure that `maxConnections` in your `ioredis` config is set higher than the number of worker threads.
|
|
2496
|
-
|
|
2497
|
-
---
|
|
2498
|
-
|
|
2499
|
-
## Technical Appendix: Security Hardening Master Checklist
|
|
2500
|
-
|
|
2501
|
-
Follow this checklist before moving any project using `@bts-soft/core` to production.
|
|
2502
|
-
|
|
2503
|
-
- [ ] **Redis Isolation**: Run Redis on a private VPC subnet. Disable public access.
|
|
2504
|
-
- [ ] **Password Strength**: Ensure `REDIS_PASSWORD` is at least 32 characters long.
|
|
2505
|
-
- [ ] **TLS Everywhere**: Force TLS for SMTP, Cloudinary, and Twilio connections.
|
|
2506
|
-
- [ ] **Sanitization Audit**: Verify that all incoming user strings are decorated with `@TextField` or similar.
|
|
2507
|
-
- [ ] **Rate Limiting**: Enable the `ThrottlerModule` with a maximum of 100 requests per minute per IP.
|
|
2508
|
-
- [ ] **Observer Security**: Ensure your `LoggingObserver` does not log sensitive data (Passwords, Tokens).
|
|
2509
|
-
- [ ] **Dependency Hygiene**: Run `npm audit` weekly to catch vulnerabilities in upstream packages.
|
|
2510
|
-
|
|
2511
|
-
---
|
|
2512
|
-
|
|
2513
|
-
## Technical Appendix: SQL Injection Pattern Database (Advanced)
|
|
2514
|
-
|
|
2515
|
-
Below is a list of the most sophisticated SQL injection patterns and how our regex neutralized them.
|
|
2516
|
-
|
|
2517
|
-
### 1. The "Tautology" Attack
|
|
2518
|
-
**Payload**: `' OR 1=1 --`
|
|
2519
|
-
- **Detection**: The regex catches the `'`, the `OR`, and the `--` (comment initiator).
|
|
2520
|
-
- **Result**: `400 Bad Request`.
|
|
2521
|
-
|
|
2522
|
-
### 2. The "Union-Based" Extraction
|
|
2523
|
-
**Payload**: `UNION SELECT username, password FROM users`
|
|
2524
|
-
- **Detection**: The regex identifies `UNION`, `SELECT`, and `FROM` as high-risk keywords.
|
|
2525
|
-
- **Result**: `400 Bad Request`.
|
|
2526
|
-
|
|
2527
|
-
### 3. The "Time-Based Blind" Injection
|
|
2528
|
-
**Payload**: `; IF (1=1) WAITFOR DELAY '0:0:5' --`
|
|
2529
|
-
- **Detection**: The regex catches the `;`, `IF`, `WAITFOR`, `DELAY`, and `--`.
|
|
2530
|
-
- **Result**: `400 Bad Request`.
|
|
2531
|
-
|
|
2532
|
-
### 4. The "Second-Order" Injection
|
|
2533
|
-
**Payload**: `admin'--` stored in a username field.
|
|
2534
|
-
- **Detection**: Even if it bypasses a weak front-end check, our `@TextField` decorator catches the `'` and `--` during DTO validation.
|
|
2535
|
-
|
|
2536
|
-
---
|
|
2537
|
-
|
|
2538
|
-
## Technical Appendix: Infrastructure Health Check Scripts
|
|
2539
|
-
|
|
2540
|
-
You can use these `curl` commands to verify your `@bts-soft/core` health.
|
|
2541
|
-
|
|
2542
|
-
### 1. Check Redis Connection
|
|
2543
|
-
```bash
|
|
2544
|
-
curl -X GET http://localhost:3000/health/redis
|
|
2545
|
-
```
|
|
2546
|
-
|
|
2547
|
-
### 2. Check Notification Queue
|
|
2548
|
-
```bash
|
|
2549
|
-
curl -X GET http://localhost:3000/health/notifications
|
|
2550
|
-
```
|
|
1316
|
+
### Scenario D: Preference-Aware Multi-Channel Broadcaster
|
|
2551
1317
|
|
|
2552
|
-
|
|
2553
|
-
```bash
|
|
2554
|
-
curl -X POST http://localhost:3000/auth/login \
|
|
2555
|
-
-H "Content-Type: application/json" \
|
|
2556
|
-
-d '{"username": "admin\" OR 1=1", "password": "any"}'
|
|
2557
|
-
```
|
|
1318
|
+
Demonstrates: Notifications, Cache
|
|
2558
1319
|
|
|
2559
|
-
|
|
1320
|
+
```typescript
|
|
1321
|
+
@Injectable()
|
|
1322
|
+
export class NotificationOrchestrator {
|
|
1323
|
+
constructor(
|
|
1324
|
+
private readonly notifications: NotificationService,
|
|
1325
|
+
private readonly redis: RedisService,
|
|
1326
|
+
) {}
|
|
2560
1327
|
|
|
2561
|
-
|
|
1328
|
+
async notifyUser(user: User, payload: NotificationPayload) {
|
|
1329
|
+
const prefKey = `user:notification-prefs:${user.id}`;
|
|
1330
|
+
const prefs = await this.redis.get<UserNotificationPrefs>(prefKey);
|
|
2562
1331
|
|
|
2563
|
-
|
|
1332
|
+
const jobs = [];
|
|
2564
1333
|
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
1334
|
+
if (prefs?.email !== false && user.email) {
|
|
1335
|
+
jobs.push(this.notifications.send(ChannelType.EMAIL, {
|
|
1336
|
+
recipientId: user.email,
|
|
1337
|
+
...payload,
|
|
1338
|
+
}));
|
|
1339
|
+
}
|
|
2568
1340
|
|
|
2569
|
-
|
|
2570
|
-
|
|
1341
|
+
if (prefs?.sms !== false && user.phone) {
|
|
1342
|
+
jobs.push(this.notifications.send(ChannelType.SMS, {
|
|
1343
|
+
recipientId: user.phone,
|
|
1344
|
+
body: payload.body,
|
|
1345
|
+
}));
|
|
1346
|
+
}
|
|
2571
1347
|
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
1348
|
+
if (prefs?.push !== false && user.fcmToken) {
|
|
1349
|
+
jobs.push(this.notifications.send(ChannelType.FIREBASE_FCM, {
|
|
1350
|
+
recipientId: user.fcmToken,
|
|
1351
|
+
title: payload.subject,
|
|
1352
|
+
body: payload.body,
|
|
1353
|
+
}));
|
|
1354
|
+
}
|
|
2577
1355
|
|
|
2578
|
-
|
|
2579
|
-
// Log to Slack as an error alert
|
|
1356
|
+
await Promise.all(jobs);
|
|
2580
1357
|
}
|
|
2581
|
-
|
|
2582
|
-
onDeleteSuccess(res: any) {}
|
|
2583
|
-
onDeleteError(err: Error) {}
|
|
2584
1358
|
}
|
|
2585
1359
|
```
|
|
2586
1360
|
|
|
2587
1361
|
---
|
|
2588
1362
|
|
|
2589
|
-
##
|
|
2590
|
-
|
|
2591
|
-
For mission-critical data, you must configure how Redis saves your state to disk.
|
|
2592
|
-
|
|
2593
|
-
### 1. RDB (Redis Database)
|
|
2594
|
-
- **Concept**: A point-in-time snapshot.
|
|
2595
|
-
- **Config**: `save 900 1` (Save every 15 mins if 1 key changed).
|
|
2596
|
-
- **BTS Soft Recommendation**: Use for secondary cache instances where a few minutes of data loss is acceptable.
|
|
2597
|
-
|
|
2598
|
-
### 2. AOF (Append Only File)
|
|
2599
|
-
- **Concept**: Logs every write operation.
|
|
2600
|
-
- **Config**: `appendfsync everysec`.
|
|
2601
|
-
- **BTS Soft Recommendation**: Mandatory for the Redis instance handling **BullMQ Notification Queues**. This ensures that even in a crash, no job is lost.
|
|
2602
|
-
|
|
2603
|
-
### 3. Hybrid (RDB + AOF)
|
|
2604
|
-
- **Concept**: Fast restarts with AOF durability.
|
|
2605
|
-
- **BTS Soft Recommendation**: The "Gold Standard" for production.
|
|
2606
|
-
|
|
2607
|
-
---
|
|
2608
|
-
|
|
2609
|
-
## Technical Appendix: Notification Retry & Backoff Mathematics
|
|
2610
|
-
|
|
2611
|
-
The `NotificationService` uses an exponential backoff formula for retries.
|
|
2612
|
-
|
|
2613
|
-
### The Formula
|
|
2614
|
-
`delay = initialDelay * (multiplier ^ retryCount)`
|
|
2615
|
-
|
|
2616
|
-
### Standard Configuration
|
|
2617
|
-
| Retry Count | Multiplier | Delay (Seconds) |
|
|
2618
|
-
| :--- | :--- | :--- |
|
|
2619
|
-
| 1 | 2 | 1.0 |
|
|
2620
|
-
| 2 | 2 | 2.0 |
|
|
2621
|
-
| 3 | 2 | 4.0 |
|
|
2622
|
-
| 4 | 2 | 8.0 |
|
|
2623
|
-
| 5 | 2 | 16.0 |
|
|
2624
|
-
|
|
2625
|
-
### Why Exponential?
|
|
2626
|
-
Linear retries often cause "Rejection Loops" where the provider (Twilio/WhatsApp) keeps rejecting the same surge of traffic. Exponential backoff allows the provider's systems to breathe and clear bottlenecks before the next attempt.
|
|
2627
|
-
|
|
2628
|
-
---
|
|
2629
|
-
|
|
2630
|
-
## Technical Appendix: The Giant SQL Injection Pattern Wiki (Part 2)---
|
|
2631
|
-
|
|
2632
|
-
## Technical Appendix: Enterprise Support & Custom Development
|
|
2633
|
-
|
|
2634
|
-
If your organization requires specialized support or custom module development within the `@bts-soft` ecosystem, we offer several enterprise-grade engagement models.
|
|
2635
|
-
|
|
2636
|
-
### 1. Dedicated Support SLA
|
|
2637
|
-
- **Response Time**: Same-day response for critical (P0) infrastructure issues.
|
|
2638
|
-
- **Consultancy**: Monthly architectural reviews of your microservices that utilize `@bts-soft/core`.
|
|
2639
|
-
- **Custom Adapters**: Development of specialized notification channels (e.g., custom SMS gateways for specific MENA regions).
|
|
2640
|
-
|
|
2641
|
-
### 2. Security Hardening & Penetration Testing
|
|
2642
|
-
- **Audit**: Biannual security audits of your codebase to ensure all `@bts-soft` decorators are correctly applied.
|
|
2643
|
-
- **Reporting**: Detailed vulnerability reports and remediation strategies.
|
|
2644
|
-
|
|
2645
|
-
---
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
## Epilogue: The BTS Soft Legacy
|
|
2649
|
-
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.
|
|
2650
|
-
|
|
2651
|
-
---
|
|
2652
|
-
|
|
2653
|
-
## Credits & License
|
|
2654
|
-
Created and maintained by **Omar Sabry** for **BTS Soft**.
|
|
2655
|
-
Licensed under the **MIT License**.
|
|
2656
|
-
© 2026 BTS Soft. All Rights Reserved.
|
|
2657
|
-
|
|
2658
|
-
### 3. `@bts-soft/cache`
|
|
2659
|
-
|
|
2660
|
-
#### `RedisService.ts`
|
|
2661
|
-
- **Purpose**: High-performance caching wrapper.
|
|
2662
|
-
- **Logic**: Handles JSON serialization, TTL management, and atomic operations.
|
|
2663
|
-
|
|
2664
|
-
#### `CacheConfigService.ts`
|
|
2665
|
-
- **Purpose**: Configuration provider for Redis connection strings.
|
|
2666
|
-
|
|
2667
|
-
---
|
|
2668
|
-
|
|
2669
|
-
### 4. `@bts-soft/upload`
|
|
2670
|
-
|
|
2671
|
-
#### `UploadService.ts`
|
|
2672
|
-
- **Purpose**: The main API for asset management.
|
|
2673
|
-
- **Logic**: Executes `UploadImageCommand` or `DeleteImageCommand`.
|
|
2674
|
-
|
|
2675
|
-
#### `CloudinaryUploadStrategy.ts`
|
|
2676
|
-
- **Purpose**: Cloudinary-specific implementation.
|
|
2677
|
-
- **Logic**: Stream-pipes files to `cloudinary.uploader.upload_stream`.
|
|
2678
|
-
|
|
2679
|
-
#### `uploadImage.command.ts`
|
|
2680
|
-
- **Purpose**: Command object for uploading images.
|
|
2681
|
-
- **Logic**: Orchestrates the strategy and notifies observers.
|
|
2682
|
-
|
|
2683
|
-
---
|
|
2684
|
-
|
|
2685
|
-
### 5. `@bts-soft/common`
|
|
2686
|
-
|
|
2687
|
-
#### `BaseEntity.ts`
|
|
2688
|
-
- **Purpose**: Global abstract class for all TypeORM entities.
|
|
2689
|
-
- **Logic**: Provides `id` (ULID), `createdAt`, `updatedAt`.
|
|
2690
|
-
|
|
2691
|
-
#### `GeneralResponseInterceptor.ts`
|
|
2692
|
-
- **Purpose**: Standardizes API output envelopes.
|
|
2693
|
-
- **Logic**: Maps success and error states into a uniform JSON structure.
|
|
2694
|
-
|
|
2695
|
-
#### `SqlInjectionInterceptor.ts`
|
|
2696
|
-
- **Purpose**: Global request scrubber.
|
|
2697
|
-
- **Logic**: Recursively walks request objects and executes regex checks.
|
|
2698
|
-
|
|
2699
|
-
---
|
|
2700
|
-
|
|
2701
|
-
## Technical Appendix: Global Event & Microservices Dictionary
|
|
2702
|
-
|
|
2703
|
-
This section provides a complete map of every event emitted and consumed within the BTS Soft ecosystem.
|
|
2704
|
-
|
|
2705
|
-
| Event ID | Emitted By | Payload | Consumer | Description |
|
|
2706
|
-
| :--- | :--- | :--- | :--- | :--- |
|
|
2707
|
-
| uth.user.login | AuthService | { userId, ip } | AuditLogService | Tracks login attempts. |
|
|
2708
|
-
| uth.user.logout | AuthService | { userId } | RedisService | Invalidates session tokens. |
|
|
2709
|
-
| cache.key.set | RedisService | { key, ttl } | MetricsService | Tracks cache fill rate. |
|
|
2710
|
-
| cache.key.hit | RedisService | { key } | MetricsService | Tracks cache hit ratio. |
|
|
2711
|
-
|
|
|
2712
|
-
otif.queue.add | NotifService | { jobId } | BullMQ | Enqueues a notification. |
|
|
2713
|
-
|
|
|
2714
|
-
otif.job.start | WorkerService | { jobId } | LoggerService | Marks start of processing. |
|
|
2715
|
-
|
|
|
2716
|
-
otif.job.done | WorkerService | { jobId } | LoggerService | Success entry. |
|
|
2717
|
-
|
|
|
2718
|
-
otif.job.fail | WorkerService | { jobId, err } | ErrorHub | Triggers retry/alerting. |
|
|
2719
|
-
| upload.local.tmp | UploadService | { path } | FileSystem | Local staging of file. |
|
|
2720
|
-
| upload.cloud.req | CloudService | { provider } | MetricsService | Outbound API call tracking. |
|
|
2721
|
-
| upload.asset.new | UploadService | { url } | DatabaseService | Permanent link storage. |
|
|
2722
|
-
|
|
2723
|
-
---
|
|
2724
|
-
|
|
2725
|
-
## Technical Appendix: SQL Injection Pattern Database (Giant Wiki Edition)
|
|
2726
|
-
|
|
2727
|
-
### Detailed Regex Walkthrough
|
|
2728
|
-
Our regex /(\b(SELECT|INSERT...)/gi is optimized for V8 engine performance.
|
|
2729
|
-
|
|
2730
|
-
| Part | Meaning | Performance Note |
|
|
2731
|
-
| :--- | :--- | :--- |
|
|
2732
|
-
| \b | Word Boundary | Prevents partial matches like 'SELECTIVE'. |
|
|
2733
|
-
| SELECT | DML keyword | Case-insensitive match. |
|
|
2734
|
-
| (--) | SQL Comment | Catches termination attempts. |
|
|
2735
|
-
| \/\* | Multiline Com | Blocks bypass strategies. |
|
|
2736
|
-
|
|
2737
|
-
---
|
|
2738
|
-
|
|
2739
|
-
## Detailed Line-by-Line Code Audit (Part 3)
|
|
2740
|
-
|
|
2741
|
-
### GeneralResponseInterceptor.ts
|
|
2742
|
-
- **Line 1-10**: Setup RxJS map operator.
|
|
2743
|
-
- **Line 11-20**: Extract request context (REST vs GQL).
|
|
2744
|
-
- **Line 21-30**: Structure the SuccessResponse object.
|
|
2745
|
-
- **Line 31-40**: Inject server timestamp.
|
|
2746
|
-
- **Line 41-50**: Execute the handleError fallback.
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
## The 100-Question Technical FAQ Registry (Part 1/2)
|
|
2750
|
-
|
|
2751
|
-
**Q1: How does the RedisService handle JSON serialization?**
|
|
2752
|
-
A: It uses the native JSON.stringify and JSON.parse. Surfaces are typed as T using TypeScript generics to ensure that when you pull data out, it matches the interface you expect. This is handled centrally in the get and set methods to ensure consistency across the entire application ecosystem.
|
|
2753
|
-
|
|
2754
|
-
**Q2: What is the benefit of using ULIDs over UUIDs in @bts-soft/common?**
|
|
2755
|
-
A: ULIDs (Universally Unique Lexicographically Sortable Identifiers) are sortable by creation time. This is a critical advantage for database indexing (especially in TypeORM/PostgreSQL) because it prevents the high index fragmentation caused by random UUIDs, while still providing the same collision resistance.
|
|
2756
|
-
|
|
2757
|
-
**Q3: How does the SQL injection regex detect 'Time-based' attacks?**
|
|
2758
|
-
A: It looks for high-risk SQL functions like SLEEP(), WAITFOR DELAY, and BENCHMARK(). By blocking these strings in the SqlInjectionInterceptor, we prevent attackers from using timing differentials to infer data from the database even when error messages are suppressed.
|
|
2759
|
-
|
|
2760
|
-
**Q4: Can I use the UploadService with local storage instead of Cloudinary?**
|
|
2761
|
-
A: Yes. You must implement the IUploadStrategy interface and create a LocalStorageStrategy. Then, swap the injection in the UploadModule. The UploadService is entirely decoupled from the actual storage implementation thanks to the Strategy Pattern.
|
|
2762
|
-
|
|
2763
|
-
**Q5: What happens if a notification job fails in BullMQ?**
|
|
2764
|
-
A: The job is moved to the 'delayed' state and retried using an exponential backoff strategy (default: 3 retries, starting at 5s). After all retries are exhausted, it is moved to the 'failed' set, where it can be inspected via tools like Bull-Board for manual intervention or debugging.
|
|
2765
|
-
|
|
2766
|
-
**Q6: Is the GeneralResponseInterceptor compatible with GraphQL?**
|
|
2767
|
-
A: Yes. The interceptor detects the request context. For GraphQL, it allows errors to pass through as GraphQLError objects (with correct extensions), while for REST it wraps responses in the success/data/items envelope. It is designed to provide a unified API experience across both protocols.
|
|
2768
|
-
|
|
2769
|
-
**Q7: How do I configure multiple email senders?**
|
|
2770
|
-
A: The EmailChannel uses a single SMTP configuration. To support multiple senders, you can override the rom field in the channelOptions of the
|
|
2771
|
-
otificationService.send call. Note that your SMTP provider (e.g., SendGrid) must allow the secondary email as a verified sender.
|
|
2772
|
-
|
|
2773
|
-
**Q8: Does the WhatsApp channel support document attachments?**
|
|
2774
|
-
A: Yes. Pass the mediaUrl array in the channelOptions. Twilio will then fetch the media from that URL and send it as a WhatsApp message. Supported formats include PDF, DOCX, and common image formats.
|
|
2775
|
-
|
|
2776
|
-
**Q9: How do I implement "Sliding Window" rate limiting?**
|
|
2777
|
-
A: Use the ThrottlerModule from @bts-soft/common. It is pre-configured to use Redis as the storage backend, ensuring that rate limits are enforced across all horizontal instances of your microservice, rather than just per-process.
|
|
2778
|
-
|
|
2779
|
-
**Q10: What is the P99 latency of the RedisService.get method?**
|
|
2780
|
-
A: In our benchmarks on AWS (c5.large instances) with a co-located Redis ElastiCache instance, P99 latency is consistently below 1.2ms for keys under 10KB. This makes it suitable for high-frequency sub-millisecond data retrieval.
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
## Internal Logic Architecture Wiki: Deep Class Registry
|
|
2784
|
-
|
|
2785
|
-
This section provide a line-by-line architectural analysis of the core classes in the @bts-soft/core monorepo.
|
|
2786
|
-
|
|
2787
|
-
---
|
|
2788
|
-
|
|
2789
|
-
### 1. @bts-soft/validation - Core Classes
|
|
2790
|
-
|
|
2791
|
-
#### BaseFieldDecorator (Abstract)
|
|
2792
|
-
- **Lines of Code**: ~50
|
|
2793
|
-
- **Purpose**: Provides a unified interface for all custom field decorators.
|
|
2794
|
-
- **Key Methods**:
|
|
2795
|
-
- pplyValidators(): Composes the array of class-validator decorators.
|
|
2796
|
-
- pplyTransformers(): Composes the array of class-transformer operators.
|
|
2797
|
-
- **Architectural Note**: This class ensures that all decorators maintain a consistent 'security-first' posture by enforcing SQL injection checks at the base level.
|
|
2798
|
-
|
|
2799
|
-
#### EmailFieldDecorator (Implementation)
|
|
2800
|
-
- **Lines of Code**: ~35
|
|
2801
|
-
- **Purpose**: Specialized logic for email address sanitization.
|
|
2802
|
-
- **Key Logic**:
|
|
2803
|
-
- oLower(): Ensures that all emails are stored and compared in lowercase to prevent case-sensitivity bugs.
|
|
2804
|
-
- IsEmail(): Standard RFC compliance check.
|
|
2805
|
-
- **Security Note**: This is part of the 'Critical Data Pathway' and has a 100% test coverage requirement.
|
|
2806
|
-
|
|
2807
|
-
#### PasswordFieldDecorator (Implementation)
|
|
2808
|
-
- **Lines of Code**: ~60
|
|
2809
|
-
- **Purpose**: Enforces the enterprise security policy for passwords.
|
|
2810
|
-
- **Policy Details**:
|
|
2811
|
-
- Minimum 8 characters.
|
|
2812
|
-
- Maximum 64 characters (to prevent Deny-of-Service attacks on bcrypt).
|
|
2813
|
-
- One non-alphanumeric character required.
|
|
2814
|
-
- **Logic Flow**: Rejects common passwords via a lookup table (if enabled) and validates complexity.
|
|
2815
|
-
|
|
2816
|
-
#### NationalIdDecorator (Implementation)
|
|
2817
|
-
- **Lines of Code**: ~40
|
|
2818
|
-
- **Domain**: Egyptian National Identity.
|
|
2819
|
-
- **Logic**:
|
|
2820
|
-
- Digit 1: Century (2 for 1900-1999, 3 for 2000-2099).
|
|
2821
|
-
- Digits 2-7: Date of Birth.
|
|
2822
|
-
- Digits 8-13: Serial number.
|
|
2823
|
-
- Digit 14: Check digit.
|
|
2824
|
-
- **Implementation**: Uses a complex regex and a transform function to strip non-numeric characters.
|
|
2825
|
-
|
|
2826
|
-
---
|
|
2827
|
-
|
|
2828
|
-
### 2. @bts-soft/notifications - Core Classes
|
|
2829
|
-
|
|
2830
|
-
#### NotificationChannelFactory
|
|
2831
|
-
- **Lines of Code**: ~110
|
|
2832
|
-
- **Purpose**: Central hub for creating notification channel instances.
|
|
2833
|
-
- **Technical Logic**:
|
|
2834
|
-
- Uses the ChannelType enum to determine the target strategy.
|
|
2835
|
-
- Injects ConfigService to retrieve API keys securely.
|
|
2836
|
-
- Returns a singleton instance for channels that are thread-safe (like Email).
|
|
2837
|
-
|
|
2838
|
-
#### EmailChannel
|
|
2839
|
-
- **Lines of Code**: ~150
|
|
2840
|
-
- **Purpose**: Nodemailer bridge.
|
|
2841
|
-
- **Implementation Details**:
|
|
2842
|
-
- Supports multiple transport protocols (SMTP, AWS SES, Mailgun).
|
|
2843
|
-
- Handles HTML and Plain Text versions of messages.
|
|
2844
|
-
- Provides detailed error logging via the Logger service.
|
|
1363
|
+
## Security Model
|
|
2845
1364
|
|
|
2846
|
-
|
|
2847
|
-
- **Lines of Code**: ~200
|
|
2848
|
-
- **Purpose**: High-performance consumer for the notification queue.
|
|
2849
|
-
- **Logic Flow**:
|
|
2850
|
-
1. Pulls job from Redis.
|
|
2851
|
-
2. Identifies the correct channel.
|
|
2852
|
-
3. Executes the send() method.
|
|
2853
|
-
4. Updates the job state (Done/Failed).
|
|
2854
|
-
- **Metric Tracking**: Reports job processing time to our internal Prometheus dashboard.
|
|
1365
|
+
### Defense in Depth
|
|
2855
1366
|
|
|
1367
|
+
```
|
|
1368
|
+
Internet Traffic
|
|
1369
|
+
│
|
|
1370
|
+
▼
|
|
1371
|
+
ThrottlingModule (rate limiting — DDoS / brute force protection)
|
|
1372
|
+
│
|
|
1373
|
+
▼
|
|
1374
|
+
SqlInjectionInterceptor (recursive payload scanning — SQLi / stacked queries)
|
|
1375
|
+
│
|
|
1376
|
+
▼
|
|
1377
|
+
ValidationPipe (DTO constraints + auto-transformation)
|
|
1378
|
+
│
|
|
1379
|
+
▼
|
|
1380
|
+
DistributedLockService (critical section guard — race condition prevention)
|
|
1381
|
+
│
|
|
1382
|
+
▼
|
|
1383
|
+
Business Logic
|
|
1384
|
+
│
|
|
1385
|
+
▼
|
|
1386
|
+
ClassSerializerInterceptor (strip @Exclude() fields — prevent data leakage)
|
|
1387
|
+
│
|
|
1388
|
+
▼
|
|
1389
|
+
Client Response
|
|
1390
|
+
```
|
|
2856
1391
|
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
This section provides an exhaustive walk-through of the critical source code within @bts-soft/core.
|
|
2860
|
-
|
|
2861
|
-
---
|
|
2862
|
-
|
|
2863
|
-
### 1. RedisService.ts Audit
|
|
2864
|
-
|
|
2865
|
-
| Line | Code | Technical Analysis |
|
|
1392
|
+
| Layer | Mechanism | Protection Against |
|
|
2866
1393
|
| :--- | :--- | :--- |
|
|
2867
|
-
|
|
|
2868
|
-
|
|
|
2869
|
-
|
|
|
2870
|
-
|
|
|
2871
|
-
|
|
|
2872
|
-
|
|
|
2873
|
-
| 28 | try { return JSON.parse(data) as T; } | Attempt deserialization of potential objects. |
|
|
2874
|
-
| 29 | catch { return data as unknown as T; } | Fallback to raw string if JSON parsing fails. |
|
|
2875
|
-
| 30 | } | Method termination. |
|
|
2876
|
-
|
|
2877
|
-
---
|
|
1394
|
+
| Network | `ThrottlingModule` | DDoS, brute force, noisy neighbor attacks |
|
|
1395
|
+
| Payload | `SqlInjectionInterceptor` | SQL injection, stacked queries, time-delay attacks, xp_cmdshell |
|
|
1396
|
+
| Input | `ValidationPipe` with composite decorators | Type coercion attacks, invalid data formats |
|
|
1397
|
+
| Concurrency | `DistributedLockService` | Race conditions across service replicas |
|
|
1398
|
+
| Output | `ClassSerializerInterceptor` | Sensitive field exposure (passwords, internal IDs) |
|
|
1399
|
+
| Files | `FileValidatorService` | Extension whitelist bypass, disk exhaustion via oversized uploads |
|
|
2878
1400
|
|
|
2879
|
-
###
|
|
1401
|
+
### Production Console Silencing
|
|
2880
1402
|
|
|
2881
|
-
|
|
2882
|
-
| :--- | :--- | :--- |
|
|
2883
|
-
| 45 | public async send(type: ChannelType, msg: IMessage): Promise<void> { | Main entry point for the Notification dispatcher. |
|
|
2884
|
-
| 46 | const start = Date.now(); | Performance tracking for the BullMQ payload. |
|
|
2885
|
-
| 47 | await this.notificationQueue.add('sendMail', { type, msg }); | Atomically enqueues the job into Redis. |
|
|
2886
|
-
| 48 | this.logger.log(Job queued in ms); | Log the overhead of the queueing operation (p99 < 5ms). |
|
|
2887
|
-
| 49 | } | End of method. |
|
|
1403
|
+
When `NODE_ENV=production`, all `console.*` methods are automatically no-ops after `setupInterceptors(app)` is called. This prevents accidental logging of tokens, passwords, or PII in production. Use NestJS `Logger` for structured production logging instead.
|
|
2888
1404
|
|
|
2889
1405
|
---
|
|
2890
1406
|
|
|
2891
|
-
|
|
1407
|
+
## Environment Variable Reference
|
|
2892
1408
|
|
|
2893
|
-
|
|
2894
|
-
| :--- | :--- | :--- |
|
|
2895
|
-
| 15 | intercept(context: ExecutionContext, next: CallHandler): Observable<any> { | Implement the NestJS Interceptor interface. |
|
|
2896
|
-
| 16 | const request = context.switchToHttp().getRequest(); | Extract the HTTP request object. |
|
|
2897
|
-
| 17 | return next.handle().pipe( | Start the RxJS processing pipeline. |
|
|
2898
|
-
| 18 | map((data) => ({ | Map raw data into the standard BTS Soft envelope. |
|
|
2899
|
-
| 19 | success: true, | Set global success flag. |
|
|
2900
|
-
| 20 | statusCode: context.switchToHttp().getResponse().statusCode, | Sync status code with the response object. |
|
|
2901
|
-
| 21 | data, | Place payload in the data field. |
|
|
2902
|
-
| 22 | timeStamp: new Date().toISOString(), | Add server-side ISO timestamp. |
|
|
2903
|
-
| 23 | })), | Close the map function. |
|
|
2904
|
-
| 24 | ); | Close the pipeline. |
|
|
1409
|
+
### Redis
|
|
2905
1410
|
|
|
2906
|
-
|
|
1411
|
+
| Variable | Default | Required | Description |
|
|
1412
|
+
| :--- | :--- | :--- | :--- |
|
|
1413
|
+
| `REDIS_HOST` | `localhost` | No | Redis hostname |
|
|
1414
|
+
| `REDIS_PORT` | `6379` | No | Redis port |
|
|
1415
|
+
| `REDIS_PASSWORD` | — | No | Authentication password |
|
|
1416
|
+
| `REDIS_DB` | `0` | No | Database index (0-15) |
|
|
1417
|
+
| `REDIS_TTL` | `3600` | No | Default cache TTL in seconds |
|
|
2907
1418
|
|
|
2908
|
-
###
|
|
1419
|
+
### Notifications
|
|
2909
1420
|
|
|
2910
|
-
|
|
|
1421
|
+
| Variable | Description |
|
|
1422
|
+
| :--- | :--- |
|
|
1423
|
+
| `EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_USER`, `EMAIL_PASS`, `EMAIL_SENDER` | SMTP configuration |
|
|
1424
|
+
| `EMAIL_PROVIDER` | `nodemailer`, `twilio-mail`, or `ses` |
|
|
1425
|
+
| `SENDGRID_API_KEY` | SendGrid API key |
|
|
1426
|
+
| `AWS_SES_ACCESS_KEY_ID`, `AWS_SES_SECRET_ACCESS_KEY`, `AWS_SES_REGION` | Amazon SES credentials |
|
|
1427
|
+
| `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_SMS_NUMBER`, `TWILIO_WHATSAPP_NUMBER` | Twilio credentials |
|
|
1428
|
+
| `SMS_PROVIDER` | `twilio`, `smsmisr`, or `vonage` |
|
|
1429
|
+
| `SMSMISR_USERNAME`, `SMSMISR_PASSWORD`, `SMSMISR_SENDER` | SMS Misr credentials |
|
|
1430
|
+
| `VONAGE_API_KEY`, `VONAGE_API_SECRET`, `VONAGE_SENDER` | Vonage credentials |
|
|
1431
|
+
| `TELEGRAM_BOT_TOKEN` | Telegram Bot API token |
|
|
1432
|
+
| `DISCORD_WEBHOOK_URL` | Discord default webhook URL |
|
|
1433
|
+
| `TEAMS_WEBHOOK_URL` | Microsoft Teams webhook URL |
|
|
1434
|
+
| `SLACK_WEBHOOK_URL`, `SLACK_BOT_TOKEN`, `SLACK_DEFAULT_CHANNEL` | Slack integration |
|
|
1435
|
+
| `FB_PAGE_ACCESS_TOKEN`, `FB_GRAPH_API_VERSION` | Facebook Messenger (default: `v18.0`) |
|
|
1436
|
+
| `FIREBASE_SERVICE_ACCOUNT_PATH` | Path to Firebase Admin SDK JSON file |
|
|
1437
|
+
| `ONESIGNAL_APP_ID`, `ONESIGNAL_REST_API_KEY` | OneSignal credentials |
|
|
1438
|
+
| `WEB_PUSH_PUBLIC_KEY`, `WEB_PUSH_PRIVATE_KEY`, `WEB_PUSH_SUBJECT` | VAPID keys for Web Push |
|
|
1439
|
+
| `IN_APP_PROVIDER` | `pusher` |
|
|
1440
|
+
| `PUSHER_APP_ID`, `PUSHER_KEY`, `PUSHER_SECRET`, `PUSHER_CLUSTER` | Pusher Channels credentials |
|
|
1441
|
+
| `WEBHOOK_DEFAULT_SIGNING_SECRET` | Webhook payload signing secret |
|
|
1442
|
+
|
|
1443
|
+
### Upload
|
|
1444
|
+
|
|
1445
|
+
| Variable | Default | Description |
|
|
2911
1446
|
| :--- | :--- | :--- |
|
|
2912
|
-
|
|
|
2913
|
-
|
|
|
2914
|
-
|
|
|
2915
|
-
|
|
|
2916
|
-
|
|
|
1447
|
+
| `UPLOAD_PROVIDER` | `local` | `local` or `cloudinary` |
|
|
1448
|
+
| `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, `CLOUDINARY_API_SECRET` | — | Cloudinary credentials |
|
|
1449
|
+
| `UPLOAD_LOCAL_PATH` | `./uploads` | Local storage directory path |
|
|
1450
|
+
| `UPLOAD_MAX_IMAGE_SIZE` | `5242880` | Max image size in bytes (5 MB) |
|
|
1451
|
+
| `UPLOAD_MAX_VIDEO_SIZE` | `104857600` | Max video size in bytes (100 MB) |
|
|
1452
|
+
| `UPLOAD_MAX_AUDIO_SIZE` | `52428800` | Max audio size in bytes (50 MB) |
|
|
1453
|
+
| `UPLOAD_MAX_FILE_SIZE` | `10485760` | Max raw file size in bytes (10 MB) |
|
|
1454
|
+
| `UPLOAD_MAX_MODEL_3D_SIZE` | `104857600` | Max 3D model size in bytes (100 MB) |
|
|
1455
|
+
| `UPLOAD_RATE_LIMIT_CAPACITY` | `10` | Token bucket capacity for chunked uploads |
|
|
1456
|
+
| `UPLOAD_RATE_LIMIT_REFILL_RATE` | `2` | Tokens refilled per second |
|
|
2917
1457
|
|
|
2918
1458
|
---
|
|
2919
1459
|
|
|
1460
|
+
## Testing and Verification
|
|
2920
1461
|
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
### 1. RedisService.ts Deep Walkthrough
|
|
1462
|
+
Each sub-package ships with its own unit and end-to-end test suites. Unit tests use mocked dependencies; E2E tests target real infrastructure via Docker Compose.
|
|
2924
1463
|
|
|
2925
|
-
|
|
|
1464
|
+
| Package | Unit Tests | E2E Tests | Infrastructure Required |
|
|
2926
1465
|
| :--- | :--- | :--- | :--- |
|
|
2927
|
-
|
|
|
2928
|
-
|
|
|
2929
|
-
|
|
|
2930
|
-
|
|
|
2931
|
-
|
|
|
2932
|
-
| 6 | Prop | logger | Standard NestJS logging hub. |
|
|
2933
|
-
| 7 | Constructor | Init Logic | Dependency injection of cache manager. |
|
|
2934
|
-
| 8 | Init | Log | Prints 'Redis Service Initialized' on boot. |
|
|
2935
|
-
| ... | ... | ... | ... |
|
|
2936
|
-
| 100 | Get | Logic | Reads key from Redis memory. |
|
|
2937
|
-
| 101 | Get | If | Checks if key exists. |
|
|
2938
|
-
| 102 | Get | Parse | Converts string to JSON object. |
|
|
2939
|
-
| 103 | Get | Catch | Error handling for malformed JSON. |
|
|
2940
|
-
| 104 | Get | Return | Returns T or null. |
|
|
1466
|
+
| `@bts-soft/validation` | Yes | Yes | None (mocked NestJS application) |
|
|
1467
|
+
| `@bts-soft/cache` | Yes (ioredis-mock) | Yes | Redis server on port 6380 |
|
|
1468
|
+
| `@bts-soft/notifications` | Yes | Yes | Redis via Docker Compose |
|
|
1469
|
+
| `@bts-soft/upload` | Yes | Yes | None (filesystem mocks) |
|
|
1470
|
+
| `@bts-soft/common` | Yes | Yes | PostgreSQL and Redis via Docker Compose |
|
|
2941
1471
|
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
### 2. NotificationService.ts Deep Walkthrough
|
|
2945
|
-
|
|
2946
|
-
| Line | Component | Purpose | Technical Summary |
|
|
2947
|
-
| :--- | :--- | :--- | :--- |
|
|
2948
|
-
| 1 | Header | Licensing | BTS Soft proprietary code. |
|
|
2949
|
-
| 2 | Imports | Queue & Strategy | BullMQ, Factory, Internal Interfaces. |
|
|
2950
|
-
| 3 | Class | NotificationService | The main dispatcher class. |
|
|
2951
|
-
| 4 | Constructor | Setup | Injecting the BullMQ 'notifications' queue. |
|
|
2952
|
-
| 5 | Method | send() | High-level API for all messaging. |
|
|
2953
|
-
| 6 | Method | sendEmail() | Proxies to EmailChannel provider. |
|
|
2954
|
-
| 7 | Method | sendSms() | Proxies to Twilio provider. |
|
|
2955
|
-
| 8 | Method | sendWhatsapp() | Proxies to Twilio WA provider. |
|
|
2956
|
-
| 9 | Method | sendFirebase() | Proxies to FCM provider. |
|
|
2957
|
-
| 10 | Method | sendTelegram() | Proxies to Bot API provider. |
|
|
2958
|
-
| 11 | Method | sendDiscord() | Proxies to Webhook provider. |
|
|
2959
|
-
| 12 | Method | sendTeams() | Proxies to Webhook provider. |
|
|
2960
|
-
| 13 | Method | sendMessenger() | Proxies to Graph API provider. |
|
|
1472
|
+
Running tests in each package directory:
|
|
2961
1473
|
|
|
2962
|
-
|
|
1474
|
+
```bash
|
|
1475
|
+
# Unit tests
|
|
1476
|
+
npm run test
|
|
2963
1477
|
|
|
2964
|
-
|
|
1478
|
+
# Coverage report
|
|
1479
|
+
npm run test:cov
|
|
2965
1480
|
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
| 41 | Map | Structure | Defines the shared JSON envelope. |
|
|
2970
|
-
| 42 | Key | Success | Hardcoded boolean true on success. |
|
|
2971
|
-
| 43 | Key | StatusCode | Mirroring the HTTP status from context. |
|
|
2972
|
-
| 44 | Key | Message | Defaults to 'Request successful'. |
|
|
2973
|
-
| 45 | Key | Data | The payload from the controller. |
|
|
2974
|
-
| 46 | Key | Items | Optional list field for arrays. |
|
|
2975
|
-
| 47 | Key | Pagination | Metadata for lists (total, page, limit). |
|
|
2976
|
-
| 48 | Key | TimeStamp | ISO string from current system clock. |
|
|
1481
|
+
# End-to-end integration tests
|
|
1482
|
+
npm run test:e2e
|
|
1483
|
+
```
|
|
2977
1484
|
|
|
2978
1485
|
---
|
|
2979
1486
|
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
| Line | Component | Purpose | Technical Summary |
|
|
2983
|
-
| :--- | :--- | :--- | :--- |
|
|
2984
|
-
| 1 | Header | Licensing | BTS Soft 2026. |
|
|
2985
|
-
| 2 | Imports | Command Pattern | IUploadCommand, IUploadStrategy. |
|
|
2986
|
-
| 3 | Class | UploadService | Facade for asset management. |
|
|
2987
|
-
| 4 | Method | upload() | Executes the 'Upload' logic flow. |
|
|
2988
|
-
| 5 | Method | delete() | Executes the 'Delete' logic flow. |
|
|
2989
|
-
| 6 | Hook | onSuccess | Reactive callback for completion. |
|
|
2990
|
-
| 7 | Hook | onError | Reactive callback for failures. |
|
|
1487
|
+
## FAQ
|
|
2991
1488
|
|
|
1489
|
+
**Can I install individual sub-packages instead of @bts-soft/core?**
|
|
1490
|
+
Yes. Each sub-package is independently published on npm and can be installed in isolation. `@bts-soft/core` is a convenience wrapper for when you need the Back-End.
|
|
2992
1491
|
|
|
2993
|
-
|
|
1492
|
+
**Does @bts-soft/core support NestJS 10?**
|
|
1493
|
+
No. Version 3.x requires NestJS 11 or later due to peer dependency alignment across all sub-packages.
|
|
2994
1494
|
|
|
2995
|
-
|
|
1495
|
+
**How do I switch from Cloudinary to local storage?**
|
|
1496
|
+
Set `UPLOAD_PROVIDER=local` in your environment. No code changes are required.
|
|
2996
1497
|
|
|
2997
|
-
|
|
1498
|
+
**What happens if Redis is unavailable?**
|
|
1499
|
+
The rate limiter in `@bts-soft/validation` automatically falls back to in-memory storage with a warning log. The `RedisService` in `@bts-soft/cache` will throw on all operation calls; wrap cache calls in `try/catch` and implement a circuit breaker if the application must remain functional without the cache layer.
|
|
2998
1500
|
|
|
2999
|
-
|
|
3000
|
-
-
|
|
3001
|
-
- **Replica Nodes**: UAE (Azure), Saudi (Google Cloud)
|
|
3002
|
-
- **Tuning Strategy**:
|
|
3003
|
-
- cp_keepalive: 30 seconds.
|
|
3004
|
-
- client_timeout: 500ms.
|
|
3005
|
-
- **Reasoning**: Intra-GCC fiber is fast, but cross-cloud peering can be jittery.
|
|
1501
|
+
**Can I use @bts-soft/common's ORM adapters without a specific ORM installed?**
|
|
1502
|
+
Yes. The ORM adapters are exposed through sub-path exports such as `@bts-soft/common/typeorm`, `/mongoose`, `/sequelize`, and `/prisma`. The main entry point does not require any specific ORM at runtime. Only the sub-path you explicitly import is loaded.
|
|
3006
1503
|
|
|
3007
|
-
|
|
3008
|
-
-
|
|
3009
|
-
- **Gateway**: SendGrid (Relay via London)
|
|
3010
|
-
- **Tuning Strategy**:
|
|
3011
|
-
- dns_lookup: Prefer IPv4 (Avoid IPv6 propagation lag).
|
|
3012
|
-
- smtp_timeout: 10,000ms.
|
|
3013
|
-
- **Reasoning**: Residential ISPs in Northern Africa often have legacy DNS resolvers.
|
|
1504
|
+
**How does the notification rate limiter interact with the validation rate limiter?**
|
|
1505
|
+
They are independent systems. The notification rate limiter operates inside the notification pre-flight pipeline using Redis sorted sets with a sliding window per recipient per channel. The validation `@RateLimit` decorator operates at the NestJS guard level on incoming HTTP or GraphQL requests.
|
|
3014
1506
|
|
|
3015
|
-
|
|
1507
|
+
**Is there a way to extend the notification system with a new channel?**
|
|
1508
|
+
Yes. Implement the channel interface and register it with the channel factory. The pre-flight pipeline and BullMQ processor automatically route to your channel based on the `ChannelType` value.
|
|
3016
1509
|
|
|
3017
1510
|
---
|
|
3018
1511
|
|
|
3019
|
-
## Support
|
|
3020
|
-
|
|
3021
|
-
### Documentation
|
|
3022
|
-
- Complete API documentation available for each package
|
|
3023
|
-
- Examples and tutorials in GitHub repositories
|
|
3024
|
-
- TypeScript definitions for all exports
|
|
3025
|
-
|
|
3026
|
-
### Community
|
|
3027
|
-
- GitHub Issues for bug reports and feature requests
|
|
3028
|
-
- Regular updates and maintenance
|
|
3029
|
-
- Active development and support
|
|
3030
|
-
|
|
3031
|
-
### Enterprise
|
|
3032
|
-
- Dedicated support available
|
|
3033
|
-
- Custom implementations and consulting
|
|
3034
|
-
- Training and integration services
|
|
3035
|
-
|
|
3036
1512
|
## License
|
|
3037
1513
|
|
|
3038
|
-
|
|
1514
|
+
This project is licensed under the MIT License. See the [license](./license) file for details.
|
|
3039
1515
|
|
|
3040
|
-
|
|
1516
|
+
---
|
|
3041
1517
|
|
|
3042
|
-
|
|
3043
|
-
**Email:** [omar.sabry.dev@gmail.com](mailto:omar.sabry.dev@gmail.com)
|
|
3044
|
-
**LinkedIn:** [Omar Sabry](https://www.linkedin.com/in/omarsa6ry/)
|
|
3045
|
-
**Portfolio:** [https://omarsabry.netlify.app/](https://omarsabry.netlify.app/)
|
|
1518
|
+
## Author
|
|
3046
1519
|
|
|
3047
|
-
|
|
1520
|
+
**Omar Sabry**
|
|
3048
1521
|
|
|
3049
|
-
|
|
1522
|
+
- Portfolio: [omarsabry.vercel.app](https://omarsabry.vercel.app/)
|
|
1523
|
+
- GitHub: [Omar-Sa6ry](https://github.com/Omar-Sa6ry)
|
|
1524
|
+
- npm: [@bts-soft](https://www.npmjs.com/~bts-soft)
|