@flink-app/generic-request-plugin 0.12.1-alpha.9 → 0.13.0
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/CHANGELOG.md +12 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +66 -3
- package/package.json +25 -27
- package/readme.md +582 -18
- package/spec/GenericRequestPlugin.spec.ts +459 -0
- package/spec/helpers/reporter.ts +22 -0
- package/spec/support/jasmine.json +7 -0
- package/src/index.ts +52 -29
- package/tsconfig.dist.json +4 -0
- package/tsconfig.json +5 -3
package/readme.md
CHANGED
|
@@ -1,35 +1,599 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flink-app/generic-request-plugin
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A lightweight Flink plugin that allows you to register custom request handlers with direct access to Express request and response objects. This plugin is useful when you need to bypass Flink's standard handler system and implement custom request handling logic.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Register custom Express route handlers
|
|
8
|
+
- Direct access to Express `req`, `res`, and Flink `app` objects
|
|
9
|
+
- Support for all HTTP methods (GET, POST, PUT, DELETE)
|
|
10
|
+
- Optional authentication and permission validation
|
|
11
|
+
- Bypass Flink's standard validation and response handling
|
|
12
|
+
- Useful for webhooks, custom integrations, and special endpoints
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @flink-app/generic-request-plugin
|
|
11
18
|
```
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
## Usage
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
### Basic Setup
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { FlinkApp } from "@flink-app/flink";
|
|
26
|
+
import { genericRequestPlugin, HttpMethod } from "@flink-app/generic-request-plugin";
|
|
17
27
|
|
|
18
28
|
function start() {
|
|
19
29
|
new FlinkApp<AppContext>({
|
|
20
30
|
name: "My app",
|
|
21
31
|
plugins: [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
32
|
+
genericRequestPlugin({
|
|
33
|
+
path: "/custom-endpoint",
|
|
34
|
+
method: HttpMethod.get,
|
|
35
|
+
handler: (req, res, app) => {
|
|
36
|
+
res.setHeader("Content-Type", "text/html");
|
|
37
|
+
res.end("Hello world!");
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
31
40
|
],
|
|
32
41
|
}).start();
|
|
33
42
|
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuration Options
|
|
46
|
+
|
|
47
|
+
- `path` (required): The URL path for the endpoint (e.g., `/webhook`, `/custom`)
|
|
48
|
+
- `method` (required): HTTP method for the request
|
|
49
|
+
- `handler` (required): Function that handles the request
|
|
50
|
+
- `permissions` (optional): Permission(s) required to access this route (string or array of strings)
|
|
51
|
+
|
|
52
|
+
## HTTP Methods
|
|
53
|
+
|
|
54
|
+
The plugin supports the following HTTP methods via the `HttpMethod` enum:
|
|
55
|
+
|
|
56
|
+
- `HttpMethod.get` - GET requests
|
|
57
|
+
- `HttpMethod.post` - POST requests
|
|
58
|
+
- `HttpMethod.put` - PUT requests
|
|
59
|
+
- `HttpMethod.delete` - DELETE requests
|
|
60
|
+
|
|
61
|
+
## Handler Function
|
|
62
|
+
|
|
63
|
+
The handler function receives three parameters:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
handler: (req, res, app) => void
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `req` - Express Request object
|
|
70
|
+
- `res` - Express Response object
|
|
71
|
+
- `app` - FlinkApp instance (provides access to context, repos, etc.)
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
### Simple GET Endpoint
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
genericRequestPlugin({
|
|
79
|
+
path: "/hello",
|
|
80
|
+
method: HttpMethod.get,
|
|
81
|
+
handler: (req, res, app) => {
|
|
82
|
+
res.json({ message: "Hello from Flink!" });
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### POST Endpoint with JSON Body
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
genericRequestPlugin({
|
|
91
|
+
path: "/webhook",
|
|
92
|
+
method: HttpMethod.post,
|
|
93
|
+
handler: (req, res, app) => {
|
|
94
|
+
const payload = req.body;
|
|
95
|
+
console.log("Received webhook:", payload);
|
|
96
|
+
res.status(200).json({ received: true });
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Endpoint with Database Access
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
genericRequestPlugin({
|
|
105
|
+
path: "/custom-data",
|
|
106
|
+
method: HttpMethod.get,
|
|
107
|
+
handler: async (req, res, app) => {
|
|
108
|
+
try {
|
|
109
|
+
const data = await app.ctx.repos.myRepo.findAll();
|
|
110
|
+
res.json({ data });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
res.status(500).json({ error: "Failed to fetch data" });
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Endpoint with Query Parameters
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
genericRequestPlugin({
|
|
122
|
+
path: "/search",
|
|
123
|
+
method: HttpMethod.get,
|
|
124
|
+
handler: (req, res, app) => {
|
|
125
|
+
const query = req.query.q;
|
|
126
|
+
const results = performSearch(query);
|
|
127
|
+
res.json({ results });
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Endpoint with Path Parameters
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
genericRequestPlugin({
|
|
136
|
+
path: "/user/:id",
|
|
137
|
+
method: HttpMethod.get,
|
|
138
|
+
handler: async (req, res, app) => {
|
|
139
|
+
const userId = req.params.id;
|
|
140
|
+
const user = await app.ctx.repos.userRepo.findById(userId);
|
|
141
|
+
|
|
142
|
+
if (!user) {
|
|
143
|
+
res.status(404).json({ error: "User not found" });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
res.json({ user });
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Custom HTML Response
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
genericRequestPlugin({
|
|
156
|
+
path: "/status",
|
|
157
|
+
method: HttpMethod.get,
|
|
158
|
+
handler: (req, res, app) => {
|
|
159
|
+
const html = `
|
|
160
|
+
<!DOCTYPE html>
|
|
161
|
+
<html>
|
|
162
|
+
<head><title>Status</title></head>
|
|
163
|
+
<body>
|
|
164
|
+
<h1>System Status: OK</h1>
|
|
165
|
+
<p>All systems operational</p>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
`;
|
|
169
|
+
res.setHeader("Content-Type", "text/html");
|
|
170
|
+
res.end(html);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Webhook Handler
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
genericRequestPlugin({
|
|
179
|
+
path: "/webhook/stripe",
|
|
180
|
+
method: HttpMethod.post,
|
|
181
|
+
handler: async (req, res, app) => {
|
|
182
|
+
const signature = req.headers["stripe-signature"];
|
|
183
|
+
const payload = req.body;
|
|
184
|
+
|
|
185
|
+
// Verify webhook signature
|
|
186
|
+
if (!verifyStripeSignature(payload, signature)) {
|
|
187
|
+
res.status(401).json({ error: "Invalid signature" });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Process webhook
|
|
192
|
+
await app.ctx.repos.orderRepo.updatePaymentStatus(payload.data);
|
|
193
|
+
|
|
194
|
+
res.status(200).json({ received: true });
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### File Download Endpoint
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
genericRequestPlugin({
|
|
203
|
+
path: "/download/:fileId",
|
|
204
|
+
method: HttpMethod.get,
|
|
205
|
+
handler: async (req, res, app) => {
|
|
206
|
+
const fileId = req.params.fileId;
|
|
207
|
+
const file = await app.ctx.repos.fileRepo.findById(fileId);
|
|
208
|
+
|
|
209
|
+
if (!file) {
|
|
210
|
+
res.status(404).json({ error: "File not found" });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
res.setHeader("Content-Type", file.mimeType);
|
|
215
|
+
res.setHeader("Content-Disposition", `attachment; filename="${file.name}"`);
|
|
216
|
+
res.end(file.buffer);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Custom Manual Authentication
|
|
222
|
+
|
|
223
|
+
If you need full control over authentication, you can implement it manually in your handler:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
genericRequestPlugin({
|
|
227
|
+
path: "/admin/action",
|
|
228
|
+
method: HttpMethod.post,
|
|
229
|
+
handler: async (req, res, app) => {
|
|
230
|
+
const apiKey = req.headers["x-api-key"];
|
|
231
|
+
|
|
232
|
+
// Custom authentication logic
|
|
233
|
+
if (apiKey !== process.env.ADMIN_API_KEY) {
|
|
234
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Perform admin action
|
|
239
|
+
const result = await performAdminAction(req.body);
|
|
240
|
+
res.json({ success: true, result });
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Authentication and Permissions
|
|
246
|
+
|
|
247
|
+
Starting with version 0.12.1-alpha.46, the generic request plugin supports automatic authentication and permission validation, just like standard Flink handlers.
|
|
248
|
+
|
|
249
|
+
### Prerequisites
|
|
250
|
+
|
|
251
|
+
You must configure an auth plugin (such as `@flink-app/jwt-auth-plugin`) in your FlinkApp:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { FlinkApp } from "@flink-app/flink";
|
|
255
|
+
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
|
|
256
|
+
import { genericRequestPlugin, HttpMethod } from "@flink-app/generic-request-plugin";
|
|
257
|
+
|
|
258
|
+
const app = new FlinkApp<AppContext>({
|
|
259
|
+
name: "My app",
|
|
260
|
+
auth: jwtAuthPlugin({
|
|
261
|
+
secret: process.env.JWT_SECRET!,
|
|
262
|
+
getUser: async (tokenData) => {
|
|
263
|
+
const user = await app.ctx.repos.userRepo.findById(tokenData.userId);
|
|
264
|
+
return {
|
|
265
|
+
id: user._id,
|
|
266
|
+
username: user.username,
|
|
267
|
+
roles: user.roles,
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
rolePermissions: {
|
|
271
|
+
admin: ["read", "write", "delete", "admin:webhooks"],
|
|
272
|
+
user: ["read", "write"],
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
plugins: [
|
|
276
|
+
// Your generic request plugins here
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Basic Authentication
|
|
282
|
+
|
|
283
|
+
Use the `permissions` option to require authentication:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
genericRequestPlugin({
|
|
287
|
+
path: "/webhook/admin",
|
|
288
|
+
method: HttpMethod.post,
|
|
289
|
+
permissions: "*", // Require any authenticated user
|
|
290
|
+
handler: (req, res, app) => {
|
|
291
|
+
// req.user is automatically populated with authenticated user
|
|
292
|
+
console.log(`Webhook called by: ${req.user.username}`);
|
|
293
|
+
res.json({ success: true });
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Single Permission
|
|
299
|
+
|
|
300
|
+
Require a specific permission:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
genericRequestPlugin({
|
|
304
|
+
path: "/admin/action",
|
|
305
|
+
method: HttpMethod.post,
|
|
306
|
+
permissions: "admin:webhooks", // User must have this permission
|
|
307
|
+
handler: (req, res, app) => {
|
|
308
|
+
// Only users with "admin:webhooks" permission can access
|
|
309
|
+
const result = performAdminAction(req.body);
|
|
310
|
+
res.json({ result });
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Multiple Permissions
|
|
316
|
+
|
|
317
|
+
Require multiple permissions (user must have ALL):
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
genericRequestPlugin({
|
|
321
|
+
path: "/sensitive/data",
|
|
322
|
+
method: HttpMethod.get,
|
|
323
|
+
permissions: ["read", "admin:sensitive"], // User needs BOTH permissions
|
|
324
|
+
handler: (req, res, app) => {
|
|
325
|
+
const data = app.ctx.repos.sensitiveRepo.findAll();
|
|
326
|
+
res.json({ data });
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Accessing Authenticated User
|
|
332
|
+
|
|
333
|
+
When permissions are validated, the authenticated user is available via `req.user`:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
genericRequestPlugin({
|
|
337
|
+
path: "/webhook/process",
|
|
338
|
+
method: HttpMethod.post,
|
|
339
|
+
permissions: "webhook:process",
|
|
340
|
+
handler: async (req, res, app) => {
|
|
341
|
+
// Access authenticated user properties
|
|
342
|
+
const userId = req.user.id;
|
|
343
|
+
const username = req.user.username;
|
|
344
|
+
const roles = req.user.roles;
|
|
345
|
+
|
|
346
|
+
// Use user info in your logic
|
|
347
|
+
await app.ctx.repos.auditRepo.create({
|
|
348
|
+
userId,
|
|
349
|
+
action: "webhook_processed",
|
|
350
|
+
payload: req.body,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
res.json({ success: true });
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### How It Works
|
|
359
|
+
|
|
360
|
+
The permission validation works identically to standard Flink handlers:
|
|
361
|
+
|
|
362
|
+
1. **Token extraction**: The auth plugin extracts the JWT token (default: `Authorization: Bearer <token>` header)
|
|
363
|
+
2. **Token validation**: Token is decoded and validated
|
|
364
|
+
3. **Permission check**: User's roles are checked against required permissions
|
|
365
|
+
4. **User attachment**: If successful, user is attached to `req.user`
|
|
366
|
+
5. **Handler execution**: Your handler is only called if authentication succeeds
|
|
367
|
+
6. **Auto 401**: If authentication fails, a 401 response is returned automatically
|
|
368
|
+
|
|
369
|
+
### Advanced: Custom Token Extraction
|
|
370
|
+
|
|
371
|
+
The auth plugin's `tokenExtractor` option works with generic request handlers:
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// In your FlinkApp setup
|
|
375
|
+
auth: jwtAuthPlugin({
|
|
376
|
+
secret: process.env.JWT_SECRET!,
|
|
377
|
+
getUser: async (tokenData) => { /* ... */ },
|
|
378
|
+
rolePermissions: { /* ... */ },
|
|
379
|
+
tokenExtractor: (req) => {
|
|
380
|
+
// Allow query param tokens for webhook routes
|
|
381
|
+
if (req.path?.startsWith('/webhook/')) {
|
|
382
|
+
return req.query?.token as string || null;
|
|
383
|
+
}
|
|
384
|
+
return undefined; // Use default Bearer token
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
|
|
388
|
+
// Your generic request handler
|
|
389
|
+
genericRequestPlugin({
|
|
390
|
+
path: "/webhook/external",
|
|
391
|
+
method: HttpMethod.post,
|
|
392
|
+
permissions: "webhook:external",
|
|
393
|
+
handler: (req, res, app) => {
|
|
394
|
+
// Token can come from query param for this route
|
|
395
|
+
res.json({ success: true });
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Advanced: Dynamic Permissions
|
|
401
|
+
|
|
402
|
+
The auth plugin's `checkPermissions` callback works with generic request handlers:
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
// In your FlinkApp setup
|
|
406
|
+
auth: jwtAuthPlugin({
|
|
407
|
+
secret: process.env.JWT_SECRET!,
|
|
408
|
+
getUser: async (tokenData) => {
|
|
409
|
+
const user = await ctx.repos.userRepo.getById(tokenData.userId);
|
|
410
|
+
const permissions = await ctx.repos.permissionRepo.getUserPermissions(user._id);
|
|
411
|
+
return {
|
|
412
|
+
id: user._id,
|
|
413
|
+
username: user.username,
|
|
414
|
+
permissions, // Attach DB permissions
|
|
415
|
+
};
|
|
416
|
+
},
|
|
417
|
+
rolePermissions: {},
|
|
418
|
+
checkPermissions: async (user, routePermissions) => {
|
|
419
|
+
// Custom permission logic from database
|
|
420
|
+
return routePermissions.every(perm => user.permissions?.includes(perm));
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
|
|
424
|
+
// Your generic request handler
|
|
425
|
+
genericRequestPlugin({
|
|
426
|
+
path: "/custom/action",
|
|
427
|
+
method: HttpMethod.post,
|
|
428
|
+
permissions: ["custom:action", "premium:feature"],
|
|
429
|
+
handler: (req, res, app) => {
|
|
430
|
+
// Permissions validated against database
|
|
431
|
+
res.json({ success: true });
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Authentication Error Response
|
|
437
|
+
|
|
438
|
+
If authentication fails, the plugin automatically returns:
|
|
439
|
+
|
|
440
|
+
```json
|
|
441
|
+
{
|
|
442
|
+
"status": 401,
|
|
443
|
+
"error": {
|
|
444
|
+
"title": "Unauthorized",
|
|
445
|
+
"detail": "Authentication required or insufficient permissions"
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Without Permissions
|
|
451
|
+
|
|
452
|
+
If you don't specify `permissions`, the route is public (no authentication required):
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
genericRequestPlugin({
|
|
456
|
+
path: "/public/webhook",
|
|
457
|
+
method: HttpMethod.post,
|
|
458
|
+
// No permissions = public route
|
|
459
|
+
handler: (req, res, app) => {
|
|
460
|
+
// Anyone can access this
|
|
461
|
+
res.json({ received: true });
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## Multiple Endpoints
|
|
467
|
+
|
|
468
|
+
You can register multiple generic request handlers by including multiple plugin instances:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
new FlinkApp<AppContext>({
|
|
472
|
+
name: "My app",
|
|
473
|
+
plugins: [
|
|
474
|
+
genericRequestPlugin({
|
|
475
|
+
path: "/webhook/stripe",
|
|
476
|
+
method: HttpMethod.post,
|
|
477
|
+
handler: stripeWebhookHandler,
|
|
478
|
+
}),
|
|
479
|
+
genericRequestPlugin({
|
|
480
|
+
path: "/webhook/github",
|
|
481
|
+
method: HttpMethod.post,
|
|
482
|
+
handler: githubWebhookHandler,
|
|
483
|
+
}),
|
|
484
|
+
genericRequestPlugin({
|
|
485
|
+
path: "/status",
|
|
486
|
+
method: HttpMethod.get,
|
|
487
|
+
handler: statusHandler,
|
|
488
|
+
}),
|
|
489
|
+
],
|
|
490
|
+
}).start();
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## When to Use This Plugin
|
|
494
|
+
|
|
495
|
+
Use the generic request plugin when you need to:
|
|
496
|
+
|
|
497
|
+
- Handle webhooks from external services
|
|
498
|
+
- Return non-JSON responses (HTML, XML, plain text, binary files)
|
|
499
|
+
- Bypass Flink's request validation
|
|
500
|
+
- Access low-level Express request/response objects
|
|
501
|
+
- Implement server-sent events (SSE)
|
|
502
|
+
- Handle file uploads/downloads with custom logic
|
|
503
|
+
- Integrate with third-party services requiring specific response formats
|
|
504
|
+
- Require authentication but need custom response handling
|
|
505
|
+
|
|
506
|
+
## When NOT to Use This Plugin
|
|
507
|
+
|
|
508
|
+
For standard API endpoints, use Flink's built-in handler system instead:
|
|
509
|
+
|
|
510
|
+
- Standard CRUD operations - Use `FlinkHttpHandler` with GET/POST/PUT/DELETE
|
|
511
|
+
- Endpoints requiring validation - Use Flink's schema validation
|
|
512
|
+
- Type-safe endpoints - Use Flink's typed handlers
|
|
513
|
+
- Auto-documented endpoints - Use standard handlers with the API docs plugin
|
|
514
|
+
|
|
515
|
+
## Access to FlinkApp Context
|
|
516
|
+
|
|
517
|
+
The handler receives the full FlinkApp instance, giving you access to:
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
handler: (req, res, app) => {
|
|
521
|
+
// Access repositories
|
|
522
|
+
const data = await app.ctx.repos.myRepo.findAll();
|
|
523
|
+
|
|
524
|
+
// Access plugins
|
|
525
|
+
const stripeAPI = app.ctx.plugins.stripePlugin.stripeAPI;
|
|
526
|
+
|
|
527
|
+
// Access database
|
|
528
|
+
const collection = app.db.collection("myCollection");
|
|
529
|
+
|
|
530
|
+
// Access any custom context properties
|
|
531
|
+
const config = app.ctx.config;
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## Error Handling
|
|
536
|
+
|
|
537
|
+
Always implement proper error handling in your custom handlers:
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
genericRequestPlugin({
|
|
541
|
+
path: "/custom",
|
|
542
|
+
method: HttpMethod.post,
|
|
543
|
+
handler: async (req, res, app) => {
|
|
544
|
+
try {
|
|
545
|
+
// Your logic here
|
|
546
|
+
const result = await performOperation(req.body);
|
|
547
|
+
res.json({ success: true, result });
|
|
548
|
+
} catch (error) {
|
|
549
|
+
console.error("Error in custom handler:", error);
|
|
550
|
+
res.status(500).json({
|
|
551
|
+
error: "Internal server error",
|
|
552
|
+
message: error.message
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
```
|
|
34
558
|
|
|
559
|
+
## TypeScript Support
|
|
560
|
+
|
|
561
|
+
The plugin includes TypeScript definitions. For better type safety, you can type your handlers:
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
import { Request, Response } from "express";
|
|
565
|
+
import { FlinkApp } from "@flink-app/flink";
|
|
566
|
+
|
|
567
|
+
const myHandler = async (
|
|
568
|
+
req: Request,
|
|
569
|
+
res: Response,
|
|
570
|
+
app: FlinkApp<AppContext>
|
|
571
|
+
) => {
|
|
572
|
+
// Fully typed handler
|
|
573
|
+
const userId: string = req.params.id;
|
|
574
|
+
const user = await app.ctx.repos.userRepo.findById(userId);
|
|
575
|
+
res.json({ user });
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
genericRequestPlugin({
|
|
579
|
+
path: "/user/:id",
|
|
580
|
+
method: HttpMethod.get,
|
|
581
|
+
handler: myHandler,
|
|
582
|
+
});
|
|
35
583
|
```
|
|
584
|
+
|
|
585
|
+
## Security Considerations
|
|
586
|
+
|
|
587
|
+
- Always validate and sanitize input data
|
|
588
|
+
- Use the `permissions` option for routes requiring authentication
|
|
589
|
+
- For manual authentication, implement it carefully and consistently
|
|
590
|
+
- Be careful with direct database access
|
|
591
|
+
- Validate webhook signatures for external integrations
|
|
592
|
+
- Use HTTPS in production
|
|
593
|
+
- Rate limit sensitive endpoints
|
|
594
|
+
- Never expose sensitive information in error messages
|
|
595
|
+
- Remember: without `permissions` set, your route is publicly accessible
|
|
596
|
+
|
|
597
|
+
## License
|
|
598
|
+
|
|
599
|
+
MIT
|