@flink-app/flink 0.13.4 → 0.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/src/FlinkApp.js +5 -6
- package/dist/src/utils.d.ts +12 -0
- package/dist/src/utils.js +71 -0
- package/package.json +1 -1
- package/spec/utils.spec.ts +135 -1
- package/src/FlinkApp.ts +5 -7
- package/src/utils.ts +74 -0
package/CHANGELOG.md
CHANGED
package/dist/src/FlinkApp.js
CHANGED
|
@@ -341,7 +341,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
341
341
|
validateRes_1 = ajv.compile(schema.resSchema);
|
|
342
342
|
}
|
|
343
343
|
this.expressApp[method](routeProps.path, function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
344
|
-
var valid, data, handlerRes, err_1, valid;
|
|
344
|
+
var valid, formattedErrors, data, handlerRes, err_1, valid, formattedErrors;
|
|
345
345
|
return __generator(this, function (_a) {
|
|
346
346
|
switch (_a.label) {
|
|
347
347
|
case 0:
|
|
@@ -356,8 +356,8 @@ var FlinkApp = /** @class */ (function () {
|
|
|
356
356
|
if (validateReq_1) {
|
|
357
357
|
valid = validateReq_1(req.body);
|
|
358
358
|
if (!valid) {
|
|
359
|
-
|
|
360
|
-
FlinkLog_1.log.
|
|
359
|
+
formattedErrors = (0, utils_1.formatValidationErrors)(validateReq_1.errors, req.body);
|
|
360
|
+
FlinkLog_1.log.warn("[".concat(req.reqId, "] ").concat(methodAndRoute_1, ": Bad request\n").concat(formattedErrors));
|
|
361
361
|
return [2 /*return*/, res.status(400).json({
|
|
362
362
|
status: 400,
|
|
363
363
|
error: {
|
|
@@ -410,9 +410,8 @@ var FlinkApp = /** @class */ (function () {
|
|
|
410
410
|
if (validateRes_1 && !(0, utils_1.isError)(handlerRes)) {
|
|
411
411
|
valid = validateRes_1(JSON.parse(JSON.stringify(handlerRes.data)));
|
|
412
412
|
if (!valid) {
|
|
413
|
-
|
|
414
|
-
FlinkLog_1.log.
|
|
415
|
-
// log.debug(JSON.stringify(schema, null, 2));
|
|
413
|
+
formattedErrors = (0, utils_1.formatValidationErrors)(validateRes_1.errors, handlerRes.data);
|
|
414
|
+
FlinkLog_1.log.warn("[".concat(req.reqId, "] ").concat(methodAndRoute_1, ": Bad response\n").concat(formattedErrors));
|
|
416
415
|
return [2 /*return*/, res.status(500).json({
|
|
417
416
|
status: 500,
|
|
418
417
|
error: {
|
package/dist/src/utils.d.ts
CHANGED
|
@@ -25,3 +25,15 @@ export declare function getJsDocComment(comment: string): string;
|
|
|
25
25
|
* @returns
|
|
26
26
|
*/
|
|
27
27
|
export declare function getPathParams(path: string): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Extracts data at a given JSON path (e.g., "/jobs/5/result")
|
|
30
|
+
* Returns the value at that path, or undefined if not found
|
|
31
|
+
*/
|
|
32
|
+
export declare function getDataAtPath(data: any, instancePath: string): any;
|
|
33
|
+
/**
|
|
34
|
+
* Formats validation errors with context about the problematic data
|
|
35
|
+
* @param errors AJV validation errors
|
|
36
|
+
* @param data The full data object that failed validation
|
|
37
|
+
* @param maxDataLength Maximum length of data to show (default 500)
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatValidationErrors(errors: any[] | null | undefined, data: any, maxDataLength?: number): string;
|
package/dist/src/utils.js
CHANGED
|
@@ -50,6 +50,8 @@ exports.getRepoInstanceName = getRepoInstanceName;
|
|
|
50
50
|
exports.getHttpMethodFromHandlerName = getHttpMethodFromHandlerName;
|
|
51
51
|
exports.getJsDocComment = getJsDocComment;
|
|
52
52
|
exports.getPathParams = getPathParams;
|
|
53
|
+
exports.getDataAtPath = getDataAtPath;
|
|
54
|
+
exports.formatValidationErrors = formatValidationErrors;
|
|
53
55
|
var path_1 = require("path");
|
|
54
56
|
var tiny_glob_1 = __importDefault(require("tiny-glob"));
|
|
55
57
|
var FlinkHttpHandler_1 = require("./FlinkHttpHandler");
|
|
@@ -162,3 +164,72 @@ function getPathParams(path) {
|
|
|
162
164
|
var _a;
|
|
163
165
|
return ((_a = path.match(pathParamsRegex)) === null || _a === void 0 ? void 0 : _a.map(function (match) { return match.slice(1); })) || [];
|
|
164
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Extracts data at a given JSON path (e.g., "/jobs/5/result")
|
|
169
|
+
* Returns the value at that path, or undefined if not found
|
|
170
|
+
*/
|
|
171
|
+
function getDataAtPath(data, instancePath) {
|
|
172
|
+
if (!instancePath || instancePath === "/") {
|
|
173
|
+
return data;
|
|
174
|
+
}
|
|
175
|
+
var parts = instancePath.split("/").filter(function (p) { return p.length > 0; });
|
|
176
|
+
var current = data;
|
|
177
|
+
for (var _i = 0, parts_1 = parts; _i < parts_1.length; _i++) {
|
|
178
|
+
var part = parts_1[_i];
|
|
179
|
+
if (current === undefined || current === null) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
current = current[part];
|
|
183
|
+
}
|
|
184
|
+
return current;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Formats validation errors with context about the problematic data
|
|
188
|
+
* @param errors AJV validation errors
|
|
189
|
+
* @param data The full data object that failed validation
|
|
190
|
+
* @param maxDataLength Maximum length of data to show (default 500)
|
|
191
|
+
*/
|
|
192
|
+
function formatValidationErrors(errors, data, maxDataLength) {
|
|
193
|
+
if (maxDataLength === void 0) { maxDataLength = 500; }
|
|
194
|
+
if (!errors || errors.length === 0) {
|
|
195
|
+
return "Unknown validation error";
|
|
196
|
+
}
|
|
197
|
+
var formatted = [];
|
|
198
|
+
// Group errors by instance path to avoid repetition
|
|
199
|
+
var errorsByPath = new Map();
|
|
200
|
+
for (var _i = 0, errors_1 = errors; _i < errors_1.length; _i++) {
|
|
201
|
+
var error = errors_1[_i];
|
|
202
|
+
var path = error.instancePath || "/";
|
|
203
|
+
if (!errorsByPath.has(path)) {
|
|
204
|
+
errorsByPath.set(path, []);
|
|
205
|
+
}
|
|
206
|
+
errorsByPath.get(path).push(error);
|
|
207
|
+
}
|
|
208
|
+
errorsByPath.forEach(function (pathErrors, path) {
|
|
209
|
+
var dataAtPath = getDataAtPath(data, path);
|
|
210
|
+
var dataStr = JSON.stringify(dataAtPath);
|
|
211
|
+
// Truncate if too long
|
|
212
|
+
if (dataStr.length > maxDataLength) {
|
|
213
|
+
dataStr = dataStr.substring(0, maxDataLength) + "... (truncated)";
|
|
214
|
+
}
|
|
215
|
+
formatted.push("\nPath: ".concat(path));
|
|
216
|
+
formatted.push("Data: ".concat(dataStr));
|
|
217
|
+
formatted.push("Errors:");
|
|
218
|
+
for (var _i = 0, pathErrors_1 = pathErrors; _i < pathErrors_1.length; _i++) {
|
|
219
|
+
var error = pathErrors_1[_i];
|
|
220
|
+
if (error.keyword === "required") {
|
|
221
|
+
formatted.push(" - Missing required property: ".concat(error.params.missingProperty));
|
|
222
|
+
}
|
|
223
|
+
else if (error.keyword === "type") {
|
|
224
|
+
formatted.push(" - Invalid type at ".concat(error.schemaPath, ": expected ").concat(error.params.type, ", got ").concat(typeof dataAtPath));
|
|
225
|
+
}
|
|
226
|
+
else if (error.keyword === "anyOf" || error.keyword === "oneOf") {
|
|
227
|
+
formatted.push(" - ".concat(error.message));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
formatted.push(" - ".concat(error.message, " (").concat(error.keyword, ")"));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
return formatted.join("\n");
|
|
235
|
+
}
|
package/package.json
CHANGED
package/spec/utils.spec.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JSONSchema7 } from "json-schema";
|
|
2
|
-
import { getJsDocComment } from "../src/utils";
|
|
2
|
+
import { formatValidationErrors, getDataAtPath, getJsDocComment } from "../src/utils";
|
|
3
3
|
|
|
4
4
|
describe("Utils", () => {
|
|
5
5
|
describe("getJsDocComment", () => {
|
|
@@ -15,6 +15,140 @@ describe("Utils", () => {
|
|
|
15
15
|
expect(getJsDocComment(comment)).toBe("Hello world\nThis is another line\nThis line contains a * (asterisk)");
|
|
16
16
|
});
|
|
17
17
|
});
|
|
18
|
+
|
|
19
|
+
describe("getDataAtPath", () => {
|
|
20
|
+
it("should extract data at root path", () => {
|
|
21
|
+
const data = { foo: "bar" };
|
|
22
|
+
expect(getDataAtPath(data, "/")).toEqual({ foo: "bar" });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should extract nested data", () => {
|
|
26
|
+
const data = { jobs: [{ id: 1 }, { id: 2 }, { id: 3, result: { seed: "123" } }] };
|
|
27
|
+
expect(getDataAtPath(data, "/jobs/2/result")).toEqual({ seed: "123" });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should return undefined for non-existent path", () => {
|
|
31
|
+
const data = { jobs: [{ id: 1 }] };
|
|
32
|
+
expect(getDataAtPath(data, "/jobs/5/result")).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("formatValidationErrors", () => {
|
|
37
|
+
it("should format simple validation errors with data context", () => {
|
|
38
|
+
const errors = [
|
|
39
|
+
{
|
|
40
|
+
instancePath: "/name",
|
|
41
|
+
schemaPath: "#/properties/name/type",
|
|
42
|
+
keyword: "type",
|
|
43
|
+
params: { type: "string" },
|
|
44
|
+
message: "must be string",
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
const data = { name: 123 };
|
|
48
|
+
|
|
49
|
+
const formatted = formatValidationErrors(errors, data);
|
|
50
|
+
|
|
51
|
+
expect(formatted).toContain('Path: /name');
|
|
52
|
+
expect(formatted).toContain('Data: 123');
|
|
53
|
+
expect(formatted).toContain('expected string');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should format missing required property errors", () => {
|
|
57
|
+
const errors = [
|
|
58
|
+
{
|
|
59
|
+
instancePath: "/user",
|
|
60
|
+
schemaPath: "#/properties/user/required",
|
|
61
|
+
keyword: "required",
|
|
62
|
+
params: { missingProperty: "email" },
|
|
63
|
+
message: "must have required property 'email'",
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
const data = { user: { name: "John" } };
|
|
67
|
+
|
|
68
|
+
const formatted = formatValidationErrors(errors, data);
|
|
69
|
+
|
|
70
|
+
expect(formatted).toContain('Path: /user');
|
|
71
|
+
expect(formatted).toContain('Data: {"name":"John"}');
|
|
72
|
+
expect(formatted).toContain('Missing required property: email');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should format errors for array items", () => {
|
|
76
|
+
const errors = [
|
|
77
|
+
{
|
|
78
|
+
instancePath: "/jobs/5/result",
|
|
79
|
+
schemaPath: "#/properties/jobs/items/properties/result/anyOf",
|
|
80
|
+
keyword: "anyOf",
|
|
81
|
+
params: {},
|
|
82
|
+
message: "must match a schema in anyOf",
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
const data = {
|
|
86
|
+
jobs: [
|
|
87
|
+
{ result: { seed: 1 } },
|
|
88
|
+
{ result: { seed: 2 } },
|
|
89
|
+
{ result: { seed: 3 } },
|
|
90
|
+
{ result: { seed: 4 } },
|
|
91
|
+
{ result: { seed: 5 } },
|
|
92
|
+
{ result: { seed: "wrong-type" } }, // This is item 5 (0-indexed)
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const formatted = formatValidationErrors(errors, data);
|
|
97
|
+
|
|
98
|
+
expect(formatted).toContain('Path: /jobs/5/result');
|
|
99
|
+
expect(formatted).toContain('Data: {"seed":"wrong-type"}');
|
|
100
|
+
expect(formatted).toContain('must match a schema in anyOf');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should truncate very long data", () => {
|
|
104
|
+
const errors = [
|
|
105
|
+
{
|
|
106
|
+
instancePath: "/data",
|
|
107
|
+
schemaPath: "#/properties/data/type",
|
|
108
|
+
keyword: "type",
|
|
109
|
+
params: { type: "string" },
|
|
110
|
+
message: "must be string",
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
const longString = "x".repeat(1000);
|
|
114
|
+
const data = { data: longString };
|
|
115
|
+
|
|
116
|
+
const formatted = formatValidationErrors(errors, data, 100);
|
|
117
|
+
|
|
118
|
+
expect(formatted).toContain('(truncated)');
|
|
119
|
+
expect(formatted.length).toBeLessThan(500);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should group multiple errors for the same path", () => {
|
|
123
|
+
const errors = [
|
|
124
|
+
{
|
|
125
|
+
instancePath: "/user",
|
|
126
|
+
schemaPath: "#/properties/user/required",
|
|
127
|
+
keyword: "required",
|
|
128
|
+
params: { missingProperty: "email" },
|
|
129
|
+
message: "must have required property 'email'",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
instancePath: "/user",
|
|
133
|
+
schemaPath: "#/properties/user/required",
|
|
134
|
+
keyword: "required",
|
|
135
|
+
params: { missingProperty: "age" },
|
|
136
|
+
message: "must have required property 'age'",
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
const data = { user: { name: "John" } };
|
|
140
|
+
|
|
141
|
+
const formatted = formatValidationErrors(errors, data);
|
|
142
|
+
|
|
143
|
+
// Should only show the path and data once
|
|
144
|
+
const pathCount = (formatted.match(/Path: \/user/g) || []).length;
|
|
145
|
+
expect(pathCount).toBe(1);
|
|
146
|
+
|
|
147
|
+
// But should list both missing properties
|
|
148
|
+
expect(formatted).toContain('Missing required property: email');
|
|
149
|
+
expect(formatted).toContain('Missing required property: age');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
18
152
|
});
|
|
19
153
|
|
|
20
154
|
const jsonSchemas: JSONSchema7 = {
|
package/src/FlinkApp.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { FlinkPlugin } from "./FlinkPlugin";
|
|
|
19
19
|
import { FlinkRepo } from "./FlinkRepo";
|
|
20
20
|
import { FlinkResponse } from "./FlinkResponse";
|
|
21
21
|
import generateMockData from "./mock-data-generator";
|
|
22
|
-
import { getPathParams, isError } from "./utils";
|
|
22
|
+
import { formatValidationErrors, getPathParams, isError } from "./utils";
|
|
23
23
|
|
|
24
24
|
const ajv = new Ajv();
|
|
25
25
|
addFormats(ajv);
|
|
@@ -504,9 +504,8 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
504
504
|
const valid = validateReq(req.body);
|
|
505
505
|
|
|
506
506
|
if (!valid) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
log.debug(`Invalid json: ${JSON.stringify(req.body)}`);
|
|
507
|
+
const formattedErrors = formatValidationErrors(validateReq.errors, req.body);
|
|
508
|
+
log.warn(`[${req.reqId}] ${methodAndRoute}: Bad request\n${formattedErrors}`);
|
|
510
509
|
|
|
511
510
|
return res.status(400).json({
|
|
512
511
|
status: 400,
|
|
@@ -563,9 +562,8 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
563
562
|
const valid = validateRes(JSON.parse(JSON.stringify(handlerRes.data)));
|
|
564
563
|
|
|
565
564
|
if (!valid) {
|
|
566
|
-
|
|
567
|
-
log.
|
|
568
|
-
// log.debug(JSON.stringify(schema, null, 2));
|
|
565
|
+
const formattedErrors = formatValidationErrors(validateRes.errors, handlerRes.data);
|
|
566
|
+
log.warn(`[${req.reqId}] ${methodAndRoute}: Bad response\n${formattedErrors}`);
|
|
569
567
|
|
|
570
568
|
return res.status(500).json({
|
|
571
569
|
status: 500,
|
package/src/utils.ts
CHANGED
|
@@ -101,3 +101,77 @@ const pathParamsRegex = /:([a-zA-Z0-9]+)/g;
|
|
|
101
101
|
export function getPathParams(path: string) {
|
|
102
102
|
return path.match(pathParamsRegex)?.map((match) => match.slice(1)) || [];
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extracts data at a given JSON path (e.g., "/jobs/5/result")
|
|
107
|
+
* Returns the value at that path, or undefined if not found
|
|
108
|
+
*/
|
|
109
|
+
export function getDataAtPath(data: any, instancePath: string): any {
|
|
110
|
+
if (!instancePath || instancePath === "/") {
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const parts = instancePath.split("/").filter((p) => p.length > 0);
|
|
115
|
+
let current = data;
|
|
116
|
+
|
|
117
|
+
for (const part of parts) {
|
|
118
|
+
if (current === undefined || current === null) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
current = current[part];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return current;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Formats validation errors with context about the problematic data
|
|
129
|
+
* @param errors AJV validation errors
|
|
130
|
+
* @param data The full data object that failed validation
|
|
131
|
+
* @param maxDataLength Maximum length of data to show (default 500)
|
|
132
|
+
*/
|
|
133
|
+
export function formatValidationErrors(errors: any[] | null | undefined, data: any, maxDataLength = 500): string {
|
|
134
|
+
if (!errors || errors.length === 0) {
|
|
135
|
+
return "Unknown validation error";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const formatted: string[] = [];
|
|
139
|
+
|
|
140
|
+
// Group errors by instance path to avoid repetition
|
|
141
|
+
const errorsByPath = new Map<string, any[]>();
|
|
142
|
+
for (const error of errors) {
|
|
143
|
+
const path = error.instancePath || "/";
|
|
144
|
+
if (!errorsByPath.has(path)) {
|
|
145
|
+
errorsByPath.set(path, []);
|
|
146
|
+
}
|
|
147
|
+
errorsByPath.get(path)!.push(error);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
errorsByPath.forEach((pathErrors, path) => {
|
|
151
|
+
const dataAtPath = getDataAtPath(data, path);
|
|
152
|
+
let dataStr = JSON.stringify(dataAtPath);
|
|
153
|
+
|
|
154
|
+
// Truncate if too long
|
|
155
|
+
if (dataStr.length > maxDataLength) {
|
|
156
|
+
dataStr = dataStr.substring(0, maxDataLength) + "... (truncated)";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
formatted.push(`\nPath: ${path}`);
|
|
160
|
+
formatted.push(`Data: ${dataStr}`);
|
|
161
|
+
formatted.push(`Errors:`);
|
|
162
|
+
|
|
163
|
+
for (const error of pathErrors) {
|
|
164
|
+
if (error.keyword === "required") {
|
|
165
|
+
formatted.push(` - Missing required property: ${error.params.missingProperty}`);
|
|
166
|
+
} else if (error.keyword === "type") {
|
|
167
|
+
formatted.push(` - Invalid type at ${error.schemaPath}: expected ${error.params.type}, got ${typeof dataAtPath}`);
|
|
168
|
+
} else if (error.keyword === "anyOf" || error.keyword === "oneOf") {
|
|
169
|
+
formatted.push(` - ${error.message}`);
|
|
170
|
+
} else {
|
|
171
|
+
formatted.push(` - ${error.message} (${error.keyword})`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return formatted.join("\n");
|
|
177
|
+
}
|