@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.
@@ -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
- this.tsSchemasSymbolsToImports.push(typeToImport.getSymbolOrThrow().getDeclaredType().getSymbolOrThrow());
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
- this.tsSchemasSymbolsToImports.push(typeToImport.getSymbolOrThrow().getDeclaredType().getSymbolOrThrow());
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
- const declarations = schema.getSymbolOrThrow().getDeclarations();
481
- const declaration = declarations[0];
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
+ }