@flink-app/flink 0.13.3 → 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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/CHANGELOG.md +18 -0
- package/dist/src/FlinkApp.js +5 -6
- package/dist/src/FlinkHttpHandler.d.ts +2 -1
- package/dist/src/FlinkHttpHandler.js +1 -0
- package/dist/src/TypeScriptCompiler.d.ts +4 -0
- package/dist/src/TypeScriptCompiler.js +123 -11
- package/dist/src/utils.d.ts +12 -0
- package/dist/src/utils.js +73 -0
- package/package.json +1 -1
- package/spec/mock-project/dist/src/handlers/PatchCar.js +58 -0
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +76 -0
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +58 -0
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +59 -0
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +59 -0
- package/spec/mock-project/src/handlers/PatchCar.ts +25 -0
- package/spec/mock-project/src/handlers/PatchOnboardingSession.ts +66 -0
- package/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.ts +79 -0
- package/spec/mock-project/src/handlers/PatchProductWithIntersection.ts +49 -0
- package/spec/mock-project/src/handlers/PatchUserWithUnion.ts +46 -0
- package/spec/utils.spec.ts +135 -1
- package/src/FlinkApp.ts +5 -7
- package/src/FlinkHttpHandler.ts +1 -0
- package/src/TypeScriptCompiler.ts +128 -5
- package/src/utils.ts +75 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.__schemas = exports.__params = exports.__query = exports.__file = exports.__assumedHttpMethod = exports.Route = void 0;
|
|
40
|
+
var flink_1 = require("@flink-app/flink");
|
|
41
|
+
exports.Route = {
|
|
42
|
+
path: "/product/:productId",
|
|
43
|
+
method: flink_1.HttpMethod.patch,
|
|
44
|
+
};
|
|
45
|
+
var PatchProductWithIntersection = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
46
|
+
var req = _b.req;
|
|
47
|
+
return __generator(this, function (_c) {
|
|
48
|
+
return [2 /*return*/, {
|
|
49
|
+
data: {
|
|
50
|
+
name: "Test Product",
|
|
51
|
+
description: "Test Description",
|
|
52
|
+
price: req.body.update || 99.99,
|
|
53
|
+
},
|
|
54
|
+
}];
|
|
55
|
+
});
|
|
56
|
+
}); };
|
|
57
|
+
exports.default = PatchProductWithIntersection;
|
|
58
|
+
exports.__assumedHttpMethod = "patch", exports.__file = "PatchProductWithIntersection.ts", exports.__query = [], exports.__params = [{ description: "", name: "productId" }];
|
|
59
|
+
exports.__schemas = { reqSchema: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "update": { "type": "object", "additionalProperties": false }, "fullUpdate": { "type": "object", "additionalProperties": false, "properties": { "notes": { "type": "string" }, "stock": { "type": "number" }, "warehouse": { "type": "string" }, "sku": { "type": "string" }, "name": { "type": "string" }, "description": { "type": "string" }, "price": { "type": "number" } }, "required": ["notes"] }, "metadataUpdate": { "type": "object", "additionalProperties": false, "properties": { "updatedBy": { "type": "string" } }, "required": ["updatedBy"] } }, "additionalProperties": false, "definitions": {} }, resSchema: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "price": { "type": "number" } }, "required": ["name", "description", "price"], "additionalProperties": false, "definitions": {} } };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.__schemas = exports.__params = exports.__query = exports.__file = exports.__assumedHttpMethod = exports.Route = void 0;
|
|
40
|
+
var flink_1 = require("@flink-app/flink");
|
|
41
|
+
exports.Route = {
|
|
42
|
+
path: "/user/:userId",
|
|
43
|
+
method: flink_1.HttpMethod.patch,
|
|
44
|
+
};
|
|
45
|
+
var PatchUserWithUnion = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
46
|
+
var req = _b.req;
|
|
47
|
+
return __generator(this, function (_c) {
|
|
48
|
+
return [2 /*return*/, {
|
|
49
|
+
data: {
|
|
50
|
+
firstName: "Test",
|
|
51
|
+
lastName: "User",
|
|
52
|
+
bio: req.body.data || "Default bio",
|
|
53
|
+
},
|
|
54
|
+
}];
|
|
55
|
+
});
|
|
56
|
+
}); };
|
|
57
|
+
exports.default = PatchUserWithUnion;
|
|
58
|
+
exports.__assumedHttpMethod = "patch", exports.__file = "PatchUserWithUnion.ts", exports.__query = [], exports.__params = [{ description: "", name: "userId" }];
|
|
59
|
+
exports.__schemas = { reqSchema: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "data": { "type": "object", "additionalProperties": false }, "profileUpdate": { "anyOf": [{ "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" }, "bio": { "type": "string" } }, "additionalProperties": false }, { "type": "object", "properties": { "theme": { "type": "string" }, "notifications": { "type": "boolean" }, "language": { "type": "string" } }, "additionalProperties": false }] } }, "additionalProperties": false, "definitions": {} }, resSchema: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" }, "bio": { "type": "string" } }, "required": ["firstName", "lastName", "bio"], "additionalProperties": false, "definitions": {} } };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Handler, HttpMethod, RouteProps } from "@flink-app/flink";
|
|
2
|
+
|
|
3
|
+
export const Route: RouteProps = {
|
|
4
|
+
path: "/car/:id",
|
|
5
|
+
method: HttpMethod.patch,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
interface Car {
|
|
9
|
+
model: string;
|
|
10
|
+
year: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Params = { id: string };
|
|
14
|
+
type PatchCarReq = Partial<Car>;
|
|
15
|
+
|
|
16
|
+
const PatchCar: Handler<any, PatchCarReq, Car, Params> = async ({ req }) => {
|
|
17
|
+
return {
|
|
18
|
+
data: {
|
|
19
|
+
model: req.body.model || "Updated Model",
|
|
20
|
+
year: req.body.year || 2024,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default PatchCar;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Handler, HttpMethod, RouteProps } from "@flink-app/flink";
|
|
2
|
+
|
|
3
|
+
export const Route: RouteProps = {
|
|
4
|
+
path: "/onboarding/:sessionId",
|
|
5
|
+
method: HttpMethod.patch,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Simulating a complex nested structure like in your example
|
|
9
|
+
interface OnboardingSession {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
status: string;
|
|
12
|
+
extractedData: {
|
|
13
|
+
companyName: string;
|
|
14
|
+
orgNumber: string;
|
|
15
|
+
address: {
|
|
16
|
+
street: string;
|
|
17
|
+
city: string;
|
|
18
|
+
postalCode: string;
|
|
19
|
+
};
|
|
20
|
+
contactInfo: {
|
|
21
|
+
email: string;
|
|
22
|
+
phone: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
metadata: {
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
updatedAt: Date;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Test case: Partial of a nested property using bracket notation
|
|
32
|
+
interface PatchOnboardingSessionReq {
|
|
33
|
+
status?: string;
|
|
34
|
+
extractedData?: Partial<OnboardingSession["extractedData"]>;
|
|
35
|
+
metadata?: Partial<OnboardingSession["metadata"]>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Params = { sessionId: string };
|
|
39
|
+
|
|
40
|
+
const PatchOnboardingSession: Handler<any, PatchOnboardingSessionReq, OnboardingSession, Params> = async ({ req }) => {
|
|
41
|
+
return {
|
|
42
|
+
data: {
|
|
43
|
+
sessionId: req.params.sessionId,
|
|
44
|
+
status: req.body.status || "pending",
|
|
45
|
+
extractedData: {
|
|
46
|
+
companyName: req.body.extractedData?.companyName || "Test Company",
|
|
47
|
+
orgNumber: req.body.extractedData?.orgNumber || "123456",
|
|
48
|
+
address: {
|
|
49
|
+
street: "Main St",
|
|
50
|
+
city: "Stockholm",
|
|
51
|
+
postalCode: "12345",
|
|
52
|
+
},
|
|
53
|
+
contactInfo: {
|
|
54
|
+
email: "test@example.com",
|
|
55
|
+
phone: "+46701234567",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
metadata: {
|
|
59
|
+
createdAt: new Date(),
|
|
60
|
+
updatedAt: new Date(),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default PatchOnboardingSession;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Handler, HttpMethod, RouteProps } from "@flink-app/flink";
|
|
2
|
+
|
|
3
|
+
export const Route: RouteProps = {
|
|
4
|
+
path: "/order/:orderId",
|
|
5
|
+
method: HttpMethod.patch,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Complex nested structures for testing edge cases
|
|
9
|
+
interface OrderCustomer {
|
|
10
|
+
customerId: string;
|
|
11
|
+
name: string;
|
|
12
|
+
email: string;
|
|
13
|
+
address: {
|
|
14
|
+
street: string;
|
|
15
|
+
city: string;
|
|
16
|
+
country: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface OrderItem {
|
|
21
|
+
productId: string;
|
|
22
|
+
quantity: number;
|
|
23
|
+
price: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface OrderItems {
|
|
27
|
+
items: Array<OrderItem>;
|
|
28
|
+
subtotal: number;
|
|
29
|
+
tax: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface OrderShipping {
|
|
33
|
+
method: string;
|
|
34
|
+
trackingNumber: string;
|
|
35
|
+
estimatedDelivery: Date;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface OrderPayment {
|
|
39
|
+
method: string;
|
|
40
|
+
status: string;
|
|
41
|
+
transactionId: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Test case: Complex combinations of utility types
|
|
45
|
+
interface PatchOrderWithComplexTypesReq {
|
|
46
|
+
// Nested indexed access - should extract OrderCustomer
|
|
47
|
+
customerCity?: Partial<OrderCustomer["address"]>;
|
|
48
|
+
|
|
49
|
+
// Omit with indexed access - should extract OrderItems
|
|
50
|
+
items?: Omit<OrderItems, "tax"> & Partial<OrderItems["subtotal"]>;
|
|
51
|
+
|
|
52
|
+
// Pick with indexed access - should extract OrderShipping
|
|
53
|
+
shipping?: Pick<OrderShipping, "method"> & Partial<OrderShipping["trackingNumber"]>;
|
|
54
|
+
|
|
55
|
+
// Union of Partial with indexed access
|
|
56
|
+
paymentOrShipping?:
|
|
57
|
+
Partial<OrderPayment["status"]> | Partial<OrderShipping["method"]>;
|
|
58
|
+
|
|
59
|
+
// Array of Partial types
|
|
60
|
+
itemUpdates?: Array<Partial<OrderItem>>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type Params = { orderId: string };
|
|
64
|
+
|
|
65
|
+
interface OrderResponse {
|
|
66
|
+
orderId: string;
|
|
67
|
+
status: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const PatchOrderWithComplexTypes: Handler<any, PatchOrderWithComplexTypesReq, OrderResponse, Params> = async ({ req }) => {
|
|
71
|
+
return {
|
|
72
|
+
data: {
|
|
73
|
+
orderId: req.params.orderId,
|
|
74
|
+
status: "updated",
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default PatchOrderWithComplexTypes;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Handler, HttpMethod, RouteProps } from "@flink-app/flink";
|
|
2
|
+
|
|
3
|
+
export const Route: RouteProps = {
|
|
4
|
+
path: "/product/:productId",
|
|
5
|
+
method: HttpMethod.patch,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Base interfaces for testing intersection types
|
|
9
|
+
interface ProductDetails {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
price: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ProductInventory {
|
|
16
|
+
stock: number;
|
|
17
|
+
warehouse: string;
|
|
18
|
+
sku: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ProductMetadata {
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
updatedAt: Date;
|
|
24
|
+
createdBy: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Test case: Intersection type with Partial indexed access patterns
|
|
28
|
+
interface PatchProductWithIntersectionReq {
|
|
29
|
+
// Intersection of Partial types - should extract ProductDetails and ProductInventory
|
|
30
|
+
update?: Partial<ProductDetails["price"]> & Partial<ProductInventory["stock"]>;
|
|
31
|
+
// More realistic intersection pattern
|
|
32
|
+
fullUpdate?: Partial<ProductDetails> & Partial<ProductInventory> & { notes: string };
|
|
33
|
+
// Intersection with indexed access
|
|
34
|
+
metadataUpdate?: Partial<ProductMetadata["updatedAt"]> & { updatedBy: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Params = { productId: string };
|
|
38
|
+
|
|
39
|
+
const PatchProductWithIntersection: Handler<any, PatchProductWithIntersectionReq, ProductDetails, Params> = async ({ req }) => {
|
|
40
|
+
return {
|
|
41
|
+
data: {
|
|
42
|
+
name: "Test Product",
|
|
43
|
+
description: "Test Description",
|
|
44
|
+
price: (req.body.update as any) || 99.99,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default PatchProductWithIntersection;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Handler, HttpMethod, RouteProps } from "@flink-app/flink";
|
|
2
|
+
|
|
3
|
+
export const Route: RouteProps = {
|
|
4
|
+
path: "/user/:userId",
|
|
5
|
+
method: HttpMethod.patch,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Base interfaces for testing union types
|
|
9
|
+
interface UserProfile {
|
|
10
|
+
firstName: string;
|
|
11
|
+
lastName: string;
|
|
12
|
+
bio: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UserSettings {
|
|
16
|
+
theme: string;
|
|
17
|
+
notifications: boolean;
|
|
18
|
+
language: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface UserPreferences {
|
|
22
|
+
emailFrequency: string;
|
|
23
|
+
newsletter: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Test case: Union type with multiple Partial indexed access patterns
|
|
27
|
+
interface PatchUserWithUnionReq {
|
|
28
|
+
// Union of Partial types - should extract UserProfile, UserSettings, UserPreferences
|
|
29
|
+
data?: Partial<UserProfile["firstName"]> | Partial<UserSettings["theme"]> | Partial<UserPreferences["emailFrequency"]>;
|
|
30
|
+
// More realistic union pattern
|
|
31
|
+
profileUpdate?: Partial<UserProfile> | Partial<UserSettings>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Params = { userId: string };
|
|
35
|
+
|
|
36
|
+
const PatchUserWithUnion: Handler<any, PatchUserWithUnionReq, UserProfile, Params> = async ({ req }) => {
|
|
37
|
+
return {
|
|
38
|
+
data: {
|
|
39
|
+
firstName: "Test",
|
|
40
|
+
lastName: "User",
|
|
41
|
+
bio: req.body.data as string || "Default bio",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default PatchUserWithUnion;
|
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,
|