@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,320 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plugins
|
|
3
|
+
description: Plugin development and encapsulation in Fastify
|
|
4
|
+
metadata:
|
|
5
|
+
tags: plugins, encapsulation, modules, architecture
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Plugin Development and Encapsulation
|
|
9
|
+
|
|
10
|
+
## Understanding Encapsulation
|
|
11
|
+
|
|
12
|
+
Fastify's plugin system provides automatic encapsulation. Each plugin creates its own context, isolating decorators, hooks, and plugins registered within it:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import Fastify from 'fastify';
|
|
16
|
+
import fp from 'fastify-plugin';
|
|
17
|
+
|
|
18
|
+
const app = Fastify();
|
|
19
|
+
|
|
20
|
+
// This plugin is encapsulated - its decorators are NOT available to siblings
|
|
21
|
+
app.register(async function childPlugin(fastify) {
|
|
22
|
+
fastify.decorate('privateUtil', () => 'only available here');
|
|
23
|
+
|
|
24
|
+
// This decorator is only available within this plugin and its children
|
|
25
|
+
fastify.get('/child', async function (request, reply) {
|
|
26
|
+
return this.privateUtil();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// This route CANNOT access privateUtil - it's in a different context
|
|
31
|
+
app.get('/parent', async function (request, reply) {
|
|
32
|
+
// this.privateUtil is undefined here
|
|
33
|
+
return { status: 'ok' };
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Breaking Encapsulation with fastify-plugin
|
|
38
|
+
|
|
39
|
+
Use `fastify-plugin` when you need to share decorators, hooks, or plugins with the parent context:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import fp from 'fastify-plugin';
|
|
43
|
+
|
|
44
|
+
// This plugin's decorators will be available to the parent and siblings
|
|
45
|
+
export default fp(async function databasePlugin(fastify, options) {
|
|
46
|
+
const db = await createConnection(options.connectionString);
|
|
47
|
+
|
|
48
|
+
fastify.decorate('db', db);
|
|
49
|
+
|
|
50
|
+
fastify.addHook('onClose', async () => {
|
|
51
|
+
await db.close();
|
|
52
|
+
});
|
|
53
|
+
}, {
|
|
54
|
+
name: 'database-plugin',
|
|
55
|
+
dependencies: [], // List plugin dependencies
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Plugin Registration Order
|
|
60
|
+
|
|
61
|
+
Plugins are registered in order, but loading is asynchronous. Use `after()` for sequential dependencies:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import Fastify from 'fastify';
|
|
65
|
+
import databasePlugin from './plugins/database.js';
|
|
66
|
+
import authPlugin from './plugins/auth.js';
|
|
67
|
+
import routesPlugin from './routes/index.js';
|
|
68
|
+
|
|
69
|
+
const app = Fastify();
|
|
70
|
+
|
|
71
|
+
// Database must be ready before auth
|
|
72
|
+
app.register(databasePlugin);
|
|
73
|
+
|
|
74
|
+
// Auth depends on database
|
|
75
|
+
app.register(authPlugin);
|
|
76
|
+
|
|
77
|
+
// Routes depend on both
|
|
78
|
+
app.register(routesPlugin);
|
|
79
|
+
|
|
80
|
+
// Or use after() for explicit sequencing
|
|
81
|
+
app.register(databasePlugin).after(() => {
|
|
82
|
+
app.register(authPlugin).after(() => {
|
|
83
|
+
app.register(routesPlugin);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await app.ready();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Plugin Options
|
|
91
|
+
|
|
92
|
+
Always validate and document plugin options:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import fp from 'fastify-plugin';
|
|
96
|
+
|
|
97
|
+
interface CachePluginOptions {
|
|
98
|
+
ttl: number;
|
|
99
|
+
maxSize?: number;
|
|
100
|
+
prefix?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default fp<CachePluginOptions>(async function cachePlugin(fastify, options) {
|
|
104
|
+
const { ttl, maxSize = 1000, prefix = 'cache:' } = options;
|
|
105
|
+
|
|
106
|
+
if (typeof ttl !== 'number' || ttl <= 0) {
|
|
107
|
+
throw new Error('Cache plugin requires a positive ttl option');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const cache = new Map<string, { value: unknown; expires: number }>();
|
|
111
|
+
|
|
112
|
+
fastify.decorate('cache', {
|
|
113
|
+
get(key: string): unknown | undefined {
|
|
114
|
+
const item = cache.get(prefix + key);
|
|
115
|
+
if (!item) return undefined;
|
|
116
|
+
if (Date.now() > item.expires) {
|
|
117
|
+
cache.delete(prefix + key);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return item.value;
|
|
121
|
+
},
|
|
122
|
+
set(key: string, value: unknown): void {
|
|
123
|
+
if (cache.size >= maxSize) {
|
|
124
|
+
const firstKey = cache.keys().next().value;
|
|
125
|
+
cache.delete(firstKey);
|
|
126
|
+
}
|
|
127
|
+
cache.set(prefix + key, { value, expires: Date.now() + ttl });
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}, {
|
|
131
|
+
name: 'cache-plugin',
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Plugin Factory Pattern
|
|
136
|
+
|
|
137
|
+
Create configurable plugins using factory functions:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import fp from 'fastify-plugin';
|
|
141
|
+
|
|
142
|
+
interface RateLimitOptions {
|
|
143
|
+
max: number;
|
|
144
|
+
timeWindow: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function createRateLimiter(defaults: Partial<RateLimitOptions> = {}) {
|
|
148
|
+
return fp<RateLimitOptions>(async function rateLimitPlugin(fastify, options) {
|
|
149
|
+
const config = { ...defaults, ...options };
|
|
150
|
+
|
|
151
|
+
// Implementation
|
|
152
|
+
fastify.decorate('rateLimit', config);
|
|
153
|
+
}, {
|
|
154
|
+
name: 'rate-limiter',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Usage
|
|
159
|
+
app.register(createRateLimiter({ max: 100 }), { timeWindow: 60000 });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Plugin Dependencies
|
|
163
|
+
|
|
164
|
+
Declare dependencies to ensure proper load order:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import fp from 'fastify-plugin';
|
|
168
|
+
|
|
169
|
+
export default fp(async function authPlugin(fastify) {
|
|
170
|
+
// This plugin requires 'database-plugin' to be loaded first
|
|
171
|
+
if (!fastify.hasDecorator('db')) {
|
|
172
|
+
throw new Error('Auth plugin requires database plugin');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fastify.decorate('authenticate', async (request) => {
|
|
176
|
+
const user = await fastify.db.users.findByToken(request.headers.authorization);
|
|
177
|
+
return user;
|
|
178
|
+
});
|
|
179
|
+
}, {
|
|
180
|
+
name: 'auth-plugin',
|
|
181
|
+
dependencies: ['database-plugin'],
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Scoped Plugins for Route Groups
|
|
186
|
+
|
|
187
|
+
Use encapsulation to scope plugins to specific routes:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import Fastify from 'fastify';
|
|
191
|
+
|
|
192
|
+
const app = Fastify();
|
|
193
|
+
|
|
194
|
+
// Public routes - no auth required
|
|
195
|
+
app.register(async function publicRoutes(fastify) {
|
|
196
|
+
fastify.get('/health', async () => ({ status: 'ok' }));
|
|
197
|
+
fastify.get('/docs', async () => ({ version: '1.0.0' }));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Protected routes - auth required
|
|
201
|
+
app.register(async function protectedRoutes(fastify) {
|
|
202
|
+
// Auth hook only applies to routes in this plugin
|
|
203
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
204
|
+
const token = request.headers.authorization;
|
|
205
|
+
if (!token) {
|
|
206
|
+
reply.code(401).send({ error: 'Unauthorized' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
request.user = await verifyToken(token);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
fastify.get('/profile', async (request) => {
|
|
213
|
+
return { user: request.user };
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
fastify.get('/settings', async (request) => {
|
|
217
|
+
return { settings: await getSettings(request.user.id) };
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Prefix Routes with Register
|
|
223
|
+
|
|
224
|
+
Use the `prefix` option to namespace routes:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
app.register(import('./routes/users.js'), { prefix: '/api/v1/users' });
|
|
228
|
+
app.register(import('./routes/posts.js'), { prefix: '/api/v1/posts' });
|
|
229
|
+
|
|
230
|
+
// In routes/users.js
|
|
231
|
+
export default async function userRoutes(fastify) {
|
|
232
|
+
// Becomes /api/v1/users
|
|
233
|
+
fastify.get('/', async () => {
|
|
234
|
+
return { users: [] };
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Becomes /api/v1/users/:id
|
|
238
|
+
fastify.get('/:id', async (request) => {
|
|
239
|
+
return { user: { id: request.params.id } };
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Plugin Metadata
|
|
245
|
+
|
|
246
|
+
Add metadata for documentation and tooling:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import fp from 'fastify-plugin';
|
|
250
|
+
|
|
251
|
+
async function metricsPlugin(fastify) {
|
|
252
|
+
// Implementation
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export default fp(metricsPlugin, {
|
|
256
|
+
name: 'metrics-plugin',
|
|
257
|
+
fastify: '5.x', // Fastify version compatibility
|
|
258
|
+
dependencies: ['pino-plugin'],
|
|
259
|
+
decorators: {
|
|
260
|
+
fastify: ['db'], // Required decorators
|
|
261
|
+
request: [],
|
|
262
|
+
reply: [],
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Autoload Plugins
|
|
268
|
+
|
|
269
|
+
Use `@fastify/autoload` for automatic plugin loading:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import Fastify from 'fastify';
|
|
273
|
+
import autoload from '@fastify/autoload';
|
|
274
|
+
import { fileURLToPath } from 'node:url';
|
|
275
|
+
import { dirname, join } from 'node:path';
|
|
276
|
+
|
|
277
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
278
|
+
|
|
279
|
+
const app = Fastify();
|
|
280
|
+
|
|
281
|
+
// Load all plugins from the plugins directory
|
|
282
|
+
app.register(autoload, {
|
|
283
|
+
dir: join(__dirname, 'plugins'),
|
|
284
|
+
options: { prefix: '/api' },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Load all routes from the routes directory
|
|
288
|
+
app.register(autoload, {
|
|
289
|
+
dir: join(__dirname, 'routes'),
|
|
290
|
+
options: { prefix: '/api' },
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Testing Plugins in Isolation
|
|
295
|
+
|
|
296
|
+
Test plugins independently:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { describe, it, before, after } from 'node:test';
|
|
300
|
+
import Fastify from 'fastify';
|
|
301
|
+
import myPlugin from './my-plugin.js';
|
|
302
|
+
|
|
303
|
+
describe('MyPlugin', () => {
|
|
304
|
+
let app;
|
|
305
|
+
|
|
306
|
+
before(async () => {
|
|
307
|
+
app = Fastify();
|
|
308
|
+
app.register(myPlugin, { option: 'value' });
|
|
309
|
+
await app.ready();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
after(async () => {
|
|
313
|
+
await app.close();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should decorate fastify instance', (t) => {
|
|
317
|
+
t.assert.ok(app.hasDecorator('myDecorator'));
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
```
|