@ranimontagna/agent-toolkit 0.1.4 → 0.1.5
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 +282 -277
- package/docs/assets/install-plan.svg +29 -0
- package/docs/assets/install-skill-packages.svg +31 -0
- package/docs/assets/install-status.svg +32 -0
- package/package.json +10 -9
- package/setup-agent-toolkit.sh +1 -1
- package/skills/backend/fastify-best-practices/LICENSE +21 -0
- package/skills/backend/fastify-best-practices/NOTICE.md +11 -0
- package/skills/backend/fastify-best-practices/SKILL.md +75 -0
- package/skills/backend/fastify-best-practices/rules/authentication.md +521 -0
- package/skills/backend/fastify-best-practices/rules/configuration.md +217 -0
- package/skills/backend/fastify-best-practices/rules/content-type.md +387 -0
- package/skills/backend/fastify-best-practices/rules/cors-security.md +445 -0
- package/skills/backend/fastify-best-practices/rules/database.md +320 -0
- package/skills/backend/fastify-best-practices/rules/decorators.md +416 -0
- package/skills/backend/fastify-best-practices/rules/deployment.md +423 -0
- package/skills/backend/fastify-best-practices/rules/error-handling.md +412 -0
- package/skills/backend/fastify-best-practices/rules/hooks.md +464 -0
- package/skills/backend/fastify-best-practices/rules/http-proxy.md +247 -0
- package/skills/backend/fastify-best-practices/rules/logging.md +402 -0
- package/skills/backend/fastify-best-practices/rules/performance.md +425 -0
- package/skills/backend/fastify-best-practices/rules/plugins.md +320 -0
- package/skills/backend/fastify-best-practices/rules/routes.md +467 -0
- package/skills/backend/fastify-best-practices/rules/schemas.md +585 -0
- package/skills/backend/fastify-best-practices/rules/serialization.md +475 -0
- package/skills/backend/fastify-best-practices/rules/testing.md +536 -0
- package/skills/backend/fastify-best-practices/rules/typescript.md +458 -0
- package/skills/backend/fastify-best-practices/rules/websockets.md +421 -0
- package/skills/backend/fastify-best-practices/tile.json +11 -0
- package/skills/core/agent-toolkit-maintainer/SKILL.md +16 -14
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: decorators
|
|
3
|
+
description: Decorators and request/reply extensions in Fastify
|
|
4
|
+
metadata:
|
|
5
|
+
tags: decorators, extensions, customization, utilities
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Decorators and Extensions
|
|
9
|
+
|
|
10
|
+
## Understanding Decorators
|
|
11
|
+
|
|
12
|
+
Decorators add custom properties and methods to Fastify instances, requests, and replies:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
|
|
17
|
+
const app = Fastify();
|
|
18
|
+
|
|
19
|
+
// Decorate the Fastify instance
|
|
20
|
+
app.decorate('utility', {
|
|
21
|
+
formatDate: (date: Date) => date.toISOString(),
|
|
22
|
+
generateId: () => crypto.randomUUID(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Use in routes
|
|
26
|
+
app.get('/example', async function (request, reply) {
|
|
27
|
+
const id = this.utility.generateId();
|
|
28
|
+
return { id, timestamp: this.utility.formatDate(new Date()) };
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Decorator Types
|
|
33
|
+
|
|
34
|
+
Three types of decorators for different contexts:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Instance decorator - available on fastify instance
|
|
38
|
+
app.decorate('config', { apiVersion: '1.0.0' });
|
|
39
|
+
app.decorate('db', databaseConnection);
|
|
40
|
+
app.decorate('cache', cacheClient);
|
|
41
|
+
|
|
42
|
+
// Request decorator - available on each request
|
|
43
|
+
app.decorateRequest('user', null); // Object property
|
|
44
|
+
app.decorateRequest('startTime', 0); // Primitive
|
|
45
|
+
app.decorateRequest('getData', function() { // Method
|
|
46
|
+
return this.body;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Reply decorator - available on each reply
|
|
50
|
+
app.decorateReply('sendError', function(code: number, message: string) {
|
|
51
|
+
return this.code(code).send({ error: message });
|
|
52
|
+
});
|
|
53
|
+
app.decorateReply('success', function(data: unknown) {
|
|
54
|
+
return this.send({ success: true, data });
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## TypeScript Declaration Merging
|
|
59
|
+
|
|
60
|
+
Extend Fastify types for type safety:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Declare custom properties
|
|
64
|
+
declare module 'fastify' {
|
|
65
|
+
interface FastifyInstance {
|
|
66
|
+
config: {
|
|
67
|
+
apiVersion: string;
|
|
68
|
+
environment: string;
|
|
69
|
+
};
|
|
70
|
+
db: DatabaseClient;
|
|
71
|
+
cache: CacheClient;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface FastifyRequest {
|
|
75
|
+
user: {
|
|
76
|
+
id: string;
|
|
77
|
+
email: string;
|
|
78
|
+
roles: string[];
|
|
79
|
+
} | null;
|
|
80
|
+
startTime: number;
|
|
81
|
+
requestId: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface FastifyReply {
|
|
85
|
+
sendError: (code: number, message: string) => void;
|
|
86
|
+
success: (data: unknown) => void;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Register decorators
|
|
91
|
+
app.decorate('config', {
|
|
92
|
+
apiVersion: '1.0.0',
|
|
93
|
+
environment: process.env.NODE_ENV,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
app.decorateRequest('user', null);
|
|
97
|
+
app.decorateRequest('startTime', 0);
|
|
98
|
+
|
|
99
|
+
app.decorateReply('sendError', function (code: number, message: string) {
|
|
100
|
+
this.code(code).send({ error: message });
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Decorator Initialization
|
|
105
|
+
|
|
106
|
+
Initialize request/reply decorators in hooks:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// Decorators with primitive defaults are copied
|
|
110
|
+
app.decorateRequest('startTime', 0);
|
|
111
|
+
|
|
112
|
+
// Initialize in hook
|
|
113
|
+
app.addHook('onRequest', async (request) => {
|
|
114
|
+
request.startTime = Date.now();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Object decorators need getter pattern for proper initialization
|
|
118
|
+
app.decorateRequest('context', null);
|
|
119
|
+
|
|
120
|
+
app.addHook('onRequest', async (request) => {
|
|
121
|
+
request.context = {
|
|
122
|
+
traceId: request.headers['x-trace-id'] || crypto.randomUUID(),
|
|
123
|
+
clientIp: request.ip,
|
|
124
|
+
userAgent: request.headers['user-agent'],
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Dependency Injection with Decorators
|
|
130
|
+
|
|
131
|
+
Use decorators for dependency injection:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import fp from 'fastify-plugin';
|
|
135
|
+
|
|
136
|
+
// Database plugin
|
|
137
|
+
export default fp(async function databasePlugin(fastify, options) {
|
|
138
|
+
const db = await createDatabaseConnection(options.connectionString);
|
|
139
|
+
|
|
140
|
+
fastify.decorate('db', db);
|
|
141
|
+
|
|
142
|
+
fastify.addHook('onClose', async () => {
|
|
143
|
+
await db.close();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// User service plugin
|
|
148
|
+
export default fp(async function userServicePlugin(fastify) {
|
|
149
|
+
// Depends on db decorator
|
|
150
|
+
if (!fastify.hasDecorator('db')) {
|
|
151
|
+
throw new Error('Database plugin must be registered first');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const userService = {
|
|
155
|
+
findById: (id: string) => fastify.db.query('SELECT * FROM users WHERE id = $1', [id]),
|
|
156
|
+
create: (data: CreateUserInput) => fastify.db.query(
|
|
157
|
+
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
|
|
158
|
+
[data.name, data.email]
|
|
159
|
+
),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
fastify.decorate('userService', userService);
|
|
163
|
+
}, {
|
|
164
|
+
dependencies: ['database-plugin'],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Use in routes
|
|
168
|
+
app.get('/users/:id', async function (request) {
|
|
169
|
+
const user = await this.userService.findById(request.params.id);
|
|
170
|
+
return user;
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Request Context Pattern
|
|
175
|
+
|
|
176
|
+
Build rich request context:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
interface RequestContext {
|
|
180
|
+
traceId: string;
|
|
181
|
+
user: User | null;
|
|
182
|
+
permissions: Set<string>;
|
|
183
|
+
startTime: number;
|
|
184
|
+
metadata: Map<string, unknown>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
declare module 'fastify' {
|
|
188
|
+
interface FastifyRequest {
|
|
189
|
+
ctx: RequestContext;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
app.decorateRequest('ctx', null);
|
|
194
|
+
|
|
195
|
+
app.addHook('onRequest', async (request) => {
|
|
196
|
+
request.ctx = {
|
|
197
|
+
traceId: request.headers['x-trace-id']?.toString() || crypto.randomUUID(),
|
|
198
|
+
user: null,
|
|
199
|
+
permissions: new Set(),
|
|
200
|
+
startTime: Date.now(),
|
|
201
|
+
metadata: new Map(),
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Auth hook populates user
|
|
206
|
+
app.addHook('preHandler', async (request) => {
|
|
207
|
+
const token = request.headers.authorization;
|
|
208
|
+
if (token) {
|
|
209
|
+
const user = await verifyToken(token);
|
|
210
|
+
request.ctx.user = user;
|
|
211
|
+
request.ctx.permissions = new Set(user.permissions);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Use in handlers
|
|
216
|
+
app.get('/profile', async (request, reply) => {
|
|
217
|
+
if (!request.ctx.user) {
|
|
218
|
+
return reply.code(401).send({ error: 'Unauthorized' });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!request.ctx.permissions.has('read:profile')) {
|
|
222
|
+
return reply.code(403).send({ error: 'Forbidden' });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return request.ctx.user;
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Reply Helpers
|
|
230
|
+
|
|
231
|
+
Create consistent response methods:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
declare module 'fastify' {
|
|
235
|
+
interface FastifyReply {
|
|
236
|
+
ok: (data?: unknown) => void;
|
|
237
|
+
created: (data: unknown) => void;
|
|
238
|
+
noContent: () => void;
|
|
239
|
+
badRequest: (message: string, details?: unknown) => void;
|
|
240
|
+
unauthorized: (message?: string) => void;
|
|
241
|
+
forbidden: (message?: string) => void;
|
|
242
|
+
notFound: (resource?: string) => void;
|
|
243
|
+
conflict: (message: string) => void;
|
|
244
|
+
serverError: (message?: string) => void;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
app.decorateReply('ok', function (data?: unknown) {
|
|
249
|
+
this.code(200).send(data ?? { success: true });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
app.decorateReply('created', function (data: unknown) {
|
|
253
|
+
this.code(201).send(data);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
app.decorateReply('noContent', function () {
|
|
257
|
+
this.code(204).send();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
app.decorateReply('badRequest', function (message: string, details?: unknown) {
|
|
261
|
+
this.code(400).send({
|
|
262
|
+
statusCode: 400,
|
|
263
|
+
error: 'Bad Request',
|
|
264
|
+
message,
|
|
265
|
+
details,
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
app.decorateReply('unauthorized', function (message = 'Authentication required') {
|
|
270
|
+
this.code(401).send({
|
|
271
|
+
statusCode: 401,
|
|
272
|
+
error: 'Unauthorized',
|
|
273
|
+
message,
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
app.decorateReply('notFound', function (resource = 'Resource') {
|
|
278
|
+
this.code(404).send({
|
|
279
|
+
statusCode: 404,
|
|
280
|
+
error: 'Not Found',
|
|
281
|
+
message: `${resource} not found`,
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Usage
|
|
286
|
+
app.get('/users/:id', async (request, reply) => {
|
|
287
|
+
const user = await db.users.findById(request.params.id);
|
|
288
|
+
if (!user) {
|
|
289
|
+
return reply.notFound('User');
|
|
290
|
+
}
|
|
291
|
+
return reply.ok(user);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
app.post('/users', async (request, reply) => {
|
|
295
|
+
const user = await db.users.create(request.body);
|
|
296
|
+
return reply.created(user);
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Checking Decorators
|
|
301
|
+
|
|
302
|
+
Check if decorators exist before using:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// Check at registration time
|
|
306
|
+
app.register(async function (fastify) {
|
|
307
|
+
if (!fastify.hasDecorator('db')) {
|
|
308
|
+
throw new Error('Database decorator required');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!fastify.hasRequestDecorator('user')) {
|
|
312
|
+
throw new Error('User request decorator required');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!fastify.hasReplyDecorator('sendError')) {
|
|
316
|
+
throw new Error('sendError reply decorator required');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Safe to use decorators
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Decorator Encapsulation
|
|
324
|
+
|
|
325
|
+
Decorators respect encapsulation by default:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
app.register(async function pluginA(fastify) {
|
|
329
|
+
fastify.decorate('pluginAUtil', () => 'A');
|
|
330
|
+
|
|
331
|
+
fastify.get('/a', async function () {
|
|
332
|
+
return this.pluginAUtil(); // Works
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
app.register(async function pluginB(fastify) {
|
|
337
|
+
// this.pluginAUtil is NOT available here (encapsulated)
|
|
338
|
+
|
|
339
|
+
fastify.get('/b', async function () {
|
|
340
|
+
// this.pluginAUtil() would be undefined
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Use `fastify-plugin` to share decorators:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import fp from 'fastify-plugin';
|
|
349
|
+
|
|
350
|
+
export default fp(async function sharedDecorator(fastify) {
|
|
351
|
+
fastify.decorate('sharedUtil', () => 'shared');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Now available to parent and sibling plugins
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Functional Decorators
|
|
358
|
+
|
|
359
|
+
Create decorators that return functions:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
declare module 'fastify' {
|
|
363
|
+
interface FastifyInstance {
|
|
364
|
+
createValidator: <T>(schema: object) => (data: unknown) => T;
|
|
365
|
+
createRateLimiter: (options: RateLimitOptions) => RateLimiter;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
app.decorate('createValidator', function <T>(schema: object) {
|
|
370
|
+
const validate = ajv.compile(schema);
|
|
371
|
+
return (data: unknown): T => {
|
|
372
|
+
if (!validate(data)) {
|
|
373
|
+
throw new ValidationError(validate.errors);
|
|
374
|
+
}
|
|
375
|
+
return data as T;
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Usage
|
|
380
|
+
const validateUser = app.createValidator<User>(userSchema);
|
|
381
|
+
|
|
382
|
+
app.post('/users', async (request) => {
|
|
383
|
+
const user = validateUser(request.body);
|
|
384
|
+
return db.users.create(user);
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Async Decorator Initialization
|
|
389
|
+
|
|
390
|
+
Handle async initialization properly:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import fp from 'fastify-plugin';
|
|
394
|
+
|
|
395
|
+
export default fp(async function asyncPlugin(fastify) {
|
|
396
|
+
// Async initialization
|
|
397
|
+
const connection = await createAsyncConnection();
|
|
398
|
+
const cache = await initializeCache();
|
|
399
|
+
|
|
400
|
+
fastify.decorate('asyncService', {
|
|
401
|
+
connection,
|
|
402
|
+
cache,
|
|
403
|
+
query: async (sql: string) => connection.query(sql),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
fastify.addHook('onClose', async () => {
|
|
407
|
+
await connection.close();
|
|
408
|
+
await cache.disconnect();
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Plugin is fully initialized before routes execute
|
|
413
|
+
app.get('/data', async function () {
|
|
414
|
+
return this.asyncService.query('SELECT * FROM data');
|
|
415
|
+
});
|
|
416
|
+
```
|