servcraft 0.1.0 → 0.1.3
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/.claude/settings.local.json +30 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/LICENSE +21 -0
- package/README.md +1102 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# Rate Limit Module
|
|
2
|
+
|
|
3
|
+
Flexible rate limiting with multiple algorithms and storage backends.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multiple Algorithms** - Fixed window, sliding window, token bucket
|
|
8
|
+
- **Multiple Stores** - Memory (default), Redis (distributed)
|
|
9
|
+
- **Whitelist/Blacklist** - IP-based access control
|
|
10
|
+
- **Custom Key Generation** - Rate limit by IP, user ID, API key, etc.
|
|
11
|
+
- **Standard Headers** - X-RateLimit-* headers support
|
|
12
|
+
- **Custom Limits** - Per-endpoint or per-user limits
|
|
13
|
+
|
|
14
|
+
## Algorithms
|
|
15
|
+
|
|
16
|
+
### Fixed Window
|
|
17
|
+
|
|
18
|
+
Simple counter that resets at fixed intervals. Can allow bursts at window boundaries.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Window 1 Window 2
|
|
22
|
+
[====|====] [====|====]
|
|
23
|
+
^burst allowed here
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Sliding Window
|
|
27
|
+
|
|
28
|
+
Weighted count based on position within the window. Prevents boundary bursts.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
◄─────── window ───────►
|
|
32
|
+
Past │ ████░░░░ │ ████████ │ Future
|
|
33
|
+
│ 30% │ 70% │
|
|
34
|
+
Weighted count = (old * 0.3) + new
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Token Bucket
|
|
38
|
+
|
|
39
|
+
Tokens refill over time, allowing small bursts but enforcing average rate.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Bucket (max 100 tokens)
|
|
43
|
+
┌──────────────────────────┐
|
|
44
|
+
│ ████████████░░░░░░░░░░░░ │ 60 tokens available
|
|
45
|
+
└──────────────────────────┘
|
|
46
|
+
Refill: 10 tokens/second
|
|
47
|
+
Request consumes 1 token
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Basic Setup
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { RateLimitService } from 'servcraft/modules/rate-limit';
|
|
56
|
+
import { RedisStore } from 'servcraft/modules/rate-limit/stores/redis.store';
|
|
57
|
+
|
|
58
|
+
// Simple rate limiter (100 requests per minute)
|
|
59
|
+
const rateLimiter = new RateLimitService({
|
|
60
|
+
max: 100,
|
|
61
|
+
windowMs: 60 * 1000,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Check if request is allowed
|
|
65
|
+
const result = await rateLimiter.check(clientIp);
|
|
66
|
+
if (!result.allowed) {
|
|
67
|
+
// Return 429 Too Many Requests
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### With Redis Store (Distributed)
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { RedisStore } from 'servcraft/modules/rate-limit/stores/redis.store';
|
|
75
|
+
|
|
76
|
+
const redisStore = new RedisStore({
|
|
77
|
+
prefix: 'ratelimit:api:',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const rateLimiter = new RateLimitService({
|
|
81
|
+
max: 100,
|
|
82
|
+
windowMs: 60 * 1000,
|
|
83
|
+
algorithm: 'sliding-window',
|
|
84
|
+
}, redisStore);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Algorithm Selection
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// Fixed window - simplest, good for non-critical APIs
|
|
91
|
+
const fixed = new RateLimitService({
|
|
92
|
+
max: 100,
|
|
93
|
+
windowMs: 60000,
|
|
94
|
+
algorithm: 'fixed-window',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Sliding window - recommended for most use cases
|
|
98
|
+
const sliding = new RateLimitService({
|
|
99
|
+
max: 100,
|
|
100
|
+
windowMs: 60000,
|
|
101
|
+
algorithm: 'sliding-window',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Token bucket - best for APIs that need to allow bursts
|
|
105
|
+
const tokenBucket = new RateLimitService({
|
|
106
|
+
max: 100,
|
|
107
|
+
windowMs: 60000,
|
|
108
|
+
algorithm: 'token-bucket',
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Custom Key Generation
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Rate limit by user ID
|
|
116
|
+
const userLimiter = new RateLimitService({
|
|
117
|
+
max: 1000,
|
|
118
|
+
windowMs: 60000,
|
|
119
|
+
keyGenerator: (req) => req.user?.id || req.ip,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Rate limit by API key
|
|
123
|
+
const apiLimiter = new RateLimitService({
|
|
124
|
+
max: 5000,
|
|
125
|
+
windowMs: 60000,
|
|
126
|
+
keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Rate limit by endpoint + IP
|
|
130
|
+
const endpointLimiter = new RateLimitService({
|
|
131
|
+
max: 10,
|
|
132
|
+
windowMs: 60000,
|
|
133
|
+
keyGenerator: (req) => `${req.method}:${req.url}:${req.ip}`,
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Whitelist and Blacklist
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const limiter = new RateLimitService({
|
|
141
|
+
max: 100,
|
|
142
|
+
windowMs: 60000,
|
|
143
|
+
whitelist: ['127.0.0.1', '10.0.0.1'], // Never rate limited
|
|
144
|
+
blacklist: ['192.168.1.100'], // Always blocked
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Dynamic whitelist/blacklist
|
|
148
|
+
limiter.addToWhitelist('10.0.0.2');
|
|
149
|
+
limiter.removeFromWhitelist('10.0.0.1');
|
|
150
|
+
limiter.addToBlacklist('suspicious-ip');
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Custom Limits per Endpoint
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Different limits for different endpoints
|
|
157
|
+
const result = await limiter.check(clientIp, {
|
|
158
|
+
max: 10, // Override default max
|
|
159
|
+
windowMs: 1000, // Override default window
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Getting Rate Limit Info
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const info = await limiter.getInfo(clientIp);
|
|
167
|
+
// {
|
|
168
|
+
// limit: 100,
|
|
169
|
+
// remaining: 45,
|
|
170
|
+
// resetAt: 1703001234567,
|
|
171
|
+
// count: 55,
|
|
172
|
+
// firstRequest: 1703001200000,
|
|
173
|
+
// lastRequest: 1703001230000
|
|
174
|
+
// }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Reset Rate Limit
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Reset for specific key
|
|
181
|
+
await limiter.reset(clientIp);
|
|
182
|
+
|
|
183
|
+
// Clear all rate limit data
|
|
184
|
+
await limiter.clear();
|
|
185
|
+
|
|
186
|
+
// Cleanup expired entries (memory store)
|
|
187
|
+
await limiter.cleanup();
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Configuration
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
interface RateLimitConfig {
|
|
194
|
+
max: number; // Maximum requests per window
|
|
195
|
+
windowMs: number; // Window size in milliseconds
|
|
196
|
+
algorithm?: Algorithm; // 'fixed-window' | 'sliding-window' | 'token-bucket'
|
|
197
|
+
whitelist?: string[]; // IPs that bypass rate limiting
|
|
198
|
+
blacklist?: string[]; // IPs that are always blocked
|
|
199
|
+
keyGenerator?: (req) => string; // Custom key generation
|
|
200
|
+
skip?: (req) => boolean; // Skip rate limiting for some requests
|
|
201
|
+
onLimitReached?: (req) => void; // Callback when limit reached
|
|
202
|
+
headers?: boolean; // Include X-RateLimit headers (default: true)
|
|
203
|
+
message?: string; // Error message (default: 'Too many requests')
|
|
204
|
+
statusCode?: number; // HTTP status code (default: 429)
|
|
205
|
+
customLimits?: Record<string, { max: number; windowMs: number }>;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Response Types
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
interface RateLimitResult {
|
|
213
|
+
allowed: boolean; // Whether request is allowed
|
|
214
|
+
limit: number; // Maximum requests per window
|
|
215
|
+
remaining: number; // Remaining requests in window
|
|
216
|
+
resetAt: number; // Timestamp when window resets
|
|
217
|
+
retryAfter?: number; // Seconds until retry (when blocked)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface RateLimitInfo {
|
|
221
|
+
limit: number;
|
|
222
|
+
remaining: number;
|
|
223
|
+
resetAt?: number;
|
|
224
|
+
count: number;
|
|
225
|
+
firstRequest?: number;
|
|
226
|
+
lastRequest?: number;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## HTTP Headers
|
|
231
|
+
|
|
232
|
+
When `headers: true` (default), include these headers in responses:
|
|
233
|
+
|
|
234
|
+
```http
|
|
235
|
+
X-RateLimit-Limit: 100
|
|
236
|
+
X-RateLimit-Remaining: 45
|
|
237
|
+
X-RateLimit-Reset: 1703001234
|
|
238
|
+
Retry-After: 30 (only when blocked)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Fastify Middleware Example
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { FastifyRequest, FastifyReply } from 'fastify';
|
|
245
|
+
|
|
246
|
+
const rateLimiter = new RateLimitService({
|
|
247
|
+
max: 100,
|
|
248
|
+
windowMs: 60000,
|
|
249
|
+
algorithm: 'sliding-window',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
async function rateLimitMiddleware(
|
|
253
|
+
request: FastifyRequest,
|
|
254
|
+
reply: FastifyReply
|
|
255
|
+
) {
|
|
256
|
+
const key = request.ip;
|
|
257
|
+
const result = await rateLimiter.check(key);
|
|
258
|
+
|
|
259
|
+
// Set headers
|
|
260
|
+
reply.header('X-RateLimit-Limit', result.limit);
|
|
261
|
+
reply.header('X-RateLimit-Remaining', result.remaining);
|
|
262
|
+
reply.header('X-RateLimit-Reset', Math.ceil(result.resetAt / 1000));
|
|
263
|
+
|
|
264
|
+
if (!result.allowed) {
|
|
265
|
+
reply.header('Retry-After', result.retryAfter);
|
|
266
|
+
return reply.status(429).send({
|
|
267
|
+
error: 'Too Many Requests',
|
|
268
|
+
retryAfter: result.retryAfter,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Register as preHandler
|
|
274
|
+
fastify.addHook('preHandler', rateLimitMiddleware);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Pre-configured Limiters
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// Strict limiter for sensitive endpoints (login, password reset)
|
|
281
|
+
const strictLimiter = new RateLimitService({
|
|
282
|
+
max: 5,
|
|
283
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
284
|
+
algorithm: 'sliding-window',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Standard API limiter
|
|
288
|
+
const standardLimiter = new RateLimitService({
|
|
289
|
+
max: 100,
|
|
290
|
+
windowMs: 60 * 1000,
|
|
291
|
+
algorithm: 'sliding-window',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Relaxed limiter for read-only endpoints
|
|
295
|
+
const relaxedLimiter = new RateLimitService({
|
|
296
|
+
max: 1000,
|
|
297
|
+
windowMs: 60 * 1000,
|
|
298
|
+
algorithm: 'token-bucket',
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Auth endpoints (very strict)
|
|
302
|
+
const authLimiter = new RateLimitService({
|
|
303
|
+
max: 10,
|
|
304
|
+
windowMs: 60 * 60 * 1000, // 1 hour
|
|
305
|
+
algorithm: 'fixed-window',
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Redis Store
|
|
310
|
+
|
|
311
|
+
The Redis store enables distributed rate limiting across multiple server instances.
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { RedisStore } from 'servcraft/modules/rate-limit/stores/redis.store';
|
|
315
|
+
|
|
316
|
+
const redisStore = new RedisStore({
|
|
317
|
+
prefix: 'ratelimit:', // Key prefix
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Uses atomic Lua scripts for accuracy
|
|
321
|
+
// Supports sliding-window and token-bucket with Redis
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Redis Key Structure
|
|
325
|
+
|
|
326
|
+
| Key Pattern | Purpose |
|
|
327
|
+
|-------------|---------|
|
|
328
|
+
| `ratelimit:{key}` | Fixed window counter |
|
|
329
|
+
| `ratelimit:sw:{key}` | Sliding window data |
|
|
330
|
+
| `ratelimit:tb:{key}` | Token bucket data |
|
|
331
|
+
|
|
332
|
+
## Best Practices
|
|
333
|
+
|
|
334
|
+
1. **Use sliding-window for APIs** - Prevents burst attacks at boundaries
|
|
335
|
+
2. **Use token-bucket for webhooks** - Allows legitimate burst traffic
|
|
336
|
+
3. **Use Redis for distributed systems** - Consistent limits across instances
|
|
337
|
+
4. **Different limits for different users** - Premium users get higher limits
|
|
338
|
+
5. **Log rate limit events** - Track abuse patterns
|
|
339
|
+
6. **Whitelist internal services** - Don't rate limit service-to-service calls
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Search Module
|
|
2
|
+
|
|
3
|
+
Full-text search with support for multiple backends (Elasticsearch, Meilisearch, in-memory).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multiple Backends** - Elasticsearch, Meilisearch, or in-memory
|
|
8
|
+
- **Full-text Search** - Text search with relevance scoring
|
|
9
|
+
- **Faceted Search** - Filter and aggregate results
|
|
10
|
+
- **Autocomplete** - Search suggestions
|
|
11
|
+
- **Bulk Indexing** - Efficient batch operations
|
|
12
|
+
- **Similar Documents** - Find related content
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Basic Setup
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { SearchService } from 'servcraft/modules/search';
|
|
20
|
+
|
|
21
|
+
// In-memory search (default)
|
|
22
|
+
const searchService = new SearchService();
|
|
23
|
+
|
|
24
|
+
// With Elasticsearch
|
|
25
|
+
const esSearch = new SearchService({
|
|
26
|
+
engine: 'elasticsearch',
|
|
27
|
+
elasticsearch: {
|
|
28
|
+
node: 'http://localhost:9200',
|
|
29
|
+
auth: { username: 'elastic', password: 'password' },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// With Meilisearch
|
|
34
|
+
const meiliSearch = new SearchService({
|
|
35
|
+
engine: 'meilisearch',
|
|
36
|
+
meilisearch: {
|
|
37
|
+
host: 'http://localhost:7700',
|
|
38
|
+
apiKey: 'masterKey',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Index Management
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Create index
|
|
47
|
+
await searchService.createIndex('products', {
|
|
48
|
+
searchableAttributes: ['name', 'description', 'category'],
|
|
49
|
+
filterableAttributes: ['category', 'price', 'inStock'],
|
|
50
|
+
sortableAttributes: ['price', 'createdAt'],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Delete index
|
|
54
|
+
await searchService.deleteIndex('products');
|
|
55
|
+
|
|
56
|
+
// Update settings
|
|
57
|
+
await searchService.updateSettings('products', {
|
|
58
|
+
searchableAttributes: ['name', 'description', 'tags'],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Get statistics
|
|
62
|
+
const stats = await searchService.getStats('products');
|
|
63
|
+
// { documentCount: 1000, indexSize: '5.2MB', ... }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Indexing Documents
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// Index single document
|
|
70
|
+
await searchService.indexDocument('products', 'prod-123', {
|
|
71
|
+
id: 'prod-123',
|
|
72
|
+
name: 'Wireless Headphones',
|
|
73
|
+
description: 'High-quality wireless headphones with noise cancellation',
|
|
74
|
+
category: 'Electronics',
|
|
75
|
+
price: 99.99,
|
|
76
|
+
inStock: true,
|
|
77
|
+
tags: ['audio', 'wireless', 'headphones'],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Bulk index
|
|
81
|
+
const products = [
|
|
82
|
+
{ id: '1', name: 'Product 1', ... },
|
|
83
|
+
{ id: '2', name: 'Product 2', ... },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const result = await searchService.indexDocuments('products', products);
|
|
87
|
+
// { success: 2, failed: 0, errors: [] }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Searching
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Simple search
|
|
94
|
+
const results = await searchService.search('products', {
|
|
95
|
+
query: 'wireless headphones',
|
|
96
|
+
limit: 20,
|
|
97
|
+
});
|
|
98
|
+
// {
|
|
99
|
+
// hits: [{ document: {...}, score: 0.95 }, ...],
|
|
100
|
+
// total: 45,
|
|
101
|
+
// took: 12
|
|
102
|
+
// }
|
|
103
|
+
|
|
104
|
+
// With filters
|
|
105
|
+
const filtered = await searchService.search('products', {
|
|
106
|
+
query: 'headphones',
|
|
107
|
+
filters: {
|
|
108
|
+
category: 'Electronics',
|
|
109
|
+
price: { $lt: 100 },
|
|
110
|
+
inStock: true,
|
|
111
|
+
},
|
|
112
|
+
limit: 10,
|
|
113
|
+
offset: 0,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// With facets
|
|
117
|
+
const faceted = await searchService.searchWithFacets('products', 'headphones', {
|
|
118
|
+
facets: ['category', 'brand'],
|
|
119
|
+
filters: { inStock: true },
|
|
120
|
+
});
|
|
121
|
+
// {
|
|
122
|
+
// hits: [...],
|
|
123
|
+
// facets: {
|
|
124
|
+
// category: { Electronics: 25, Audio: 15 },
|
|
125
|
+
// brand: { Sony: 10, Bose: 8 }
|
|
126
|
+
// }
|
|
127
|
+
// }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Document Operations
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// Get document
|
|
134
|
+
const doc = await searchService.getDocument('products', 'prod-123');
|
|
135
|
+
|
|
136
|
+
// Update document
|
|
137
|
+
await searchService.updateDocument('products', 'prod-123', {
|
|
138
|
+
price: 89.99,
|
|
139
|
+
inStock: false,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Delete document
|
|
143
|
+
await searchService.deleteDocument('products', 'prod-123');
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Autocomplete
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const suggestions = await searchService.autocomplete('products', 'wire', 10);
|
|
150
|
+
// {
|
|
151
|
+
// suggestions: ['wireless headphones', 'wireless mouse', 'wireless keyboard'],
|
|
152
|
+
// took: 5
|
|
153
|
+
// }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Similar Documents
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Find similar products
|
|
160
|
+
const similar = await searchService.searchSimilar('products', 'prod-123', 5);
|
|
161
|
+
// Returns documents similar to product 'prod-123'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Reindexing
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// Reindex with transformation
|
|
168
|
+
await searchService.reindex(
|
|
169
|
+
'products-v1',
|
|
170
|
+
'products-v2',
|
|
171
|
+
(doc) => ({
|
|
172
|
+
...doc,
|
|
173
|
+
fullName: `${doc.brand} ${doc.name}`,
|
|
174
|
+
priceRange: doc.price < 50 ? 'budget' : 'premium',
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Configuration
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
interface SearchConfig {
|
|
183
|
+
engine?: 'memory' | 'elasticsearch' | 'meilisearch';
|
|
184
|
+
defaultSettings?: IndexSettings;
|
|
185
|
+
elasticsearch?: {
|
|
186
|
+
node: string;
|
|
187
|
+
auth?: { username: string; password: string };
|
|
188
|
+
};
|
|
189
|
+
meilisearch?: {
|
|
190
|
+
host: string;
|
|
191
|
+
apiKey: string;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface IndexSettings {
|
|
196
|
+
searchableAttributes?: string[];
|
|
197
|
+
filterableAttributes?: string[];
|
|
198
|
+
sortableAttributes?: string[];
|
|
199
|
+
distinctAttribute?: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface SearchQuery {
|
|
203
|
+
query: string;
|
|
204
|
+
filters?: Record<string, unknown>;
|
|
205
|
+
facets?: string[];
|
|
206
|
+
limit?: number;
|
|
207
|
+
offset?: number;
|
|
208
|
+
sort?: string[];
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Filter Operators
|
|
213
|
+
|
|
214
|
+
| Operator | Description | Example |
|
|
215
|
+
|----------|-------------|---------|
|
|
216
|
+
| `$eq` | Equal | `{ status: { $eq: 'active' } }` |
|
|
217
|
+
| `$ne` | Not equal | `{ status: { $ne: 'deleted' } }` |
|
|
218
|
+
| `$gt` | Greater than | `{ price: { $gt: 50 } }` |
|
|
219
|
+
| `$gte` | Greater or equal | `{ rating: { $gte: 4 } }` |
|
|
220
|
+
| `$lt` | Less than | `{ price: { $lt: 100 } }` |
|
|
221
|
+
| `$lte` | Less or equal | `{ stock: { $lte: 10 } }` |
|
|
222
|
+
| `$in` | In array | `{ category: { $in: ['A', 'B'] } }` |
|
|
223
|
+
| `$nin` | Not in array | `{ status: { $nin: ['deleted'] } }` |
|
|
224
|
+
|
|
225
|
+
## Fastify Integration
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// Search endpoint
|
|
229
|
+
fastify.get('/api/search', async (request, reply) => {
|
|
230
|
+
const { q, category, page = 1, limit = 20 } = request.query;
|
|
231
|
+
|
|
232
|
+
const results = await searchService.search('products', {
|
|
233
|
+
query: q,
|
|
234
|
+
filters: category ? { category } : undefined,
|
|
235
|
+
limit,
|
|
236
|
+
offset: (page - 1) * limit,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
data: results.hits.map(h => h.document),
|
|
242
|
+
total: results.total,
|
|
243
|
+
page,
|
|
244
|
+
totalPages: Math.ceil(results.total / limit),
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Autocomplete endpoint
|
|
249
|
+
fastify.get('/api/search/suggest', async (request, reply) => {
|
|
250
|
+
const { q } = request.query;
|
|
251
|
+
|
|
252
|
+
const suggestions = await searchService.autocomplete('products', q, 5);
|
|
253
|
+
return { suggestions: suggestions.suggestions };
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Indexing Strategy
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// When to index
|
|
261
|
+
userService.on('user:created', async (user) => {
|
|
262
|
+
await searchService.indexDocument('users', user.id, {
|
|
263
|
+
id: user.id,
|
|
264
|
+
name: user.name,
|
|
265
|
+
email: user.email,
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
userService.on('user:updated', async (user) => {
|
|
270
|
+
await searchService.updateDocument('users', user.id, {
|
|
271
|
+
name: user.name,
|
|
272
|
+
email: user.email,
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
userService.on('user:deleted', async (userId) => {
|
|
277
|
+
await searchService.deleteDocument('users', userId);
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Best Practices
|
|
282
|
+
|
|
283
|
+
1. **Index Design** - Only index searchable/filterable fields
|
|
284
|
+
2. **Bulk Operations** - Use bulk indexing for large datasets
|
|
285
|
+
3. **Pagination** - Always paginate search results
|
|
286
|
+
4. **Relevance Tuning** - Configure searchable attributes by importance
|
|
287
|
+
5. **Sync Strategy** - Keep search index in sync with database
|
|
288
|
+
6. **Monitoring** - Track search latency and index size
|