@flink-app/generic-request-plugin 0.12.1-alpha.7 → 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/readme.md CHANGED
@@ -1,35 +1,599 @@
1
- # Flink API Docs
1
+ # @flink-app/generic-request-plugin
2
2
 
3
- A FLINK plugin that makes it possible to overide the default request handler and handle a request by your own code.
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
- ## Usage
5
+ ## Features
6
6
 
7
- Install plugin to your flink app project:
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
- npm i -S @flink-app/generic-request-plugin
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @flink-app/generic-request-plugin
11
18
  ```
12
19
 
13
- Add and configure plugin in your app startup (probable the `index.ts` in root project):
20
+ ## Usage
14
21
 
15
- ```
16
- import { genericRequestPlugin, HttpMethod} from "@flink-app/generic-request-plugin";
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
- // Register plugin
23
- genericRequestPlugin({
24
- "path" : "/url",
25
- "method" : HttpMethod.get,
26
- "handler" : (req, res, app) => {
27
- res.setHeader('Content-type','text/html');
28
- res.end("Hello world!")
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