servcraft 0.1.0 → 0.1.1
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 +29 -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/README.md +1070 -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,294 @@
|
|
|
1
|
+
# Validation Module
|
|
2
|
+
|
|
3
|
+
Request validation using Zod schemas with type-safe validation helpers.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Zod Integration** - Full Zod schema support
|
|
8
|
+
- **Type Safety** - Full TypeScript inference
|
|
9
|
+
- **Error Formatting** - Structured error messages
|
|
10
|
+
- **Common Schemas** - Pre-built validation schemas
|
|
11
|
+
- **Request Validation** - Body, query, and params validation
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Basic Validation
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { validate, validateBody, validateQuery, validateParams } from 'servcraft/modules/validation';
|
|
20
|
+
|
|
21
|
+
// Define schema
|
|
22
|
+
const userSchema = z.object({
|
|
23
|
+
email: z.string().email(),
|
|
24
|
+
name: z.string().min(2).max(50),
|
|
25
|
+
age: z.number().min(18).optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Validate data
|
|
29
|
+
const validatedUser = validate(userSchema, requestBody);
|
|
30
|
+
// Returns typed data or throws ValidationError
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Request Validation
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Validate request body
|
|
37
|
+
const createUser = async (request, reply) => {
|
|
38
|
+
const body = validateBody(userSchema, request.body);
|
|
39
|
+
// body is typed as { email: string; name: string; age?: number }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Validate query parameters
|
|
43
|
+
const listUsers = async (request, reply) => {
|
|
44
|
+
const query = validateQuery(paginationSchema, request.query);
|
|
45
|
+
// query is typed with pagination fields
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Validate URL parameters
|
|
49
|
+
const getUser = async (request, reply) => {
|
|
50
|
+
const params = validateParams(idParamSchema, request.params);
|
|
51
|
+
// params is typed as { id: string }
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Pre-built Schemas
|
|
56
|
+
|
|
57
|
+
### ID Parameter
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { idParamSchema, IdParam } from 'servcraft/modules/validation';
|
|
61
|
+
|
|
62
|
+
// Schema: { id: string (UUID) }
|
|
63
|
+
const params = validateParams(idParamSchema, request.params);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Pagination
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { paginationSchema, PaginationInput } from 'servcraft/modules/validation';
|
|
70
|
+
|
|
71
|
+
// Schema: { page, limit, sortBy, sortOrder }
|
|
72
|
+
const query = validateQuery(paginationSchema, request.query);
|
|
73
|
+
// Defaults: page=1, limit=20, sortOrder='asc'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Search
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { searchSchema } from 'servcraft/modules/validation';
|
|
80
|
+
|
|
81
|
+
// Schema: { q?, search? }
|
|
82
|
+
const query = validateQuery(searchSchema, request.query);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Email
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { emailSchema } from 'servcraft/modules/validation';
|
|
89
|
+
|
|
90
|
+
const email = emailSchema.parse('user@example.com');
|
|
91
|
+
// Throws if invalid email
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Password (Strong)
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { passwordSchema } from 'servcraft/modules/validation';
|
|
98
|
+
|
|
99
|
+
// Requirements:
|
|
100
|
+
// - Minimum 8 characters
|
|
101
|
+
// - At least one uppercase letter
|
|
102
|
+
// - At least one lowercase letter
|
|
103
|
+
// - At least one number
|
|
104
|
+
// - At least one special character
|
|
105
|
+
|
|
106
|
+
const password = passwordSchema.parse('MyP@ssw0rd!');
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### URL
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { urlSchema } from 'servcraft/modules/validation';
|
|
113
|
+
|
|
114
|
+
const url = urlSchema.parse('https://example.com');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Phone
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { phoneSchema } from 'servcraft/modules/validation';
|
|
121
|
+
|
|
122
|
+
// International format: +1234567890
|
|
123
|
+
const phone = phoneSchema.parse('+12025551234');
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Date
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { dateSchema, futureDateSchema, pastDateSchema } from 'servcraft/modules/validation';
|
|
130
|
+
|
|
131
|
+
// Any valid date
|
|
132
|
+
const date = dateSchema.parse('2024-12-20');
|
|
133
|
+
|
|
134
|
+
// Must be in the future
|
|
135
|
+
const futureDate = futureDateSchema.parse('2025-01-01');
|
|
136
|
+
|
|
137
|
+
// Must be in the past
|
|
138
|
+
const pastDate = pastDateSchema.parse('2020-01-01');
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Custom Schemas
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { z } from 'zod';
|
|
145
|
+
|
|
146
|
+
// User registration schema
|
|
147
|
+
const registerSchema = z.object({
|
|
148
|
+
email: z.string().email('Invalid email address'),
|
|
149
|
+
password: z.string()
|
|
150
|
+
.min(8, 'Password must be at least 8 characters')
|
|
151
|
+
.regex(/[A-Z]/, 'Password must contain uppercase'),
|
|
152
|
+
confirmPassword: z.string(),
|
|
153
|
+
name: z.string().min(2).max(50),
|
|
154
|
+
acceptTerms: z.literal(true, {
|
|
155
|
+
errorMap: () => ({ message: 'You must accept the terms' }),
|
|
156
|
+
}),
|
|
157
|
+
}).refine(data => data.password === data.confirmPassword, {
|
|
158
|
+
message: 'Passwords do not match',
|
|
159
|
+
path: ['confirmPassword'],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Order schema
|
|
163
|
+
const orderSchema = z.object({
|
|
164
|
+
items: z.array(z.object({
|
|
165
|
+
productId: z.string().uuid(),
|
|
166
|
+
quantity: z.number().int().positive(),
|
|
167
|
+
})).min(1, 'Order must have at least one item'),
|
|
168
|
+
shippingAddress: z.object({
|
|
169
|
+
street: z.string(),
|
|
170
|
+
city: z.string(),
|
|
171
|
+
zipCode: z.string(),
|
|
172
|
+
country: z.string().length(2),
|
|
173
|
+
}),
|
|
174
|
+
notes: z.string().max(500).optional(),
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Error Handling
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { ValidationError } from 'servcraft/utils/errors';
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const data = validate(schema, input);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error instanceof ValidationError) {
|
|
187
|
+
// error.errors is structured:
|
|
188
|
+
// {
|
|
189
|
+
// email: ['Invalid email address'],
|
|
190
|
+
// password: ['Password must be at least 8 characters'],
|
|
191
|
+
// 'address.zipCode': ['Invalid zip code']
|
|
192
|
+
// }
|
|
193
|
+
|
|
194
|
+
return reply.status(400).send({
|
|
195
|
+
success: false,
|
|
196
|
+
message: 'Validation failed',
|
|
197
|
+
errors: error.errors,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Fastify Integration
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { z } from 'zod';
|
|
207
|
+
import { validateBody, validateQuery, validateParams } from 'servcraft/modules/validation';
|
|
208
|
+
|
|
209
|
+
// Route with validation
|
|
210
|
+
fastify.post('/api/users', async (request, reply) => {
|
|
211
|
+
const body = validateBody(registerSchema, request.body);
|
|
212
|
+
|
|
213
|
+
const user = await userService.create(body);
|
|
214
|
+
return { success: true, data: user };
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// With query and params
|
|
218
|
+
fastify.get('/api/users/:id/orders', async (request, reply) => {
|
|
219
|
+
const params = validateParams(idParamSchema, request.params);
|
|
220
|
+
const query = validateQuery(paginationSchema, request.query);
|
|
221
|
+
|
|
222
|
+
const orders = await orderService.findByUser(params.id, query);
|
|
223
|
+
return { success: true, data: orders };
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Type Inference
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { z } from 'zod';
|
|
231
|
+
|
|
232
|
+
const schema = z.object({
|
|
233
|
+
name: z.string(),
|
|
234
|
+
age: z.number(),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Infer type from schema
|
|
238
|
+
type User = z.infer<typeof schema>;
|
|
239
|
+
// { name: string; age: number }
|
|
240
|
+
|
|
241
|
+
// Use in function
|
|
242
|
+
function createUser(data: z.infer<typeof schema>) {
|
|
243
|
+
// data is typed
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Common Patterns
|
|
248
|
+
|
|
249
|
+
### Optional with Default
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
const schema = z.object({
|
|
253
|
+
limit: z.number().default(20),
|
|
254
|
+
active: z.boolean().default(true),
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Transform
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const schema = z.object({
|
|
262
|
+
email: z.string().email().toLowerCase(),
|
|
263
|
+
tags: z.string().transform(s => s.split(',')),
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Refinement
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
const schema = z.object({
|
|
271
|
+
startDate: z.date(),
|
|
272
|
+
endDate: z.date(),
|
|
273
|
+
}).refine(
|
|
274
|
+
data => data.endDate > data.startDate,
|
|
275
|
+
{ message: 'End date must be after start date' }
|
|
276
|
+
);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Union Types
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const schema = z.discriminatedUnion('type', [
|
|
283
|
+
z.object({ type: z.literal('email'), email: z.string().email() }),
|
|
284
|
+
z.object({ type: z.literal('sms'), phone: z.string() }),
|
|
285
|
+
]);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Best Practices
|
|
289
|
+
|
|
290
|
+
1. **Reuse Schemas** - Create reusable schemas for common patterns
|
|
291
|
+
2. **Meaningful Errors** - Provide clear error messages
|
|
292
|
+
3. **Type Safety** - Use `z.infer<typeof schema>` for types
|
|
293
|
+
4. **Validation Early** - Validate at the start of handlers
|
|
294
|
+
5. **Sanitize Input** - Use transforms for normalization
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Webhook Module
|
|
2
|
+
|
|
3
|
+
Outbound webhook system with retry logic, signatures, and delivery tracking.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Endpoint Management** - Create, update, delete webhook endpoints
|
|
8
|
+
- **Event Publishing** - Publish events to subscribed endpoints
|
|
9
|
+
- **Automatic Retries** - Exponential backoff with configurable limits
|
|
10
|
+
- **Signature Verification** - HMAC signatures for payload integrity
|
|
11
|
+
- **Delivery Tracking** - Full delivery history with status tracking
|
|
12
|
+
- **Secret Rotation** - Rotate endpoint secrets without downtime
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
18
|
+
│ Webhook Service │
|
|
19
|
+
├──────────────────────────────────────────────────────────────┤
|
|
20
|
+
│ Endpoint Mgmt │ Event Publishing │ Retry Processor │
|
|
21
|
+
└────────┬────────┴─────────┬──────────┴──────────┬────────────┘
|
|
22
|
+
│ │ │
|
|
23
|
+
▼ ▼ ▼
|
|
24
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
25
|
+
│ Prisma Repository │
|
|
26
|
+
├──────────────────────────────────────────────────────────────┤
|
|
27
|
+
│ Endpoints │ Deliveries │ Statistics │
|
|
28
|
+
└─────────────────┴──────────────────┴─────────────────────────┘
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Basic Setup
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { createWebhookService } from 'servcraft/modules/webhook';
|
|
37
|
+
|
|
38
|
+
const webhookService = createWebhookService({
|
|
39
|
+
maxRetries: 5,
|
|
40
|
+
initialRetryDelay: 1000,
|
|
41
|
+
maxRetryDelay: 60000,
|
|
42
|
+
backoffMultiplier: 2,
|
|
43
|
+
timeout: 10000,
|
|
44
|
+
enableSignature: true,
|
|
45
|
+
signatureHeader: 'X-Webhook-Signature',
|
|
46
|
+
timestampHeader: 'X-Webhook-Timestamp',
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Endpoint Management
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// Create endpoint
|
|
54
|
+
const endpoint = await webhookService.createEndpoint({
|
|
55
|
+
url: 'https://example.com/webhooks',
|
|
56
|
+
events: ['user.created', 'user.updated', 'order.completed'],
|
|
57
|
+
headers: { 'X-Custom-Header': 'value' },
|
|
58
|
+
description: 'Main webhook endpoint',
|
|
59
|
+
});
|
|
60
|
+
// endpoint.secret is auto-generated
|
|
61
|
+
|
|
62
|
+
// List endpoints
|
|
63
|
+
const endpoints = await webhookService.listEndpoints();
|
|
64
|
+
|
|
65
|
+
// Update endpoint
|
|
66
|
+
await webhookService.updateEndpoint(endpoint.id, {
|
|
67
|
+
events: ['user.created', 'user.updated'],
|
|
68
|
+
enabled: true,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Rotate secret
|
|
72
|
+
const rotated = await webhookService.rotateSecret(endpoint.id);
|
|
73
|
+
// rotated.secret is new secret
|
|
74
|
+
|
|
75
|
+
// Delete endpoint
|
|
76
|
+
await webhookService.deleteEndpoint(endpoint.id);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Publishing Events
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Publish to all matching endpoints
|
|
83
|
+
const event = await webhookService.publishEvent('user.created', {
|
|
84
|
+
userId: 'user-123',
|
|
85
|
+
email: 'user@example.com',
|
|
86
|
+
createdAt: new Date().toISOString(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Publish to specific endpoints only
|
|
90
|
+
await webhookService.publishEvent('order.completed', payload, ['endpoint-id-1', 'endpoint-id-2']);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Delivery Management
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Get delivery status
|
|
97
|
+
const delivery = await webhookService.getDelivery(deliveryId);
|
|
98
|
+
// {
|
|
99
|
+
// id, endpointId, eventType, payload,
|
|
100
|
+
// status: 'pending' | 'success' | 'retrying' | 'failed',
|
|
101
|
+
// attempts, deliveredAt, error
|
|
102
|
+
// }
|
|
103
|
+
|
|
104
|
+
// List deliveries with filters
|
|
105
|
+
const deliveries = await webhookService.listDeliveries({
|
|
106
|
+
endpointId: 'endpoint-123',
|
|
107
|
+
status: 'failed',
|
|
108
|
+
eventType: 'user.created',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Manual retry
|
|
112
|
+
await webhookService.retryDelivery(deliveryId);
|
|
113
|
+
|
|
114
|
+
// Get statistics
|
|
115
|
+
const stats = await webhookService.getStats(endpointId);
|
|
116
|
+
// {
|
|
117
|
+
// totalEvents, successfulDeliveries, failedDeliveries,
|
|
118
|
+
// pendingDeliveries, successRate
|
|
119
|
+
// }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Cleanup
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Clean up old deliveries (older than 30 days)
|
|
126
|
+
const deleted = await webhookService.cleanup(30);
|
|
127
|
+
|
|
128
|
+
// Stop retry processor (on shutdown)
|
|
129
|
+
webhookService.stop();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Configuration
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
interface WebhookConfig {
|
|
136
|
+
maxRetries?: number; // Max retry attempts (default: 5)
|
|
137
|
+
initialRetryDelay?: number; // First retry delay ms (default: 1000)
|
|
138
|
+
maxRetryDelay?: number; // Max retry delay ms (default: 60000)
|
|
139
|
+
backoffMultiplier?: number; // Backoff multiplier (default: 2)
|
|
140
|
+
timeout?: number; // Request timeout ms (default: 10000)
|
|
141
|
+
enableSignature?: boolean; // Enable HMAC signatures (default: true)
|
|
142
|
+
signatureHeader?: string; // Signature header name
|
|
143
|
+
timestampHeader?: string; // Timestamp header name
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Webhook Payload Format
|
|
148
|
+
|
|
149
|
+
Delivered webhooks have this structure:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"id": "delivery-uuid",
|
|
154
|
+
"type": "user.created",
|
|
155
|
+
"created": "2024-01-15T10:30:00.000Z",
|
|
156
|
+
"data": {
|
|
157
|
+
"userId": "user-123",
|
|
158
|
+
"email": "user@example.com"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Signature Verification
|
|
164
|
+
|
|
165
|
+
Webhooks are signed using HMAC-SHA256. Recipients should verify:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// Headers sent with webhook
|
|
169
|
+
// X-Webhook-Signature: t=1705312200,v1=abc123...
|
|
170
|
+
// X-Webhook-Timestamp: 1705312200
|
|
171
|
+
|
|
172
|
+
// Verification (recipient side)
|
|
173
|
+
import crypto from 'crypto';
|
|
174
|
+
|
|
175
|
+
function verifyWebhookSignature(
|
|
176
|
+
payload: string,
|
|
177
|
+
signature: string,
|
|
178
|
+
timestamp: string,
|
|
179
|
+
secret: string
|
|
180
|
+
): boolean {
|
|
181
|
+
const signedPayload = `${timestamp}.${payload}`;
|
|
182
|
+
const expectedSignature = crypto
|
|
183
|
+
.createHmac('sha256', secret)
|
|
184
|
+
.update(signedPayload)
|
|
185
|
+
.digest('hex');
|
|
186
|
+
|
|
187
|
+
// Parse signature header: t=timestamp,v1=signature
|
|
188
|
+
const parts = signature.split(',');
|
|
189
|
+
const receivedSig = parts.find(p => p.startsWith('v1='))?.slice(3);
|
|
190
|
+
|
|
191
|
+
return crypto.timingSafeEqual(
|
|
192
|
+
Buffer.from(expectedSignature),
|
|
193
|
+
Buffer.from(receivedSig || '')
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Retry Strategy
|
|
199
|
+
|
|
200
|
+
Retries use exponential backoff:
|
|
201
|
+
|
|
202
|
+
| Attempt | Delay |
|
|
203
|
+
|---------|-------|
|
|
204
|
+
| 1 | Immediate |
|
|
205
|
+
| 2 | 1 second |
|
|
206
|
+
| 3 | 2 seconds |
|
|
207
|
+
| 4 | 4 seconds |
|
|
208
|
+
| 5 | 8 seconds |
|
|
209
|
+
| 6+ | Failed |
|
|
210
|
+
|
|
211
|
+
## Event Types
|
|
212
|
+
|
|
213
|
+
Define your event types:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
type WebhookEventType =
|
|
217
|
+
| 'user.created'
|
|
218
|
+
| 'user.updated'
|
|
219
|
+
| 'user.deleted'
|
|
220
|
+
| 'order.created'
|
|
221
|
+
| 'order.completed'
|
|
222
|
+
| 'order.cancelled'
|
|
223
|
+
| 'payment.succeeded'
|
|
224
|
+
| 'payment.failed'
|
|
225
|
+
| 'subscription.created'
|
|
226
|
+
| 'subscription.cancelled';
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Database Schema
|
|
230
|
+
|
|
231
|
+
```sql
|
|
232
|
+
-- Endpoints
|
|
233
|
+
CREATE TABLE webhook_endpoints (
|
|
234
|
+
id UUID PRIMARY KEY,
|
|
235
|
+
url TEXT NOT NULL,
|
|
236
|
+
secret TEXT NOT NULL,
|
|
237
|
+
events TEXT[] NOT NULL,
|
|
238
|
+
headers JSONB,
|
|
239
|
+
enabled BOOLEAN DEFAULT true,
|
|
240
|
+
description TEXT,
|
|
241
|
+
created_at TIMESTAMP,
|
|
242
|
+
updated_at TIMESTAMP
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
-- Deliveries
|
|
246
|
+
CREATE TABLE webhook_deliveries (
|
|
247
|
+
id UUID PRIMARY KEY,
|
|
248
|
+
endpoint_id UUID REFERENCES webhook_endpoints,
|
|
249
|
+
event_type TEXT NOT NULL,
|
|
250
|
+
payload JSONB NOT NULL,
|
|
251
|
+
status TEXT NOT NULL,
|
|
252
|
+
attempts INTEGER DEFAULT 0,
|
|
253
|
+
max_attempts INTEGER DEFAULT 5,
|
|
254
|
+
delivered_at TIMESTAMP,
|
|
255
|
+
next_retry_at TIMESTAMP,
|
|
256
|
+
response_status INTEGER,
|
|
257
|
+
response_body TEXT,
|
|
258
|
+
error TEXT,
|
|
259
|
+
created_at TIMESTAMP
|
|
260
|
+
);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Best Practices
|
|
264
|
+
|
|
265
|
+
1. **Idempotency** - Include delivery ID for idempotent processing
|
|
266
|
+
2. **Timeout Handling** - Set reasonable timeouts (10s recommended)
|
|
267
|
+
3. **Signature Verification** - Always verify signatures on receipt
|
|
268
|
+
4. **Event Filtering** - Subscribe only to needed events
|
|
269
|
+
5. **Retry Handling** - Implement idempotent handlers for retries
|
|
270
|
+
6. **Secret Rotation** - Rotate secrets periodically
|