fastypest 3.0.9 → 3.0.11
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/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
- package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
- package/.agents/skills/nodejs-best-practices/SKILL.md +338 -0
- package/.agents/skills/typescript-advanced-types/SKILL.md +717 -0
- package/package.json +10 -10
- package/skills-lock.json +20 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# Node.js Advanced Patterns
|
|
2
|
+
|
|
3
|
+
Advanced patterns for dependency injection, database integration, authentication, caching, and API response formatting.
|
|
4
|
+
|
|
5
|
+
## Dependency Injection
|
|
6
|
+
|
|
7
|
+
### DI Container
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// di-container.ts
|
|
11
|
+
import { Pool } from "pg";
|
|
12
|
+
import { UserRepository } from "./repositories/user.repository";
|
|
13
|
+
import { UserService } from "./services/user.service";
|
|
14
|
+
import { UserController } from "./controllers/user.controller";
|
|
15
|
+
import { AuthService } from "./services/auth.service";
|
|
16
|
+
|
|
17
|
+
class Container {
|
|
18
|
+
private instances = new Map<string, any>();
|
|
19
|
+
|
|
20
|
+
register<T>(key: string, factory: () => T): void {
|
|
21
|
+
this.instances.set(key, factory);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
resolve<T>(key: string): T {
|
|
25
|
+
const factory = this.instances.get(key);
|
|
26
|
+
if (!factory) {
|
|
27
|
+
throw new Error(`No factory registered for ${key}`);
|
|
28
|
+
}
|
|
29
|
+
return factory();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
singleton<T>(key: string, factory: () => T): void {
|
|
33
|
+
let instance: T;
|
|
34
|
+
this.instances.set(key, () => {
|
|
35
|
+
if (!instance) {
|
|
36
|
+
instance = factory();
|
|
37
|
+
}
|
|
38
|
+
return instance;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const container = new Container();
|
|
44
|
+
|
|
45
|
+
// Register dependencies
|
|
46
|
+
container.singleton(
|
|
47
|
+
"db",
|
|
48
|
+
() =>
|
|
49
|
+
new Pool({
|
|
50
|
+
host: process.env.DB_HOST,
|
|
51
|
+
port: parseInt(process.env.DB_PORT || "5432"),
|
|
52
|
+
database: process.env.DB_NAME,
|
|
53
|
+
user: process.env.DB_USER,
|
|
54
|
+
password: process.env.DB_PASSWORD,
|
|
55
|
+
max: 20,
|
|
56
|
+
idleTimeoutMillis: 30000,
|
|
57
|
+
connectionTimeoutMillis: 2000,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
container.singleton(
|
|
62
|
+
"userRepository",
|
|
63
|
+
() => new UserRepository(container.resolve("db")),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
container.singleton(
|
|
67
|
+
"userService",
|
|
68
|
+
() => new UserService(container.resolve("userRepository")),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
container.register(
|
|
72
|
+
"userController",
|
|
73
|
+
() => new UserController(container.resolve("userService")),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
container.singleton(
|
|
77
|
+
"authService",
|
|
78
|
+
() => new AuthService(container.resolve("userRepository")),
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Database Patterns
|
|
83
|
+
|
|
84
|
+
### PostgreSQL with Connection Pool
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// config/database.ts
|
|
88
|
+
import { Pool, PoolConfig } from "pg";
|
|
89
|
+
|
|
90
|
+
const poolConfig: PoolConfig = {
|
|
91
|
+
host: process.env.DB_HOST,
|
|
92
|
+
port: parseInt(process.env.DB_PORT || "5432"),
|
|
93
|
+
database: process.env.DB_NAME,
|
|
94
|
+
user: process.env.DB_USER,
|
|
95
|
+
password: process.env.DB_PASSWORD,
|
|
96
|
+
max: 20,
|
|
97
|
+
idleTimeoutMillis: 30000,
|
|
98
|
+
connectionTimeoutMillis: 2000,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const pool = new Pool(poolConfig);
|
|
102
|
+
|
|
103
|
+
// Test connection
|
|
104
|
+
pool.on("connect", () => {
|
|
105
|
+
console.log("Database connected");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
pool.on("error", (err) => {
|
|
109
|
+
console.error("Unexpected database error", err);
|
|
110
|
+
process.exit(-1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Graceful shutdown
|
|
114
|
+
export const closeDatabase = async () => {
|
|
115
|
+
await pool.end();
|
|
116
|
+
console.log("Database connection closed");
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### MongoDB with Mongoose
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// config/mongoose.ts
|
|
124
|
+
import mongoose from "mongoose";
|
|
125
|
+
|
|
126
|
+
const connectDB = async () => {
|
|
127
|
+
try {
|
|
128
|
+
await mongoose.connect(process.env.MONGODB_URI!, {
|
|
129
|
+
maxPoolSize: 10,
|
|
130
|
+
serverSelectionTimeoutMS: 5000,
|
|
131
|
+
socketTimeoutMS: 45000,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log("MongoDB connected");
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error("MongoDB connection error:", error);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
mongoose.connection.on("disconnected", () => {
|
|
142
|
+
console.log("MongoDB disconnected");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
mongoose.connection.on("error", (err) => {
|
|
146
|
+
console.error("MongoDB error:", err);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
export { connectDB };
|
|
150
|
+
|
|
151
|
+
// Model example
|
|
152
|
+
import { Schema, model, Document } from "mongoose";
|
|
153
|
+
|
|
154
|
+
interface IUser extends Document {
|
|
155
|
+
name: string;
|
|
156
|
+
email: string;
|
|
157
|
+
password: string;
|
|
158
|
+
createdAt: Date;
|
|
159
|
+
updatedAt: Date;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const userSchema = new Schema<IUser>(
|
|
163
|
+
{
|
|
164
|
+
name: { type: String, required: true },
|
|
165
|
+
email: { type: String, required: true, unique: true },
|
|
166
|
+
password: { type: String, required: true },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
timestamps: true,
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Indexes
|
|
174
|
+
userSchema.index({ email: 1 });
|
|
175
|
+
|
|
176
|
+
export const User = model<IUser>("User", userSchema);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Transaction Pattern
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// services/order.service.ts
|
|
183
|
+
import { Pool } from "pg";
|
|
184
|
+
|
|
185
|
+
export class OrderService {
|
|
186
|
+
constructor(private db: Pool) {}
|
|
187
|
+
|
|
188
|
+
async createOrder(userId: string, items: any[]) {
|
|
189
|
+
const client = await this.db.connect();
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await client.query("BEGIN");
|
|
193
|
+
|
|
194
|
+
// Create order
|
|
195
|
+
const orderResult = await client.query(
|
|
196
|
+
"INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id",
|
|
197
|
+
[userId, calculateTotal(items)],
|
|
198
|
+
);
|
|
199
|
+
const orderId = orderResult.rows[0].id;
|
|
200
|
+
|
|
201
|
+
// Create order items
|
|
202
|
+
for (const item of items) {
|
|
203
|
+
await client.query(
|
|
204
|
+
"INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)",
|
|
205
|
+
[orderId, item.productId, item.quantity, item.price],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Update inventory
|
|
209
|
+
await client.query(
|
|
210
|
+
"UPDATE products SET stock = stock - $1 WHERE id = $2",
|
|
211
|
+
[item.quantity, item.productId],
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await client.query("COMMIT");
|
|
216
|
+
return orderId;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
await client.query("ROLLBACK");
|
|
219
|
+
throw error;
|
|
220
|
+
} finally {
|
|
221
|
+
client.release();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Authentication & Authorization
|
|
228
|
+
|
|
229
|
+
### JWT Authentication
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// services/auth.service.ts
|
|
233
|
+
import jwt from "jsonwebtoken";
|
|
234
|
+
import bcrypt from "bcrypt";
|
|
235
|
+
import { UserRepository } from "../repositories/user.repository";
|
|
236
|
+
import { UnauthorizedError } from "../utils/errors";
|
|
237
|
+
|
|
238
|
+
export class AuthService {
|
|
239
|
+
constructor(private userRepository: UserRepository) {}
|
|
240
|
+
|
|
241
|
+
async login(email: string, password: string) {
|
|
242
|
+
const user = await this.userRepository.findByEmail(email);
|
|
243
|
+
|
|
244
|
+
if (!user) {
|
|
245
|
+
throw new UnauthorizedError("Invalid credentials");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const isValid = await bcrypt.compare(password, user.password);
|
|
249
|
+
|
|
250
|
+
if (!isValid) {
|
|
251
|
+
throw new UnauthorizedError("Invalid credentials");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const token = this.generateToken({
|
|
255
|
+
userId: user.id,
|
|
256
|
+
email: user.email,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const refreshToken = this.generateRefreshToken({
|
|
260
|
+
userId: user.id,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
token,
|
|
265
|
+
refreshToken,
|
|
266
|
+
user: {
|
|
267
|
+
id: user.id,
|
|
268
|
+
name: user.name,
|
|
269
|
+
email: user.email,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async refreshToken(refreshToken: string) {
|
|
275
|
+
try {
|
|
276
|
+
const payload = jwt.verify(
|
|
277
|
+
refreshToken,
|
|
278
|
+
process.env.REFRESH_TOKEN_SECRET!,
|
|
279
|
+
) as { userId: string };
|
|
280
|
+
|
|
281
|
+
const user = await this.userRepository.findById(payload.userId);
|
|
282
|
+
|
|
283
|
+
if (!user) {
|
|
284
|
+
throw new UnauthorizedError("User not found");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const token = this.generateToken({
|
|
288
|
+
userId: user.id,
|
|
289
|
+
email: user.email,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return { token };
|
|
293
|
+
} catch (error) {
|
|
294
|
+
throw new UnauthorizedError("Invalid refresh token");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private generateToken(payload: any): string {
|
|
299
|
+
return jwt.sign(payload, process.env.JWT_SECRET!, {
|
|
300
|
+
expiresIn: "15m",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private generateRefreshToken(payload: any): string {
|
|
305
|
+
return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, {
|
|
306
|
+
expiresIn: "7d",
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Caching Strategies
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// utils/cache.ts
|
|
316
|
+
import Redis from "ioredis";
|
|
317
|
+
|
|
318
|
+
const redis = new Redis({
|
|
319
|
+
host: process.env.REDIS_HOST,
|
|
320
|
+
port: parseInt(process.env.REDIS_PORT || "6379"),
|
|
321
|
+
retryStrategy: (times) => {
|
|
322
|
+
const delay = Math.min(times * 50, 2000);
|
|
323
|
+
return delay;
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
export class CacheService {
|
|
328
|
+
async get<T>(key: string): Promise<T | null> {
|
|
329
|
+
const data = await redis.get(key);
|
|
330
|
+
return data ? JSON.parse(data) : null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async set(key: string, value: any, ttl?: number): Promise<void> {
|
|
334
|
+
const serialized = JSON.stringify(value);
|
|
335
|
+
if (ttl) {
|
|
336
|
+
await redis.setex(key, ttl, serialized);
|
|
337
|
+
} else {
|
|
338
|
+
await redis.set(key, serialized);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async delete(key: string): Promise<void> {
|
|
343
|
+
await redis.del(key);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async invalidatePattern(pattern: string): Promise<void> {
|
|
347
|
+
const keys = await redis.keys(pattern);
|
|
348
|
+
if (keys.length > 0) {
|
|
349
|
+
await redis.del(...keys);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Cache decorator
|
|
355
|
+
export function Cacheable(ttl: number = 300) {
|
|
356
|
+
return function (
|
|
357
|
+
target: any,
|
|
358
|
+
propertyKey: string,
|
|
359
|
+
descriptor: PropertyDescriptor,
|
|
360
|
+
) {
|
|
361
|
+
const originalMethod = descriptor.value;
|
|
362
|
+
|
|
363
|
+
descriptor.value = async function (...args: any[]) {
|
|
364
|
+
const cache = new CacheService();
|
|
365
|
+
const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
|
|
366
|
+
|
|
367
|
+
const cached = await cache.get(cacheKey);
|
|
368
|
+
if (cached) {
|
|
369
|
+
return cached;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const result = await originalMethod.apply(this, args);
|
|
373
|
+
await cache.set(cacheKey, result, ttl);
|
|
374
|
+
|
|
375
|
+
return result;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
return descriptor;
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## API Response Format
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// utils/response.ts
|
|
387
|
+
import { Response } from "express";
|
|
388
|
+
|
|
389
|
+
export class ApiResponse {
|
|
390
|
+
static success<T>(
|
|
391
|
+
res: Response,
|
|
392
|
+
data: T,
|
|
393
|
+
message?: string,
|
|
394
|
+
statusCode = 200,
|
|
395
|
+
) {
|
|
396
|
+
return res.status(statusCode).json({
|
|
397
|
+
status: "success",
|
|
398
|
+
message,
|
|
399
|
+
data,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
static error(res: Response, message: string, statusCode = 500, errors?: any) {
|
|
404
|
+
return res.status(statusCode).json({
|
|
405
|
+
status: "error",
|
|
406
|
+
message,
|
|
407
|
+
...(errors && { errors }),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
static paginated<T>(
|
|
412
|
+
res: Response,
|
|
413
|
+
data: T[],
|
|
414
|
+
page: number,
|
|
415
|
+
limit: number,
|
|
416
|
+
total: number,
|
|
417
|
+
) {
|
|
418
|
+
return res.json({
|
|
419
|
+
status: "success",
|
|
420
|
+
data,
|
|
421
|
+
pagination: {
|
|
422
|
+
page,
|
|
423
|
+
limit,
|
|
424
|
+
total,
|
|
425
|
+
pages: Math.ceil(total / limit),
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|