@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/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
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { FlinkApp, FlinkAuthPlugin, FlinkRequest } from "@flink-app/flink";
|
|
2
|
+
import { genericRequestPlugin, HttpMethod } from "../src/index";
|
|
3
|
+
|
|
4
|
+
describe("GenericRequestPlugin", () => {
|
|
5
|
+
let mockApp: any;
|
|
6
|
+
let mockExpressApp: any;
|
|
7
|
+
let mockAuth: FlinkAuthPlugin;
|
|
8
|
+
let registeredRoutes: Map<string, Function>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
registeredRoutes = new Map();
|
|
12
|
+
|
|
13
|
+
// Mock Express app
|
|
14
|
+
mockExpressApp = {
|
|
15
|
+
get: jasmine.createSpy("get").and.callFake((path: string, handler: Function) => {
|
|
16
|
+
registeredRoutes.set(`GET:${path}`, handler);
|
|
17
|
+
}),
|
|
18
|
+
post: jasmine.createSpy("post").and.callFake((path: string, handler: Function) => {
|
|
19
|
+
registeredRoutes.set(`POST:${path}`, handler);
|
|
20
|
+
}),
|
|
21
|
+
put: jasmine.createSpy("put").and.callFake((path: string, handler: Function) => {
|
|
22
|
+
registeredRoutes.set(`PUT:${path}`, handler);
|
|
23
|
+
}),
|
|
24
|
+
delete: jasmine.createSpy("delete").and.callFake((path: string, handler: Function) => {
|
|
25
|
+
registeredRoutes.set(`DELETE:${path}`, handler);
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Mock auth plugin
|
|
30
|
+
mockAuth = {
|
|
31
|
+
authenticateRequest: jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(true)),
|
|
32
|
+
createToken: jasmine.createSpy("createToken").and.returnValue(Promise.resolve("token")),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Mock FlinkApp
|
|
36
|
+
mockApp = {
|
|
37
|
+
expressApp: mockExpressApp,
|
|
38
|
+
auth: mockAuth,
|
|
39
|
+
} as unknown as FlinkApp<any>;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("Plugin Registration", () => {
|
|
43
|
+
it("should create a valid plugin", () => {
|
|
44
|
+
const plugin = genericRequestPlugin({
|
|
45
|
+
path: "/test",
|
|
46
|
+
method: HttpMethod.get,
|
|
47
|
+
handler: () => {},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(plugin).toBeDefined();
|
|
51
|
+
expect(plugin.id).toBe("genericRequestPlugin");
|
|
52
|
+
expect(plugin.init).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should throw error if Express app is not initialized", () => {
|
|
56
|
+
const appWithoutExpress = { expressApp: null } as unknown as FlinkApp<any>;
|
|
57
|
+
const plugin = genericRequestPlugin({
|
|
58
|
+
path: "/test",
|
|
59
|
+
method: HttpMethod.get,
|
|
60
|
+
handler: () => {},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(() => plugin.init(appWithoutExpress)).toThrowError("Express app not initialized");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should register GET route", () => {
|
|
67
|
+
const plugin = genericRequestPlugin({
|
|
68
|
+
path: "/test",
|
|
69
|
+
method: HttpMethod.get,
|
|
70
|
+
handler: () => {},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
plugin.init(mockApp);
|
|
74
|
+
|
|
75
|
+
expect(mockExpressApp.get).toHaveBeenCalledWith("/test", jasmine.any(Function));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should register POST route", () => {
|
|
79
|
+
const plugin = genericRequestPlugin({
|
|
80
|
+
path: "/webhook",
|
|
81
|
+
method: HttpMethod.post,
|
|
82
|
+
handler: () => {},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
plugin.init(mockApp);
|
|
86
|
+
|
|
87
|
+
expect(mockExpressApp.post).toHaveBeenCalledWith("/webhook", jasmine.any(Function));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should register PUT route", () => {
|
|
91
|
+
const plugin = genericRequestPlugin({
|
|
92
|
+
path: "/update",
|
|
93
|
+
method: HttpMethod.put,
|
|
94
|
+
handler: () => {},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
plugin.init(mockApp);
|
|
98
|
+
|
|
99
|
+
expect(mockExpressApp.put).toHaveBeenCalledWith("/update", jasmine.any(Function));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should register DELETE route", () => {
|
|
103
|
+
const plugin = genericRequestPlugin({
|
|
104
|
+
path: "/remove",
|
|
105
|
+
method: HttpMethod.delete,
|
|
106
|
+
handler: () => {},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
plugin.init(mockApp);
|
|
110
|
+
|
|
111
|
+
expect(mockExpressApp.delete).toHaveBeenCalledWith("/remove", jasmine.any(Function));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("Handler Execution - No Permissions", () => {
|
|
116
|
+
it("should call handler directly when no permissions are set", async () => {
|
|
117
|
+
const handlerSpy = jasmine.createSpy("handler");
|
|
118
|
+
const plugin = genericRequestPlugin({
|
|
119
|
+
path: "/public",
|
|
120
|
+
method: HttpMethod.get,
|
|
121
|
+
handler: handlerSpy,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
plugin.init(mockApp);
|
|
125
|
+
|
|
126
|
+
const routeHandler = registeredRoutes.get("GET:/public");
|
|
127
|
+
if (!routeHandler) {
|
|
128
|
+
fail("Route handler not registered");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const mockReq = {} as any;
|
|
133
|
+
const mockRes = {} as any;
|
|
134
|
+
|
|
135
|
+
await routeHandler(mockReq, mockRes);
|
|
136
|
+
|
|
137
|
+
expect(handlerSpy).toHaveBeenCalledWith(mockReq, mockRes, mockApp);
|
|
138
|
+
expect(mockAuth.authenticateRequest).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should pass req, res, and app to handler", async () => {
|
|
142
|
+
let capturedReq: any;
|
|
143
|
+
let capturedRes: any;
|
|
144
|
+
let capturedApp: any;
|
|
145
|
+
|
|
146
|
+
const handler = (req: any, res: any, app: FlinkApp<any>) => {
|
|
147
|
+
capturedReq = req;
|
|
148
|
+
capturedRes = res;
|
|
149
|
+
capturedApp = app;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const plugin = genericRequestPlugin({
|
|
153
|
+
path: "/test",
|
|
154
|
+
method: HttpMethod.get,
|
|
155
|
+
handler,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
plugin.init(mockApp);
|
|
159
|
+
|
|
160
|
+
const routeHandler = registeredRoutes.get("GET:/test");
|
|
161
|
+
if (!routeHandler) {
|
|
162
|
+
fail("Route handler not registered");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const mockReq = { path: "/test" } as any;
|
|
167
|
+
const mockRes = { status: () => ({ json: () => {} }) } as any;
|
|
168
|
+
|
|
169
|
+
await routeHandler(mockReq, mockRes);
|
|
170
|
+
|
|
171
|
+
expect(capturedReq).toBe(mockReq);
|
|
172
|
+
expect(capturedRes).toBe(mockRes);
|
|
173
|
+
expect(capturedApp).toBe(mockApp);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("Permission Validation", () => {
|
|
178
|
+
it("should validate permissions when set", async () => {
|
|
179
|
+
const handlerSpy = jasmine.createSpy("handler");
|
|
180
|
+
const plugin = genericRequestPlugin({
|
|
181
|
+
path: "/protected",
|
|
182
|
+
method: HttpMethod.get,
|
|
183
|
+
permissions: "read",
|
|
184
|
+
handler: handlerSpy,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
plugin.init(mockApp);
|
|
188
|
+
|
|
189
|
+
const routeHandler = registeredRoutes.get("GET:/protected");
|
|
190
|
+
if (!routeHandler) {
|
|
191
|
+
fail("Route handler not registered");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const mockReq = {} as FlinkRequest;
|
|
196
|
+
const mockRes = {} as any;
|
|
197
|
+
|
|
198
|
+
await routeHandler(mockReq, mockRes);
|
|
199
|
+
|
|
200
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "read");
|
|
201
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should validate multiple permissions", async () => {
|
|
205
|
+
const handlerSpy = jasmine.createSpy("handler");
|
|
206
|
+
const plugin = genericRequestPlugin({
|
|
207
|
+
path: "/admin",
|
|
208
|
+
method: HttpMethod.post,
|
|
209
|
+
permissions: ["read", "write", "admin"],
|
|
210
|
+
handler: handlerSpy,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
plugin.init(mockApp);
|
|
214
|
+
|
|
215
|
+
const routeHandler = registeredRoutes.get("POST:/admin");
|
|
216
|
+
if (!routeHandler) {
|
|
217
|
+
fail("Route handler not registered");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const mockReq = {} as FlinkRequest;
|
|
222
|
+
const mockRes = {} as any;
|
|
223
|
+
|
|
224
|
+
await routeHandler(mockReq, mockRes);
|
|
225
|
+
|
|
226
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, ["read", "write", "admin"]);
|
|
227
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should return 401 when authentication fails", async () => {
|
|
231
|
+
mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(false));
|
|
232
|
+
|
|
233
|
+
const handlerSpy = jasmine.createSpy("handler");
|
|
234
|
+
const plugin = genericRequestPlugin({
|
|
235
|
+
path: "/protected",
|
|
236
|
+
method: HttpMethod.get,
|
|
237
|
+
permissions: "read",
|
|
238
|
+
handler: handlerSpy,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
plugin.init(mockApp);
|
|
242
|
+
|
|
243
|
+
const routeHandler = registeredRoutes.get("GET:/protected");
|
|
244
|
+
if (!routeHandler) {
|
|
245
|
+
fail("Route handler not registered");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const mockReq = {} as FlinkRequest;
|
|
250
|
+
const mockJsonSpy = jasmine.createSpy("json");
|
|
251
|
+
const mockRes = {
|
|
252
|
+
status: jasmine.createSpy("status").and.returnValue({ json: mockJsonSpy }),
|
|
253
|
+
} as any;
|
|
254
|
+
|
|
255
|
+
await routeHandler(mockReq, mockRes);
|
|
256
|
+
|
|
257
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "read");
|
|
258
|
+
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
259
|
+
expect(mockJsonSpy).toHaveBeenCalledWith({
|
|
260
|
+
status: 401,
|
|
261
|
+
error: {
|
|
262
|
+
title: "Unauthorized",
|
|
263
|
+
detail: "Authentication required or insufficient permissions",
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
expect(handlerSpy).not.toHaveBeenCalled();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should not call handler when authentication fails", async () => {
|
|
270
|
+
mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(false));
|
|
271
|
+
|
|
272
|
+
const handlerSpy = jasmine.createSpy("handler");
|
|
273
|
+
const plugin = genericRequestPlugin({
|
|
274
|
+
path: "/protected",
|
|
275
|
+
method: HttpMethod.post,
|
|
276
|
+
permissions: "write",
|
|
277
|
+
handler: handlerSpy,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
plugin.init(mockApp);
|
|
281
|
+
|
|
282
|
+
const routeHandler = registeredRoutes.get("POST:/protected");
|
|
283
|
+
if (!routeHandler) {
|
|
284
|
+
fail("Route handler not registered");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const mockReq = {} as FlinkRequest;
|
|
289
|
+
const mockRes = {
|
|
290
|
+
status: () => ({ json: () => {} }),
|
|
291
|
+
} as any;
|
|
292
|
+
|
|
293
|
+
await routeHandler(mockReq, mockRes);
|
|
294
|
+
|
|
295
|
+
expect(handlerSpy).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should throw error if permissions are set but no auth plugin is configured", async () => {
|
|
299
|
+
const appWithoutAuth = {
|
|
300
|
+
expressApp: mockExpressApp,
|
|
301
|
+
auth: null,
|
|
302
|
+
} as unknown as FlinkApp<any>;
|
|
303
|
+
|
|
304
|
+
const plugin = genericRequestPlugin({
|
|
305
|
+
path: "/protected",
|
|
306
|
+
method: HttpMethod.get,
|
|
307
|
+
permissions: "read",
|
|
308
|
+
handler: () => {},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
plugin.init(appWithoutAuth);
|
|
312
|
+
|
|
313
|
+
const routeHandler = registeredRoutes.get("GET:/protected");
|
|
314
|
+
if (!routeHandler) {
|
|
315
|
+
fail("Route handler not registered");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const mockReq = {} as FlinkRequest;
|
|
320
|
+
const mockRes = {} as any;
|
|
321
|
+
|
|
322
|
+
await expectAsync(routeHandler(mockReq, mockRes)).toBeRejectedWithError(
|
|
323
|
+
"Route GET /protected requires permissions but no auth plugin is configured"
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should call handler after successful authentication", async () => {
|
|
328
|
+
mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.callFake((req: FlinkRequest) => {
|
|
329
|
+
req.user = { id: "123", username: "testuser" };
|
|
330
|
+
return Promise.resolve(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const handlerSpy = jasmine.createSpy("handler");
|
|
334
|
+
const plugin = genericRequestPlugin({
|
|
335
|
+
path: "/protected",
|
|
336
|
+
method: HttpMethod.get,
|
|
337
|
+
permissions: "read",
|
|
338
|
+
handler: handlerSpy,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
plugin.init(mockApp);
|
|
342
|
+
|
|
343
|
+
const routeHandler = registeredRoutes.get("GET:/protected");
|
|
344
|
+
if (!routeHandler) {
|
|
345
|
+
fail("Route handler not registered");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const mockReq = {} as FlinkRequest;
|
|
350
|
+
const mockRes = {} as any;
|
|
351
|
+
|
|
352
|
+
await routeHandler(mockReq, mockRes);
|
|
353
|
+
|
|
354
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalled();
|
|
355
|
+
expect(mockReq.user).toEqual({ id: "123", username: "testuser" });
|
|
356
|
+
expect(handlerSpy).toHaveBeenCalledWith(mockReq, mockRes, mockApp);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe("Wildcard Permission", () => {
|
|
361
|
+
it("should validate wildcard permission for any authenticated user", async () => {
|
|
362
|
+
const handlerSpy = jasmine.createSpy("handler");
|
|
363
|
+
const plugin = genericRequestPlugin({
|
|
364
|
+
path: "/authenticated",
|
|
365
|
+
method: HttpMethod.get,
|
|
366
|
+
permissions: "*",
|
|
367
|
+
handler: handlerSpy,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
plugin.init(mockApp);
|
|
371
|
+
|
|
372
|
+
const routeHandler = registeredRoutes.get("GET:/authenticated");
|
|
373
|
+
if (!routeHandler) {
|
|
374
|
+
fail("Route handler not registered");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const mockReq = {} as FlinkRequest;
|
|
379
|
+
const mockRes = {} as any;
|
|
380
|
+
|
|
381
|
+
await routeHandler(mockReq, mockRes);
|
|
382
|
+
|
|
383
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "*");
|
|
384
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe("Real-world Scenarios", () => {
|
|
389
|
+
it("should handle webhook with permissions", async () => {
|
|
390
|
+
let webhookData: any;
|
|
391
|
+
|
|
392
|
+
const webhookHandler = (req: any, res: any) => {
|
|
393
|
+
webhookData = req.body;
|
|
394
|
+
res.json({ received: true });
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const plugin = genericRequestPlugin({
|
|
398
|
+
path: "/webhook/stripe",
|
|
399
|
+
method: HttpMethod.post,
|
|
400
|
+
permissions: "webhook:stripe",
|
|
401
|
+
handler: webhookHandler,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
plugin.init(mockApp);
|
|
405
|
+
|
|
406
|
+
const routeHandler = registeredRoutes.get("POST:/webhook/stripe");
|
|
407
|
+
if (!routeHandler) {
|
|
408
|
+
fail("Route handler not registered");
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const mockReq = { body: { event: "payment.success" } } as any;
|
|
413
|
+
const mockJsonSpy = jasmine.createSpy("json");
|
|
414
|
+
const mockRes = { json: mockJsonSpy } as any;
|
|
415
|
+
|
|
416
|
+
await routeHandler(mockReq, mockRes);
|
|
417
|
+
|
|
418
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "webhook:stripe");
|
|
419
|
+
expect(webhookData).toEqual({ event: "payment.success" });
|
|
420
|
+
expect(mockJsonSpy).toHaveBeenCalledWith({ received: true });
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("should handle file download with permissions", async () => {
|
|
424
|
+
const fileHandler = (req: any, res: any) => {
|
|
425
|
+
res.setHeader("Content-Type", "application/pdf");
|
|
426
|
+
res.end("file-data");
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const plugin = genericRequestPlugin({
|
|
430
|
+
path: "/download/:fileId",
|
|
431
|
+
method: HttpMethod.get,
|
|
432
|
+
permissions: "file:download",
|
|
433
|
+
handler: fileHandler,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
plugin.init(mockApp);
|
|
437
|
+
|
|
438
|
+
const routeHandler = registeredRoutes.get("GET:/download/:fileId");
|
|
439
|
+
if (!routeHandler) {
|
|
440
|
+
fail("Route handler not registered");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const mockReq = { params: { fileId: "123" } } as any;
|
|
445
|
+
const mockSetHeaderSpy = jasmine.createSpy("setHeader");
|
|
446
|
+
const mockEndSpy = jasmine.createSpy("end");
|
|
447
|
+
const mockRes = {
|
|
448
|
+
setHeader: mockSetHeaderSpy,
|
|
449
|
+
end: mockEndSpy,
|
|
450
|
+
} as any;
|
|
451
|
+
|
|
452
|
+
await routeHandler(mockReq, mockRes);
|
|
453
|
+
|
|
454
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "file:download");
|
|
455
|
+
expect(mockSetHeaderSpy).toHaveBeenCalledWith("Content-Type", "application/pdf");
|
|
456
|
+
expect(mockEndSpy).toHaveBeenCalledWith("file-data");
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DisplayProcessor,
|
|
3
|
+
SpecReporter,
|
|
4
|
+
StacktraceOption,
|
|
5
|
+
} from "jasmine-spec-reporter";
|
|
6
|
+
import SuiteInfo = jasmine.SuiteInfo;
|
|
7
|
+
|
|
8
|
+
class CustomProcessor extends DisplayProcessor {
|
|
9
|
+
public displayJasmineStarted(info: SuiteInfo, log: string): string {
|
|
10
|
+
return `TypeScript ${log}`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
jasmine.getEnv().clearReporters();
|
|
15
|
+
jasmine.getEnv().addReporter(
|
|
16
|
+
new SpecReporter({
|
|
17
|
+
spec: {
|
|
18
|
+
displayStacktrace: StacktraceOption.NONE,
|
|
19
|
+
},
|
|
20
|
+
customProcessors: [CustomProcessor],
|
|
21
|
+
})
|
|
22
|
+
);
|
package/src/index.ts
CHANGED
|
@@ -1,52 +1,75 @@
|
|
|
1
|
-
import { FlinkApp, FlinkPlugin } from "@flink-app/flink";
|
|
1
|
+
import { FlinkApp, FlinkPlugin, FlinkRequest } from "@flink-app/flink";
|
|
2
2
|
import log from "node-color-log";
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
export enum HttpMethod {
|
|
6
5
|
get = "get",
|
|
7
6
|
post = "post",
|
|
8
7
|
put = "put",
|
|
9
8
|
delete = "delete",
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
}
|
|
13
10
|
|
|
14
11
|
export type GenericRequestOptions = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Path for request
|
|
14
|
+
*/
|
|
15
|
+
path: string;
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Function to handle the request
|
|
19
|
+
*/
|
|
20
|
+
handler: any;
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Http method for this request
|
|
24
|
+
*/
|
|
25
|
+
method: HttpMethod;
|
|
29
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Optional permission(s) required to access this route.
|
|
29
|
+
* If set, the auth plugin will validate the request before calling the handler.
|
|
30
|
+
* Requires an auth plugin (e.g., jwt-auth-plugin) to be configured in FlinkApp.
|
|
31
|
+
*/
|
|
32
|
+
permissions?: string | string[];
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
export const genericRequestPlugin = (options: GenericRequestOptions): FlinkPlugin => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
return {
|
|
37
|
+
id: "genericRequestPlugin",
|
|
38
|
+
init: (app) => init(app, options),
|
|
39
|
+
};
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
function init(app: FlinkApp<any>, options: GenericRequestOptions) {
|
|
43
|
+
const { expressApp } = app;
|
|
44
|
+
|
|
45
|
+
if (!expressApp) {
|
|
46
|
+
throw new Error("Express app not initialized");
|
|
47
|
+
}
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
expressApp[options.method](options.path, async (req, res) => {
|
|
50
|
+
// Validate permissions if set
|
|
51
|
+
if (options.permissions) {
|
|
52
|
+
if (!app.auth) {
|
|
53
|
+
throw new Error(`Route ${options.method.toUpperCase()} ${options.path} requires permissions but no auth plugin is configured`);
|
|
54
|
+
}
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
// Express Request is structurally compatible with FlinkRequest for auth purposes
|
|
57
|
+
// (both have headers and user properties). This follows the same pattern as FlinkApp.ts:826
|
|
58
|
+
const authenticated = await app.auth.authenticateRequest(req as FlinkRequest, options.permissions);
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
if (!authenticated) {
|
|
61
|
+
return res.status(401).json({
|
|
62
|
+
status: 401,
|
|
63
|
+
error: {
|
|
64
|
+
title: "Unauthorized",
|
|
65
|
+
detail: "Authentication required or insufficient permissions",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
51
70
|
|
|
71
|
+
// Call the handler
|
|
72
|
+
options.handler(req, res, app);
|
|
73
|
+
});
|
|
74
|
+
log.info(`Registered genericRequest route ${options.method} ${options.path}`);
|
|
52
75
|
}
|
package/tsconfig.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"esModuleInterop": true,
|
|
8
8
|
"allowSyntheticDefaultImports": true,
|
|
9
9
|
"strict": true,
|
|
10
|
+
"strictNullChecks": false,
|
|
10
11
|
"forceConsistentCasingInFileNames": true,
|
|
11
12
|
"module": "commonjs",
|
|
12
13
|
"moduleResolution": "node",
|
|
@@ -15,9 +16,10 @@
|
|
|
15
16
|
"noEmit": false,
|
|
16
17
|
"declaration": true,
|
|
17
18
|
"experimentalDecorators": true,
|
|
18
|
-
"checkJs":
|
|
19
|
-
"outDir": "dist"
|
|
19
|
+
"checkJs": false,
|
|
20
|
+
"outDir": "dist",
|
|
21
|
+
"typeRoots": ["./node_modules/@types"]
|
|
20
22
|
},
|
|
21
|
-
"include": ["./src/*"],
|
|
23
|
+
"include": ["./src/*", "./spec/*"],
|
|
22
24
|
"exclude": ["./node_modules/*"]
|
|
23
25
|
}
|