@flink-app/flink 0.14.3 → 1.0.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 +60 -0
- package/dist/src/FlinkApp.js +35 -11
- package/dist/src/FlinkHttpHandler.d.ts +62 -8
- package/dist/src/FlinkHttpHandler.js +37 -1
- package/package.json +3 -1
- package/spec/FlinkApp.query.spec.ts +107 -0
- package/spec/FlinkApp.validationMode.spec.ts +155 -0
- package/src/FlinkApp.ts +26 -3
- package/src/FlinkHttpHandler.ts +64 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# @flink-app/flink
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Align minor version, from now all packages in monorepo will have same version
|
|
8
|
+
- 5c2ff97: Add schema validation bypass options to RouteProps
|
|
9
|
+
|
|
10
|
+
Handlers can now control schema validation behavior via the new `validation` property in `RouteProps`:
|
|
11
|
+
|
|
12
|
+
- `ValidationMode.Validate` (default): Validate both request and response
|
|
13
|
+
- `ValidationMode.SkipValidation`: Skip both request and response validation
|
|
14
|
+
- `ValidationMode.ValidateRequest`: Validate only request, skip response validation
|
|
15
|
+
- `ValidationMode.ValidateResponse`: Validate only response, skip request validation
|
|
16
|
+
|
|
17
|
+
This is useful for handlers that need custom validation logic or want to bypass validation for performance reasons. The feature is opt-in and defaults to existing behavior.
|
|
18
|
+
|
|
19
|
+
**Security Considerations:**
|
|
20
|
+
|
|
21
|
+
⚠️ Use validation bypass options carefully:
|
|
22
|
+
|
|
23
|
+
- `SkipValidation` and `ValidateRequest: false` allow unvalidated data to reach handlers, which can introduce security risks
|
|
24
|
+
- Only skip validation when you have implemented custom validation logic or the endpoint is internal/trusted
|
|
25
|
+
- Suitable use cases include: webhook handlers with custom signature verification, performance-critical internal endpoints, or handlers using alternative validation methods
|
|
26
|
+
- When skipping response validation, ensure your code returns consistent data shapes to avoid breaking API contracts
|
|
27
|
+
|
|
28
|
+
Example usage:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// Skip validation for webhook with custom verification
|
|
32
|
+
export const Route: RouteProps = {
|
|
33
|
+
path: "/webhook",
|
|
34
|
+
validation: ValidationMode.SkipValidation,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Validate request but allow flexible response during development
|
|
38
|
+
export const Route: RouteProps = {
|
|
39
|
+
path: "/api/debug",
|
|
40
|
+
validation: ValidationMode.ValidateRequest,
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- ee21c29: Normalize query parameters to predictable string types
|
|
47
|
+
|
|
48
|
+
Query parameters are now automatically normalized to `string | string[]` types before reaching handlers:
|
|
49
|
+
|
|
50
|
+
- Single values: converted to strings (e.g., `?page=1` → `{ page: "1" }`)
|
|
51
|
+
- Repeated parameters: converted to string arrays (e.g., `?tag=a&tag=b` → `{ tag: ["a", "b"] }`)
|
|
52
|
+
- All types (numbers, booleans, etc.) from Express parser are converted to strings
|
|
53
|
+
|
|
54
|
+
**Breaking Change (Minor):**
|
|
55
|
+
|
|
56
|
+
- `Query` type changed from `{ [x: string]: string | string[] | undefined }` to `Record<string, string | string[]>`
|
|
57
|
+
- `Params` type changed from `Request["params"]` to explicit `Record<string, string>`
|
|
58
|
+
- Removed `undefined` from query/param types since normalization ensures values are never undefined
|
|
59
|
+
- Updated OAuth and OIDC plugin query type definitions to satisfy new Query constraint
|
|
60
|
+
|
|
61
|
+
This ensures predictable query and path parameter handling regardless of Express parser configuration. Handlers can reliably parse string values as needed using `Number()`, `parseInt()`, boolean comparisons, etc.
|
|
62
|
+
|
|
3
63
|
## 0.14.3
|
|
4
64
|
|
|
5
65
|
### Patch Changes
|
package/dist/src/FlinkApp.js
CHANGED
|
@@ -62,6 +62,7 @@ var ms_1 = __importDefault(require("ms"));
|
|
|
62
62
|
var toad_scheduler_1 = require("toad-scheduler");
|
|
63
63
|
var uuid_1 = require("uuid");
|
|
64
64
|
var FlinkErrors_1 = require("./FlinkErrors");
|
|
65
|
+
var FlinkHttpHandler_1 = require("./FlinkHttpHandler");
|
|
65
66
|
var FlinkLog_1 = require("./FlinkLog");
|
|
66
67
|
var mock_data_generator_1 = __importDefault(require("./mock-data-generator"));
|
|
67
68
|
var utils_1 = require("./utils");
|
|
@@ -335,24 +336,28 @@ var FlinkApp = /** @class */ (function () {
|
|
|
335
336
|
}
|
|
336
337
|
var validateReq_1;
|
|
337
338
|
var validateRes_1;
|
|
338
|
-
if
|
|
339
|
+
// Determine validation mode (default to Validate if not specified)
|
|
340
|
+
var validationMode = routeProps.validation || FlinkHttpHandler_1.ValidationMode.Validate;
|
|
341
|
+
// Compile request schema if validation mode requires it
|
|
342
|
+
if (schema.reqSchema && validationMode !== FlinkHttpHandler_1.ValidationMode.SkipValidation && validationMode !== FlinkHttpHandler_1.ValidationMode.ValidateResponse) {
|
|
339
343
|
validateReq_1 = ajv.compile(schema.reqSchema);
|
|
340
344
|
}
|
|
341
|
-
|
|
345
|
+
// Compile response schema if validation mode requires it
|
|
346
|
+
if (schema.resSchema && validationMode !== FlinkHttpHandler_1.ValidationMode.SkipValidation && validationMode !== FlinkHttpHandler_1.ValidationMode.ValidateRequest) {
|
|
342
347
|
validateRes_1 = ajv.compile(schema.resSchema);
|
|
343
348
|
}
|
|
344
349
|
this.expressApp[method](routeProps.path, function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
345
|
-
var valid, formattedErrors, data, handlerRes, err_1, errorResponse, result, valid, formattedErrors;
|
|
346
|
-
return __generator(this, function (
|
|
347
|
-
switch (
|
|
350
|
+
var valid, formattedErrors, data, normalizedQuery, _i, _a, _b, key, value, handlerRes, err_1, errorResponse, result, valid, formattedErrors;
|
|
351
|
+
return __generator(this, function (_c) {
|
|
352
|
+
switch (_c.label) {
|
|
348
353
|
case 0:
|
|
349
354
|
if (!routeProps.permissions) return [3 /*break*/, 2];
|
|
350
355
|
return [4 /*yield*/, this.authenticate(req, routeProps.permissions)];
|
|
351
356
|
case 1:
|
|
352
|
-
if (!(
|
|
357
|
+
if (!(_c.sent())) {
|
|
353
358
|
return [2 /*return*/, res.status(401).json((0, FlinkErrors_1.unauthorized)())];
|
|
354
359
|
}
|
|
355
|
-
|
|
360
|
+
_c.label = 2;
|
|
356
361
|
case 2:
|
|
357
362
|
if (validateReq_1) {
|
|
358
363
|
valid = validateReq_1(req.body);
|
|
@@ -378,9 +383,28 @@ var FlinkApp = /** @class */ (function () {
|
|
|
378
383
|
});
|
|
379
384
|
return [2 /*return*/];
|
|
380
385
|
}
|
|
381
|
-
|
|
386
|
+
// Normalize query parameters to predictable string or string[] types
|
|
387
|
+
// Express query parser can produce numbers, booleans, objects, etc.
|
|
388
|
+
// We normalize everything to strings or string arrays for consistency
|
|
389
|
+
if (req.query && typeof req.query === "object") {
|
|
390
|
+
normalizedQuery = {};
|
|
391
|
+
for (_i = 0, _a = Object.entries(req.query); _i < _a.length; _i++) {
|
|
392
|
+
_b = _a[_i], key = _b[0], value = _b[1];
|
|
393
|
+
if (Array.isArray(value)) {
|
|
394
|
+
// Handle array values (e.g., ?tag=a&tag=b)
|
|
395
|
+
normalizedQuery[key] = value.map(function (v) { return String(v); });
|
|
396
|
+
}
|
|
397
|
+
else if (value !== undefined && value !== null) {
|
|
398
|
+
// Convert single values to strings
|
|
399
|
+
normalizedQuery[key] = String(value);
|
|
400
|
+
}
|
|
401
|
+
// Skip undefined/null values - they won't appear in the normalized query
|
|
402
|
+
}
|
|
403
|
+
req.query = normalizedQuery;
|
|
404
|
+
}
|
|
405
|
+
_c.label = 3;
|
|
382
406
|
case 3:
|
|
383
|
-
|
|
407
|
+
_c.trys.push([3, 5, , 6]);
|
|
384
408
|
return [4 /*yield*/, handler({
|
|
385
409
|
req: req,
|
|
386
410
|
ctx: this.ctx,
|
|
@@ -388,10 +412,10 @@ var FlinkApp = /** @class */ (function () {
|
|
|
388
412
|
})];
|
|
389
413
|
case 4:
|
|
390
414
|
// 👇 This is where the actual handler gets invoked
|
|
391
|
-
handlerRes =
|
|
415
|
+
handlerRes = _c.sent();
|
|
392
416
|
return [3 /*break*/, 6];
|
|
393
417
|
case 5:
|
|
394
|
-
err_1 =
|
|
418
|
+
err_1 = _c.sent();
|
|
395
419
|
errorResponse = void 0;
|
|
396
420
|
// duck typing to check if it is a FlinkError
|
|
397
421
|
if (typeof err_1.status === "number" && err_1.status >= 400 && err_1.status < 600 && err_1.error) {
|
|
@@ -10,18 +10,52 @@ export declare enum HttpMethod {
|
|
|
10
10
|
delete = "delete",
|
|
11
11
|
patch = "patch"
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Validation mode for handler request and response schemas.
|
|
15
|
+
*
|
|
16
|
+
* Controls whether request and/or response data is validated against JSON schemas.
|
|
17
|
+
*
|
|
18
|
+
* **Security Note:** Skipping validation can introduce security risks. Only use
|
|
19
|
+
* SkipValidation or ValidateResponse when you have implemented custom validation
|
|
20
|
+
* or the endpoint is internal/trusted.
|
|
21
|
+
*
|
|
22
|
+
* - Validate: Validate both request and response (default behavior)
|
|
23
|
+
* - SkipValidation: Skip both request and response validation
|
|
24
|
+
* - ValidateRequest: Validate only request, skip response validation
|
|
25
|
+
* - ValidateResponse: Validate only response, skip request validation
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // Skip validation for webhook with custom signature verification
|
|
30
|
+
* export const Route: RouteProps = {
|
|
31
|
+
* path: "/webhook",
|
|
32
|
+
* validation: ValidationMode.SkipValidation
|
|
33
|
+
* };
|
|
34
|
+
*
|
|
35
|
+
* // Validate request but allow flexible response during development
|
|
36
|
+
* export const Route: RouteProps = {
|
|
37
|
+
* path: "/api/data",
|
|
38
|
+
* validation: ValidationMode.ValidateRequest
|
|
39
|
+
* };
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare enum ValidationMode {
|
|
43
|
+
Validate = "Validate",
|
|
44
|
+
SkipValidation = "SkipValidation",
|
|
45
|
+
ValidateRequest = "ValidateRequest",
|
|
46
|
+
ValidateResponse = "ValidateResponse"
|
|
47
|
+
}
|
|
48
|
+
type Params = Record<string, string>;
|
|
14
49
|
/**
|
|
15
50
|
* Query type for request query parameters.
|
|
16
|
-
* Does currently not allow nested objects, although
|
|
17
|
-
* underlying express Request does allow it.
|
|
18
51
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
52
|
+
* All query parameter values are normalized to strings or string arrays:
|
|
53
|
+
* - Single values: string (e.g., ?name=John becomes { name: "John" })
|
|
54
|
+
* - Multiple values: string[] (e.g., ?tag=a&tag=b becomes { tag: ["a", "b"] })
|
|
55
|
+
*
|
|
56
|
+
* Does not allow nested objects, although underlying Express Request does allow it.
|
|
21
57
|
*/
|
|
22
|
-
type Query =
|
|
23
|
-
[x: string]: string | string[] | undefined;
|
|
24
|
-
};
|
|
58
|
+
type Query = Record<string, string | string[]>;
|
|
25
59
|
/**
|
|
26
60
|
* Flink request extends express Request but adds reqId and user object.
|
|
27
61
|
*/
|
|
@@ -82,6 +116,26 @@ export interface RouteProps {
|
|
|
82
116
|
* to avoid conflicts you can set a negative order.
|
|
83
117
|
*/
|
|
84
118
|
order?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Validation mode for request and response schemas.
|
|
121
|
+
*
|
|
122
|
+
* Controls schema validation behavior for this handler. Use with caution as
|
|
123
|
+
* skipping validation can introduce security vulnerabilities.
|
|
124
|
+
*
|
|
125
|
+
* **Options:**
|
|
126
|
+
* - Validate: Validate both request and response (default)
|
|
127
|
+
* - SkipValidation: Skip both request and response validation
|
|
128
|
+
* - ValidateRequest: Validate only request, skip response validation
|
|
129
|
+
* - ValidateResponse: Validate only response, skip request validation
|
|
130
|
+
*
|
|
131
|
+
* **When to skip validation:**
|
|
132
|
+
* - Webhook handlers with custom signature verification
|
|
133
|
+
* - Performance-critical internal endpoints
|
|
134
|
+
* - Handlers using alternative validation methods (e.g., Zod, Joi)
|
|
135
|
+
*
|
|
136
|
+
* @default ValidationMode.Validate
|
|
137
|
+
*/
|
|
138
|
+
validation?: ValidationMode;
|
|
85
139
|
}
|
|
86
140
|
/**
|
|
87
141
|
* Http handler function that handlers implements in order to
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HttpMethod = void 0;
|
|
3
|
+
exports.ValidationMode = exports.HttpMethod = void 0;
|
|
4
4
|
var HttpMethod;
|
|
5
5
|
(function (HttpMethod) {
|
|
6
6
|
HttpMethod["get"] = "get";
|
|
@@ -9,3 +9,39 @@ var HttpMethod;
|
|
|
9
9
|
HttpMethod["delete"] = "delete";
|
|
10
10
|
HttpMethod["patch"] = "patch";
|
|
11
11
|
})(HttpMethod || (exports.HttpMethod = HttpMethod = {}));
|
|
12
|
+
/**
|
|
13
|
+
* Validation mode for handler request and response schemas.
|
|
14
|
+
*
|
|
15
|
+
* Controls whether request and/or response data is validated against JSON schemas.
|
|
16
|
+
*
|
|
17
|
+
* **Security Note:** Skipping validation can introduce security risks. Only use
|
|
18
|
+
* SkipValidation or ValidateResponse when you have implemented custom validation
|
|
19
|
+
* or the endpoint is internal/trusted.
|
|
20
|
+
*
|
|
21
|
+
* - Validate: Validate both request and response (default behavior)
|
|
22
|
+
* - SkipValidation: Skip both request and response validation
|
|
23
|
+
* - ValidateRequest: Validate only request, skip response validation
|
|
24
|
+
* - ValidateResponse: Validate only response, skip request validation
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* // Skip validation for webhook with custom signature verification
|
|
29
|
+
* export const Route: RouteProps = {
|
|
30
|
+
* path: "/webhook",
|
|
31
|
+
* validation: ValidationMode.SkipValidation
|
|
32
|
+
* };
|
|
33
|
+
*
|
|
34
|
+
* // Validate request but allow flexible response during development
|
|
35
|
+
* export const Route: RouteProps = {
|
|
36
|
+
* path: "/api/data",
|
|
37
|
+
* validation: ValidationMode.ValidateRequest
|
|
38
|
+
* };
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
var ValidationMode;
|
|
42
|
+
(function (ValidationMode) {
|
|
43
|
+
ValidationMode["Validate"] = "Validate";
|
|
44
|
+
ValidationMode["SkipValidation"] = "SkipValidation";
|
|
45
|
+
ValidationMode["ValidateRequest"] = "ValidateRequest";
|
|
46
|
+
ValidationMode["ValidateResponse"] = "ValidateResponse";
|
|
47
|
+
})(ValidationMode || (exports.ValidationMode = ValidationMode = {}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/flink",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Typescript only framework for creating REST-like APIs on top of Express and mongodb",
|
|
5
5
|
"types": "dist/src/index.d.ts",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -50,7 +50,9 @@
|
|
|
50
50
|
"@types/mkdirp": "^1.0.1",
|
|
51
51
|
"@types/morgan": "^1.9.4",
|
|
52
52
|
"@types/node": "22.13.10",
|
|
53
|
+
"@types/supertest": "^6.0.3",
|
|
53
54
|
"mongodb": "^6.15.0",
|
|
55
|
+
"supertest": "^7.2.2",
|
|
54
56
|
"ts-node": "^10.9.2"
|
|
55
57
|
},
|
|
56
58
|
"peerDependencies": {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { FlinkApp } from "../src/FlinkApp";
|
|
2
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
3
|
+
import { GetHandler, HttpMethod } from "../src/FlinkHttpHandler";
|
|
4
|
+
|
|
5
|
+
const request = require("supertest");
|
|
6
|
+
|
|
7
|
+
interface TestContext extends FlinkContext {}
|
|
8
|
+
|
|
9
|
+
describe("Query parameter normalization", () => {
|
|
10
|
+
let app: FlinkApp<TestContext>;
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
if (app && app.started) {
|
|
14
|
+
await app.stop();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should normalize query params to strings", async () => {
|
|
19
|
+
const handler: GetHandler<TestContext, any> = async ({ req }) => {
|
|
20
|
+
return { status: 200, data: { query: req.query } };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
app = new FlinkApp<TestContext>({ name: "test-query-norm", port: 4000 });
|
|
24
|
+
await app.start();
|
|
25
|
+
|
|
26
|
+
app.addHandler({
|
|
27
|
+
default: handler,
|
|
28
|
+
Route: { method: HttpMethod.get, path: "/test" },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const response = await request(app.expressApp).get("/test?name=John&age=25");
|
|
32
|
+
|
|
33
|
+
expect(response.status).toBe(200);
|
|
34
|
+
expect(response.body.data.query.name).toBe("John");
|
|
35
|
+
expect(response.body.data.query.age).toBe("25");
|
|
36
|
+
expect(typeof response.body.data.query.name).toBe("string");
|
|
37
|
+
expect(typeof response.body.data.query.age).toBe("string");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should normalize array query params to string arrays", async () => {
|
|
41
|
+
const handler: GetHandler<TestContext, any> = async ({ req }) => {
|
|
42
|
+
return { status: 200, data: { query: req.query } };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
app = new FlinkApp<TestContext>({ name: "test-query-array", port: 4001 });
|
|
46
|
+
await app.start();
|
|
47
|
+
|
|
48
|
+
app.addHandler({
|
|
49
|
+
default: handler,
|
|
50
|
+
Route: { method: HttpMethod.get, path: "/test-array" },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const response = await request(app.expressApp).get("/test-array?tag=a&tag=b&tag=c");
|
|
54
|
+
|
|
55
|
+
expect(response.status).toBe(200);
|
|
56
|
+
expect(Array.isArray(response.body.data.query.tag)).toBe(true);
|
|
57
|
+
expect(response.body.data.query.tag).toEqual(["a", "b", "c"]);
|
|
58
|
+
response.body.data.query.tag.forEach((tag: string) => {
|
|
59
|
+
expect(typeof tag).toBe("string");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should normalize numeric values to strings", async () => {
|
|
64
|
+
const handler: GetHandler<TestContext, any> = async ({ req }) => {
|
|
65
|
+
return { status: 200, data: { query: req.query } };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
app = new FlinkApp<TestContext>({ name: "test-query-numbers", port: 4002 });
|
|
69
|
+
await app.start();
|
|
70
|
+
|
|
71
|
+
app.addHandler({
|
|
72
|
+
default: handler,
|
|
73
|
+
Route: { method: HttpMethod.get, path: "/test-numbers" },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const response = await request(app.expressApp).get("/test-numbers?count=100&price=99.99");
|
|
77
|
+
|
|
78
|
+
expect(response.status).toBe(200);
|
|
79
|
+
expect(response.body.data.query.count).toBe("100");
|
|
80
|
+
expect(response.body.data.query.price).toBe("99.99");
|
|
81
|
+
expect(typeof response.body.data.query.count).toBe("string");
|
|
82
|
+
expect(typeof response.body.data.query.price).toBe("string");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should allow handlers to parse strings as needed", async () => {
|
|
86
|
+
const handler: GetHandler<TestContext, any> = async ({ req }) => {
|
|
87
|
+
const page = Number(req.query.page) || 1;
|
|
88
|
+
const active = req.query.active === "true";
|
|
89
|
+
|
|
90
|
+
return { status: 200, data: { page, active } };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
app = new FlinkApp<TestContext>({ name: "test-query-parsing", port: 4003 });
|
|
94
|
+
await app.start();
|
|
95
|
+
|
|
96
|
+
app.addHandler({
|
|
97
|
+
default: handler,
|
|
98
|
+
Route: { method: HttpMethod.get, path: "/test-parsing" },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const response = await request(app.expressApp).get("/test-parsing?page=2&active=true");
|
|
102
|
+
|
|
103
|
+
expect(response.status).toBe(200);
|
|
104
|
+
expect(response.body.data.page).toBe(2);
|
|
105
|
+
expect(response.body.data.active).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { FlinkApp } from "../src/FlinkApp";
|
|
2
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
3
|
+
import { Handler, RouteProps, ValidationMode } from "../src/FlinkHttpHandler";
|
|
4
|
+
|
|
5
|
+
interface TestContext extends FlinkContext {}
|
|
6
|
+
|
|
7
|
+
// Test schemas
|
|
8
|
+
interface ValidRequest {
|
|
9
|
+
name: string;
|
|
10
|
+
age: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ValidResponse {
|
|
14
|
+
message: string;
|
|
15
|
+
status: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("FlinkApp validation modes", () => {
|
|
19
|
+
let app: FlinkApp<TestContext>;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
app = new FlinkApp<TestContext>({
|
|
23
|
+
name: "validation-test-app",
|
|
24
|
+
debug: false,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
if (app && app.started) {
|
|
30
|
+
await app.stop();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("ValidationMode enum", () => {
|
|
35
|
+
it("should have all validation mode values", () => {
|
|
36
|
+
expect(ValidationMode.Validate).toBe("Validate");
|
|
37
|
+
expect(ValidationMode.SkipValidation).toBe("SkipValidation");
|
|
38
|
+
expect(ValidationMode.ValidateRequest).toBe("ValidateRequest");
|
|
39
|
+
expect(ValidationMode.ValidateResponse).toBe("ValidateResponse");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("RouteProps validation property", () => {
|
|
44
|
+
it("should accept validation property with ValidationMode.Validate", () => {
|
|
45
|
+
const route: RouteProps = {
|
|
46
|
+
path: "/test",
|
|
47
|
+
validation: ValidationMode.Validate,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
expect(route.validation).toBe(ValidationMode.Validate);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should accept validation property with ValidationMode.SkipValidation", () => {
|
|
54
|
+
const route: RouteProps = {
|
|
55
|
+
path: "/test",
|
|
56
|
+
validation: ValidationMode.SkipValidation,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
expect(route.validation).toBe(ValidationMode.SkipValidation);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should accept validation property with ValidationMode.ValidateRequest", () => {
|
|
63
|
+
const route: RouteProps = {
|
|
64
|
+
path: "/test",
|
|
65
|
+
validation: ValidationMode.ValidateRequest,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
expect(route.validation).toBe(ValidationMode.ValidateRequest);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should accept validation property with ValidationMode.ValidateResponse", () => {
|
|
72
|
+
const route: RouteProps = {
|
|
73
|
+
path: "/test",
|
|
74
|
+
validation: ValidationMode.ValidateResponse,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
expect(route.validation).toBe(ValidationMode.ValidateResponse);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should be optional and default to undefined", () => {
|
|
81
|
+
const route: RouteProps = {
|
|
82
|
+
path: "/test",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
expect(route.validation).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("Handler type compatibility", () => {
|
|
90
|
+
it("should allow handlers with validation modes to compile", () => {
|
|
91
|
+
// This test verifies TypeScript compilation
|
|
92
|
+
|
|
93
|
+
const handler: Handler<TestContext, ValidRequest, ValidResponse> = async ({ ctx, req }) => {
|
|
94
|
+
return {
|
|
95
|
+
status: 200,
|
|
96
|
+
data: {
|
|
97
|
+
message: "success",
|
|
98
|
+
status: "ok",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
expect(handler).toBeDefined();
|
|
104
|
+
expect(typeof handler).toBe("function");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should allow route props with different validation modes", () => {
|
|
108
|
+
const routes: RouteProps[] = [
|
|
109
|
+
{ path: "/test1", validation: ValidationMode.Validate },
|
|
110
|
+
{ path: "/test2", validation: ValidationMode.SkipValidation },
|
|
111
|
+
{ path: "/test3", validation: ValidationMode.ValidateRequest },
|
|
112
|
+
{ path: "/test4", validation: ValidationMode.ValidateResponse },
|
|
113
|
+
{ path: "/test5" }, // No validation specified
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
expect(routes.length).toBe(5);
|
|
117
|
+
expect(routes[0].validation).toBe(ValidationMode.Validate);
|
|
118
|
+
expect(routes[1].validation).toBe(ValidationMode.SkipValidation);
|
|
119
|
+
expect(routes[2].validation).toBe(ValidationMode.ValidateRequest);
|
|
120
|
+
expect(routes[3].validation).toBe(ValidationMode.ValidateResponse);
|
|
121
|
+
expect(routes[4].validation).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("Backward compatibility", () => {
|
|
126
|
+
it("should not break existing code without validation property", () => {
|
|
127
|
+
const route: RouteProps = {
|
|
128
|
+
path: "/legacy",
|
|
129
|
+
permissions: ["admin"],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// TypeScript should compile this without errors
|
|
133
|
+
expect(route.path).toBe("/legacy");
|
|
134
|
+
expect(route.validation).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should allow all existing RouteProps properties with validation", () => {
|
|
138
|
+
const route: RouteProps = {
|
|
139
|
+
path: "/test",
|
|
140
|
+
permissions: ["admin", "user"],
|
|
141
|
+
skipAutoRegister: true,
|
|
142
|
+
docs: "Test endpoint",
|
|
143
|
+
order: -1,
|
|
144
|
+
validation: ValidationMode.SkipValidation,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
expect(route.path).toBe("/test");
|
|
148
|
+
expect(route.permissions).toEqual(["admin", "user"]);
|
|
149
|
+
expect(route.skipAutoRegister).toBe(true);
|
|
150
|
+
expect(route.docs).toBe("Test endpoint");
|
|
151
|
+
expect(route.order).toBe(-1);
|
|
152
|
+
expect(route.validation).toBe(ValidationMode.SkipValidation);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
package/src/FlinkApp.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { v4 } from "uuid";
|
|
|
12
12
|
import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
|
|
13
13
|
import { FlinkContext } from "./FlinkContext";
|
|
14
14
|
import { FlinkError, internalServerError, notFound, unauthorized } from "./FlinkErrors";
|
|
15
|
-
import { FlinkRequest, Handler, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps } from "./FlinkHttpHandler";
|
|
15
|
+
import { FlinkRequest, Handler, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps, ValidationMode } from "./FlinkHttpHandler";
|
|
16
16
|
import { FlinkJobFile } from "./FlinkJob";
|
|
17
17
|
import { log } from "./FlinkLog";
|
|
18
18
|
import { FlinkPlugin } from "./FlinkPlugin";
|
|
@@ -535,11 +535,16 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
535
535
|
let validateReq: ValidateFunction<any> | undefined;
|
|
536
536
|
let validateRes: ValidateFunction<any> | undefined;
|
|
537
537
|
|
|
538
|
-
if
|
|
538
|
+
// Determine validation mode (default to Validate if not specified)
|
|
539
|
+
const validationMode = routeProps.validation || ValidationMode.Validate;
|
|
540
|
+
|
|
541
|
+
// Compile request schema if validation mode requires it
|
|
542
|
+
if (schema.reqSchema && validationMode !== ValidationMode.SkipValidation && validationMode !== ValidationMode.ValidateResponse) {
|
|
539
543
|
validateReq = ajv.compile(schema.reqSchema);
|
|
540
544
|
}
|
|
541
545
|
|
|
542
|
-
|
|
546
|
+
// Compile response schema if validation mode requires it
|
|
547
|
+
if (schema.resSchema && validationMode !== ValidationMode.SkipValidation && validationMode !== ValidationMode.ValidateRequest) {
|
|
543
548
|
validateRes = ajv.compile(schema.resSchema);
|
|
544
549
|
}
|
|
545
550
|
|
|
@@ -580,6 +585,24 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
580
585
|
return;
|
|
581
586
|
}
|
|
582
587
|
|
|
588
|
+
// Normalize query parameters to predictable string or string[] types
|
|
589
|
+
// Express query parser can produce numbers, booleans, objects, etc.
|
|
590
|
+
// We normalize everything to strings or string arrays for consistency
|
|
591
|
+
if (req.query && typeof req.query === "object") {
|
|
592
|
+
const normalizedQuery: Record<string, string | string[]> = {};
|
|
593
|
+
for (const [key, value] of Object.entries(req.query)) {
|
|
594
|
+
if (Array.isArray(value)) {
|
|
595
|
+
// Handle array values (e.g., ?tag=a&tag=b)
|
|
596
|
+
normalizedQuery[key] = value.map((v) => String(v));
|
|
597
|
+
} else if (value !== undefined && value !== null) {
|
|
598
|
+
// Convert single values to strings
|
|
599
|
+
normalizedQuery[key] = String(value);
|
|
600
|
+
}
|
|
601
|
+
// Skip undefined/null values - they won't appear in the normalized query
|
|
602
|
+
}
|
|
603
|
+
req.query = normalizedQuery;
|
|
604
|
+
}
|
|
605
|
+
|
|
583
606
|
let handlerRes: FlinkResponse<any>;
|
|
584
607
|
|
|
585
608
|
try {
|
package/src/FlinkHttpHandler.ts
CHANGED
|
@@ -12,19 +12,54 @@ export enum HttpMethod {
|
|
|
12
12
|
patch = "patch",
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Validation mode for handler request and response schemas.
|
|
17
|
+
*
|
|
18
|
+
* Controls whether request and/or response data is validated against JSON schemas.
|
|
19
|
+
*
|
|
20
|
+
* **Security Note:** Skipping validation can introduce security risks. Only use
|
|
21
|
+
* SkipValidation or ValidateResponse when you have implemented custom validation
|
|
22
|
+
* or the endpoint is internal/trusted.
|
|
23
|
+
*
|
|
24
|
+
* - Validate: Validate both request and response (default behavior)
|
|
25
|
+
* - SkipValidation: Skip both request and response validation
|
|
26
|
+
* - ValidateRequest: Validate only request, skip response validation
|
|
27
|
+
* - ValidateResponse: Validate only response, skip request validation
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // Skip validation for webhook with custom signature verification
|
|
32
|
+
* export const Route: RouteProps = {
|
|
33
|
+
* path: "/webhook",
|
|
34
|
+
* validation: ValidationMode.SkipValidation
|
|
35
|
+
* };
|
|
36
|
+
*
|
|
37
|
+
* // Validate request but allow flexible response during development
|
|
38
|
+
* export const Route: RouteProps = {
|
|
39
|
+
* path: "/api/data",
|
|
40
|
+
* validation: ValidationMode.ValidateRequest
|
|
41
|
+
* };
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export enum ValidationMode {
|
|
45
|
+
Validate = "Validate",
|
|
46
|
+
SkipValidation = "SkipValidation",
|
|
47
|
+
ValidateRequest = "ValidateRequest",
|
|
48
|
+
ValidateResponse = "ValidateResponse",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Params = Record<string, string>;
|
|
16
52
|
|
|
17
53
|
/**
|
|
18
54
|
* Query type for request query parameters.
|
|
19
|
-
* Does currently not allow nested objects, although
|
|
20
|
-
* underlying express Request does allow it.
|
|
21
55
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
56
|
+
* All query parameter values are normalized to strings or string arrays:
|
|
57
|
+
* - Single values: string (e.g., ?name=John becomes { name: "John" })
|
|
58
|
+
* - Multiple values: string[] (e.g., ?tag=a&tag=b becomes { tag: ["a", "b"] })
|
|
59
|
+
*
|
|
60
|
+
* Does not allow nested objects, although underlying Express Request does allow it.
|
|
24
61
|
*/
|
|
25
|
-
type Query =
|
|
26
|
-
[x: string]: string | string[] | undefined;
|
|
27
|
-
};
|
|
62
|
+
type Query = Record<string, string | string[]>;
|
|
28
63
|
|
|
29
64
|
/**
|
|
30
65
|
* Flink request extends express Request but adds reqId and user object.
|
|
@@ -91,6 +126,27 @@ export interface RouteProps {
|
|
|
91
126
|
* to avoid conflicts you can set a negative order.
|
|
92
127
|
*/
|
|
93
128
|
order?: number;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validation mode for request and response schemas.
|
|
132
|
+
*
|
|
133
|
+
* Controls schema validation behavior for this handler. Use with caution as
|
|
134
|
+
* skipping validation can introduce security vulnerabilities.
|
|
135
|
+
*
|
|
136
|
+
* **Options:**
|
|
137
|
+
* - Validate: Validate both request and response (default)
|
|
138
|
+
* - SkipValidation: Skip both request and response validation
|
|
139
|
+
* - ValidateRequest: Validate only request, skip response validation
|
|
140
|
+
* - ValidateResponse: Validate only response, skip request validation
|
|
141
|
+
*
|
|
142
|
+
* **When to skip validation:**
|
|
143
|
+
* - Webhook handlers with custom signature verification
|
|
144
|
+
* - Performance-critical internal endpoints
|
|
145
|
+
* - Handlers using alternative validation methods (e.g., Zod, Joi)
|
|
146
|
+
*
|
|
147
|
+
* @default ValidationMode.Validate
|
|
148
|
+
*/
|
|
149
|
+
validation?: ValidationMode;
|
|
94
150
|
}
|
|
95
151
|
|
|
96
152
|
/**
|