@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
|
@@ -409,6 +409,48 @@ export default {}; // Export an empty object to make it a module
|
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Recursively copies an interface and all its dependencies from the same file
|
|
414
|
+
*/
|
|
415
|
+
private copyInterfaceWithDependencies(interfaceDecl: any, handlerFile: SourceFile): void {
|
|
416
|
+
const interfaceName = interfaceDecl.getName?.() || interfaceDecl.getFirstChildByKind(SyntaxKind.Identifier)?.getText();
|
|
417
|
+
if (!interfaceName) return;
|
|
418
|
+
|
|
419
|
+
// Check if already copied
|
|
420
|
+
const existingInterface = this.parsedTsSchemas.find(
|
|
421
|
+
s => s.includes(`interface ${interfaceName} `) || s.includes(`type ${interfaceName} =`)
|
|
422
|
+
);
|
|
423
|
+
if (existingInterface) return;
|
|
424
|
+
|
|
425
|
+
// Copy the interface
|
|
426
|
+
this.parsedTsSchemas.push(interfaceDecl.getText());
|
|
427
|
+
|
|
428
|
+
// Find and recursively copy dependencies from the same file
|
|
429
|
+
// First, find direct type references in this interface
|
|
430
|
+
const typeRefIdentifiers = interfaceDecl
|
|
431
|
+
.getDescendantsOfKind(SyntaxKind.TypeReference)
|
|
432
|
+
.filter((typeRefNode: any) => !!typeRefNode.getFirstChildIfKind(SyntaxKind.Identifier))
|
|
433
|
+
.map((typeRefNode: any) => typeRefNode.getFirstChildIfKindOrThrow(SyntaxKind.Identifier));
|
|
434
|
+
|
|
435
|
+
for (const typeRefIdentifier of typeRefIdentifiers) {
|
|
436
|
+
const typeSymbol = typeRefIdentifier.getSymbol();
|
|
437
|
+
if (typeSymbol) {
|
|
438
|
+
const declaredType = typeSymbol.getDeclaredType();
|
|
439
|
+
const declaration = declaredType.getSymbol()?.getDeclarations()[0];
|
|
440
|
+
if (declaration && declaration.getSourceFile() === handlerFile) {
|
|
441
|
+
// Same file - recursively copy this dependency
|
|
442
|
+
this.copyInterfaceWithDependencies(declaration, handlerFile);
|
|
443
|
+
} else if (declaration && declaration.getSourceFile() !== handlerFile) {
|
|
444
|
+
// Different file - add to imports
|
|
445
|
+
const declaredTypeSymbol = declaredType.getSymbol();
|
|
446
|
+
if (declaredTypeSymbol) {
|
|
447
|
+
this.tsSchemasSymbolsToImports.push(declaredTypeSymbol);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
412
454
|
private async saveIntermediateTsSchema(schema: Type<ts.Type>, handlerFile: SourceFile, suffix: string) {
|
|
413
455
|
if (schema.isAny()) {
|
|
414
456
|
return; // 'any' indicates that no schema is used
|
|
@@ -431,7 +473,7 @@ export default {}; // Export an empty object to make it a module
|
|
|
431
473
|
|
|
432
474
|
if (declaration.getSourceFile() === handlerFile) {
|
|
433
475
|
// Interface is declared within handler file
|
|
434
|
-
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} {
|
|
476
|
+
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} {
|
|
435
477
|
${schema
|
|
436
478
|
.getProperties()
|
|
437
479
|
.map((p) => p.getValueDeclarationOrThrow().getText())
|
|
@@ -439,7 +481,34 @@ export default {}; // Export an empty object to make it a module
|
|
|
439
481
|
}`;
|
|
440
482
|
|
|
441
483
|
for (const typeToImport of getTypesToImport(declaration)) {
|
|
442
|
-
|
|
484
|
+
const typeSymbol = typeToImport.getSymbol();
|
|
485
|
+
if (typeSymbol) {
|
|
486
|
+
const declaredTypeSymbol = typeSymbol.getDeclaredType().getSymbol();
|
|
487
|
+
if (declaredTypeSymbol) {
|
|
488
|
+
this.tsSchemasSymbolsToImports.push(declaredTypeSymbol);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Also check for utility types with indexed access patterns like Partial<Foo["bar"]>
|
|
494
|
+
for (const prop of schema.getProperties()) {
|
|
495
|
+
const propDecl = prop.getValueDeclaration();
|
|
496
|
+
if (propDecl) {
|
|
497
|
+
const propText = propDecl.getText();
|
|
498
|
+
// Match interface names in patterns like: Partial<InterfaceName["prop"]>
|
|
499
|
+
const interfaceNameMatches = propText.match(/\b([A-Z][a-zA-Z0-9]*)\s*\[/g);
|
|
500
|
+
if (interfaceNameMatches) {
|
|
501
|
+
for (const match of interfaceNameMatches) {
|
|
502
|
+
const referencedInterfaceName = match.replace(/\s*\[$/, '').trim();
|
|
503
|
+
// Try to find this interface in the handler file
|
|
504
|
+
const referencedInterfaceDecl = handlerFile.getInterface(referencedInterfaceName) || handlerFile.getTypeAlias(referencedInterfaceName);
|
|
505
|
+
if (referencedInterfaceDecl) {
|
|
506
|
+
// Interface is in same file - copy it and all its dependencies recursively
|
|
507
|
+
this.copyInterfaceWithDependencies(referencedInterfaceDecl, handlerFile);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
443
512
|
}
|
|
444
513
|
} else {
|
|
445
514
|
// Interface is imported from other file
|
|
@@ -468,7 +537,13 @@ export default {}; // Export an empty object to make it a module
|
|
|
468
537
|
}
|
|
469
538
|
|
|
470
539
|
for (const typeToImport of getTypesToImport(declaration)) {
|
|
471
|
-
|
|
540
|
+
const typeSymbol = typeToImport.getSymbol();
|
|
541
|
+
if (typeSymbol) {
|
|
542
|
+
const declaredTypeSymbol = typeSymbol.getDeclaredType().getSymbol();
|
|
543
|
+
if (declaredTypeSymbol) {
|
|
544
|
+
this.tsSchemasSymbolsToImports.push(declaredTypeSymbol);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
472
547
|
}
|
|
473
548
|
}
|
|
474
549
|
} else if (schema.isObject()) {
|
|
@@ -477,8 +552,11 @@ export default {}; // Export an empty object to make it a module
|
|
|
477
552
|
* We need extract `{car: Car}` into its own interface and make sure
|
|
478
553
|
* to import types if needed to
|
|
479
554
|
*/
|
|
480
|
-
|
|
481
|
-
|
|
555
|
+
|
|
556
|
+
// Try to get symbol - it may not exist for utility types (Partial, Omit, Pick, etc.)
|
|
557
|
+
const schemaSymbol = schema.getSymbol();
|
|
558
|
+
const declarations = schemaSymbol?.getDeclarations();
|
|
559
|
+
const declaration = declarations?.[0];
|
|
482
560
|
|
|
483
561
|
// Build property signatures using resolved types instead of source text
|
|
484
562
|
// This ensures generic type parameters are properly expanded
|
|
@@ -528,6 +606,51 @@ export default {}; // Export an empty object to make it a module
|
|
|
528
606
|
}
|
|
529
607
|
}
|
|
530
608
|
}
|
|
609
|
+
|
|
610
|
+
// Check for utility types (Partial, Omit, Pick, etc.) and extract their type arguments
|
|
611
|
+
// For example: Partial<Foo["bar"]> should extract Foo
|
|
612
|
+
// Use a pragmatic text-based approach to find interface references in type expressions
|
|
613
|
+
// Match interface names in patterns like: Partial<InterfaceName["prop"]>, Omit<InterfaceName, "key">, etc.
|
|
614
|
+
const currentPropTypeText = propType.getText(undefined, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
|
|
615
|
+
const interfaceNameMatches = currentPropTypeText.match(/\b([A-Z][a-zA-Z0-9]*)\s*\[/g);
|
|
616
|
+
if (interfaceNameMatches) {
|
|
617
|
+
for (const match of interfaceNameMatches) {
|
|
618
|
+
const interfaceName = match.replace(/\s*\[$/, '').trim();
|
|
619
|
+
// Try to find this interface in the handler file
|
|
620
|
+
const interfaceDecl = handlerFile.getInterface(interfaceName) || handlerFile.getTypeAlias(interfaceName);
|
|
621
|
+
if (interfaceDecl) {
|
|
622
|
+
// Interface is in same file - copy it and all its dependencies recursively
|
|
623
|
+
this.copyInterfaceWithDependencies(interfaceDecl, handlerFile);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Also check regular type arguments (for types like Array<Foo>, Promise<Bar>)
|
|
629
|
+
const typeArgs = propType.getTypeArguments();
|
|
630
|
+
if (typeArgs && typeArgs.length > 0) {
|
|
631
|
+
for (const typeArg of typeArgs) {
|
|
632
|
+
const argSymbol = typeArg.getSymbol();
|
|
633
|
+
if (argSymbol) {
|
|
634
|
+
const argDeclaration = argSymbol.getDeclarations()[0];
|
|
635
|
+
if (argDeclaration && argDeclaration.getSourceFile() !== handlerFile) {
|
|
636
|
+
this.tsSchemasSymbolsToImports.push(argSymbol);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// If we have a declaration, check if we need to import any types
|
|
644
|
+
if (declaration) {
|
|
645
|
+
for (const typeToImport of getTypesToImport(declaration)) {
|
|
646
|
+
const typeSymbol = typeToImport.getSymbol();
|
|
647
|
+
if (typeSymbol) {
|
|
648
|
+
const declaredTypeSymbol = typeSymbol.getDeclaredType().getSymbol();
|
|
649
|
+
if (declaredTypeSymbol) {
|
|
650
|
+
this.tsSchemasSymbolsToImports.push(declaredTypeSymbol);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
531
654
|
}
|
|
532
655
|
|
|
533
656
|
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} { ${propertySignatures.join(";\n")} }`;
|
package/src/utils.ts
CHANGED
|
@@ -75,6 +75,7 @@ export function getHttpMethodFromHandlerName(handlerFilename: string) {
|
|
|
75
75
|
if (handlerFilename.startsWith(HttpMethod.post)) return HttpMethod.post;
|
|
76
76
|
if (handlerFilename.startsWith(HttpMethod.put)) return HttpMethod.put;
|
|
77
77
|
if (handlerFilename.startsWith(HttpMethod.delete)) return HttpMethod.delete;
|
|
78
|
+
if (handlerFilename.startsWith(HttpMethod.patch)) return HttpMethod.patch;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
export function getJsDocComment(comment: string) {
|
|
@@ -100,3 +101,77 @@ const pathParamsRegex = /:([a-zA-Z0-9]+)/g;
|
|
|
100
101
|
export function getPathParams(path: string) {
|
|
101
102
|
return path.match(pathParamsRegex)?.map((match) => match.slice(1)) || [];
|
|
102
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
|
+
}
|