@flink-app/flink 0.14.3 → 2.0.0-alpha.100
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 +1051 -0
- package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
- package/SIMPLE_AST_FEASIBILITY.md +570 -0
- package/bin/flink.ts +13 -2
- package/cli/build.ts +24 -44
- package/cli/clean.ts +13 -25
- package/cli/cli-utils.ts +190 -17
- package/cli/dev.ts +252 -0
- package/cli/loadEnvFiles.ts +116 -0
- package/cli/run.ts +45 -62
- package/dist/bin/flink.js +61 -2
- package/dist/cli/build.js +20 -25
- package/dist/cli/clean.js +12 -10
- package/dist/cli/cli-utils.d.ts +34 -3
- package/dist/cli/cli-utils.js +193 -12
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.js +279 -0
- package/dist/cli/loadEnvFiles.d.ts +30 -0
- package/dist/cli/loadEnvFiles.js +113 -0
- package/dist/cli/run.js +47 -46
- package/dist/src/DependencyTracker.d.ts +44 -0
- package/dist/src/DependencyTracker.js +239 -0
- package/dist/src/FlinkApp.d.ts +163 -10
- package/dist/src/FlinkApp.js +847 -184
- package/dist/src/FlinkContext.d.ts +41 -0
- package/dist/src/FlinkErrors.d.ts +19 -6
- package/dist/src/FlinkErrors.js +36 -42
- package/dist/src/FlinkHttpHandler.d.ts +219 -26
- package/dist/src/FlinkHttpHandler.js +37 -1
- package/dist/src/FlinkJob.d.ts +10 -0
- package/dist/src/FlinkLog.d.ts +82 -18
- package/dist/src/FlinkLog.js +165 -13
- package/dist/src/FlinkLogFactory.d.ts +288 -0
- package/dist/src/FlinkLogFactory.js +619 -0
- package/dist/src/FlinkRepo.d.ts +10 -2
- package/dist/src/FlinkRepo.js +11 -1
- package/dist/src/FlinkRequestContext.d.ts +63 -0
- package/dist/src/FlinkRequestContext.js +74 -0
- package/dist/src/FlinkResponse.d.ts +6 -0
- package/dist/src/FlinkService.d.ts +38 -0
- package/dist/src/FlinkService.js +46 -0
- package/dist/src/LeaderElection.d.ts +45 -0
- package/dist/src/LeaderElection.js +269 -0
- package/dist/src/SchemaCache.d.ts +84 -0
- package/dist/src/SchemaCache.js +289 -0
- package/dist/src/TypeScriptCompiler.d.ts +161 -51
- package/dist/src/TypeScriptCompiler.js +1253 -617
- package/dist/src/TypeScriptUtils.js +4 -0
- package/dist/src/ai/AgentRunner.d.ts +39 -0
- package/dist/src/ai/AgentRunner.js +760 -0
- package/dist/src/ai/ConversationAgent.d.ts +279 -0
- package/dist/src/ai/ConversationAgent.js +404 -0
- package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
- package/dist/src/ai/ConversationFlinkAgent.js +404 -0
- package/dist/src/ai/FlinkAgent.d.ts +690 -0
- package/dist/src/ai/FlinkAgent.js +729 -0
- package/dist/src/ai/FlinkTool.d.ts +135 -0
- package/dist/src/ai/FlinkTool.js +2 -0
- package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
- package/dist/src/ai/InMemoryConversationAgent.js +209 -0
- package/dist/src/ai/LLMAdapter.d.ts +148 -0
- package/dist/src/ai/LLMAdapter.js +2 -0
- package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
- package/dist/src/ai/PersistentFlinkAgent.js +403 -0
- package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
- package/dist/src/ai/SubAgentExecutor.js +223 -0
- package/dist/src/ai/ToolExecutor.d.ts +64 -0
- package/dist/src/ai/ToolExecutor.js +497 -0
- package/dist/src/ai/agentInstructions.d.ts +68 -0
- package/dist/src/ai/agentInstructions.js +286 -0
- package/dist/src/ai/index.d.ts +8 -0
- package/dist/src/ai/index.js +26 -0
- package/dist/src/ai/instructionFileLoader.d.ts +44 -0
- package/dist/src/ai/instructionFileLoader.js +179 -0
- package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
- package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
- package/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +17 -0
- package/dist/src/loadPluginSchemas.d.ts +45 -0
- package/dist/src/loadPluginSchemas.js +143 -0
- package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
- package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
- package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
- package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
- package/dist/src/schema-extraction/index.d.ts +2 -0
- package/dist/src/schema-extraction/index.js +20 -0
- package/dist/src/schema-extraction/types.d.ts +31 -0
- package/dist/src/schema-extraction/types.js +2 -0
- package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
- package/dist/src/utils/loadFlinkConfig.js +77 -0
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.js +52 -0
- package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
- package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
- package/dist/src/workers/WorkerPool.d.ts +60 -0
- package/dist/src/workers/WorkerPool.js +306 -0
- package/examples/logging-hierarchical-example.ts +125 -0
- package/package.json +29 -4
- package/readme.md +499 -0
- package/spec/AgentDescendantDetection.spec.ts +335 -0
- package/spec/AgentDuplicateDetection.spec.ts +112 -0
- package/spec/AgentObserver.spec.ts +266 -0
- package/spec/AgentRunner.spec.ts +1062 -0
- package/spec/AsyncLocalStorageContext.spec.ts +223 -0
- package/spec/ConversationHooks.spec.ts +257 -0
- package/spec/FlinkAgent.spec.ts +681 -0
- package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
- package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
- package/spec/FlinkApp.onError.spec.ts +1 -2
- package/spec/FlinkApp.query.spec.ts +107 -0
- package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
- package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
- package/spec/FlinkApp.validationMode.spec.ts +155 -0
- package/spec/FlinkJob.spec.ts +171 -0
- package/spec/FlinkLogFactory.spec.ts +337 -0
- package/spec/FlinkRepo.spec.ts +1 -1
- package/spec/LeaderElection.spec.ts +174 -0
- package/spec/StreamingIntegration.spec.ts +139 -0
- package/spec/ToolExecutor.spec.ts +465 -0
- package/spec/TypeScriptCompiler.spec.ts +1 -1
- package/spec/TypeScriptSourceParser.spec.ts +1215 -0
- package/spec/TypeScriptTokenizer.spec.ts +366 -0
- package/spec/ai/ContextCompaction.spec.ts +405 -0
- package/spec/ai/ConversationAgent.spec.ts +520 -0
- package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
- package/spec/ai/agentInstructions.spec.ts +358 -0
- package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
- package/spec/fixtures/agent-instructions/simple.md +3 -0
- package/spec/fixtures/agent-instructions/template.md +18 -0
- package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
- package/spec/mock-project/dist/.tsbuildinfo +1 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
- package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
- package/spec/mock-project/dist/src/FlinkApp.js +1000 -0
- package/spec/mock-project/dist/src/FlinkContext.js +2 -0
- package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
- package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
- package/spec/mock-project/dist/src/FlinkJob.js +2 -0
- package/spec/mock-project/dist/src/FlinkLog.js +119 -0
- package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -0
- package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
- package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
- package/spec/mock-project/dist/src/FlinkRequestContext.js +74 -0
- package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
- package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
- package/spec/mock-project/dist/src/ai/AgentRunner.js +632 -0
- package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
- package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
- package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
- package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
- package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
- package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
- package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
- package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
- package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
- package/spec/mock-project/dist/src/handlers/GetCar.js +26 -52
- package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
- package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
- package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
- package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
- package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
- package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
- package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/spec/mock-project/dist/src/index.js +52 -76
- package/spec/mock-project/dist/src/index.js.map +1 -0
- package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
- package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
- package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/Car.js +3 -1
- package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
- package/spec/mock-project/dist/src/utils.js +290 -0
- package/spec/mock-project/tsconfig.json +6 -1
- package/spec/schema-generation-nested-objects.spec.ts +97 -0
- package/spec/testHelpers.ts +49 -0
- package/spec/utils.caseConversion.spec.ts +78 -0
- package/spec/utils.spec.ts +13 -13
- package/src/DependencyTracker.ts +166 -0
- package/src/FlinkApp.ts +919 -155
- package/src/FlinkContext.ts +43 -0
- package/src/FlinkErrors.ts +32 -12
- package/src/FlinkHttpHandler.ts +246 -28
- package/src/FlinkJob.ts +11 -0
- package/src/FlinkLog.ts +119 -12
- package/src/FlinkLogFactory.ts +699 -0
- package/src/FlinkRepo.ts +10 -3
- package/src/FlinkRequestContext.ts +95 -0
- package/src/FlinkResponse.ts +6 -0
- package/src/FlinkService.ts +49 -0
- package/src/LeaderElection.ts +203 -0
- package/src/SchemaCache.ts +232 -0
- package/src/TypeScriptCompiler.ts +1347 -610
- package/src/TypeScriptUtils.ts +5 -0
- package/src/ai/AgentRunner.ts +646 -0
- package/src/ai/ConversationAgent.ts +413 -0
- package/src/ai/FlinkAgent.ts +1069 -0
- package/src/ai/FlinkTool.ts +165 -0
- package/src/ai/InMemoryConversationAgent.ts +149 -0
- package/src/ai/LLMAdapter.ts +126 -0
- package/src/ai/ToolExecutor.ts +485 -0
- package/src/ai/agentInstructions.ts +245 -0
- package/src/ai/index.ts +8 -0
- package/src/ai/instructionFileLoader.ts +156 -0
- package/src/auth/FlinkAuthPlugin.ts +2 -1
- package/src/handlers/StreamWriterFactory.ts +84 -0
- package/src/index.ts +14 -0
- package/src/loadPluginSchemas.ts +141 -0
- package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
- package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
- package/src/schema-extraction/index.ts +2 -0
- package/src/schema-extraction/types.ts +34 -0
- package/src/utils/loadFlinkConfig.ts +89 -0
- package/src/utils.ts +52 -0
- package/tsconfig.json +6 -1
|
@@ -0,0 +1,1215 @@
|
|
|
1
|
+
import { TypeScriptSourceParser } from "../src/schema-extraction";
|
|
2
|
+
|
|
3
|
+
describe("TypeScriptSourceParser", () => {
|
|
4
|
+
describe("detectSchemaType", () => {
|
|
5
|
+
it("should detect Zod schemas", () => {
|
|
6
|
+
const source = `
|
|
7
|
+
export const Tool: FlinkToolProps = {
|
|
8
|
+
inputSchema: z.object({ name: z.string() })
|
|
9
|
+
}
|
|
10
|
+
`;
|
|
11
|
+
const result = TypeScriptSourceParser.detectSchemaType(source);
|
|
12
|
+
expect(result.hasZodSchemas).toBe(true);
|
|
13
|
+
expect(result.shouldSkipTypeScriptExtraction).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should detect JSON schemas", () => {
|
|
17
|
+
const source = `
|
|
18
|
+
export const Tool: FlinkToolProps = {
|
|
19
|
+
inputJsonSchema: { type: "object" }
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
const result = TypeScriptSourceParser.detectSchemaType(source);
|
|
23
|
+
expect(result.hasJsonSchemas).toBe(true);
|
|
24
|
+
expect(result.shouldSkipTypeScriptExtraction).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should detect neither for TypeScript-only tools", () => {
|
|
28
|
+
const source = `
|
|
29
|
+
const handler: FlinkTool<Ctx, Input, Output> = async () => {};
|
|
30
|
+
`;
|
|
31
|
+
const result = TypeScriptSourceParser.detectSchemaType(source);
|
|
32
|
+
expect(result.hasZodSchemas).toBe(false);
|
|
33
|
+
expect(result.hasJsonSchemas).toBe(false);
|
|
34
|
+
expect(result.shouldSkipTypeScriptExtraction).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("parseFlinkToolTypeArgs", () => {
|
|
39
|
+
it("should extract type arguments from FlinkTool declaration", () => {
|
|
40
|
+
const source = `const handler: FlinkTool<Ctx, CreateDocInput, ToolResult<DocOutput>> = async () => {};`;
|
|
41
|
+
const result = TypeScriptSourceParser.parseFlinkToolTypeArgs(source);
|
|
42
|
+
|
|
43
|
+
expect(result).toEqual({
|
|
44
|
+
contextType: "Ctx",
|
|
45
|
+
inputType: "CreateDocInput",
|
|
46
|
+
outputType: "ToolResult<DocOutput>",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should handle inline object types", () => {
|
|
51
|
+
const source = `const handler: FlinkTool<Ctx, { foo: string }, ToolResult<Output>> = async () => {};`;
|
|
52
|
+
const result = TypeScriptSourceParser.parseFlinkToolTypeArgs(source);
|
|
53
|
+
|
|
54
|
+
expect(result?.inputType).toBe("{ foo: string }");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should handle empty object input", () => {
|
|
58
|
+
const source = `const handler: FlinkTool<Ctx, {}, ToolResult<Output>> = async () => {};`;
|
|
59
|
+
const result = TypeScriptSourceParser.parseFlinkToolTypeArgs(source);
|
|
60
|
+
|
|
61
|
+
expect(result?.inputType).toBe("{}");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should return null if no FlinkTool found", () => {
|
|
65
|
+
const source = `const handler = async () => {};`;
|
|
66
|
+
const result = TypeScriptSourceParser.parseFlinkToolTypeArgs(source);
|
|
67
|
+
|
|
68
|
+
expect(result).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should handle whitespace variations", () => {
|
|
72
|
+
const source = `const handler: FlinkTool<Ctx,Input,Output> = async () => {};`;
|
|
73
|
+
const result = TypeScriptSourceParser.parseFlinkToolTypeArgs(source);
|
|
74
|
+
|
|
75
|
+
expect(result).toEqual({
|
|
76
|
+
contextType: "Ctx",
|
|
77
|
+
inputType: "Input",
|
|
78
|
+
outputType: "Output",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("shouldGenerateSchema", () => {
|
|
84
|
+
it("should return false for 'any'", () => {
|
|
85
|
+
expect(TypeScriptSourceParser.shouldGenerateSchema("any")).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should return false for primitives", () => {
|
|
89
|
+
expect(TypeScriptSourceParser.shouldGenerateSchema("string")).toBe(false);
|
|
90
|
+
expect(TypeScriptSourceParser.shouldGenerateSchema("number")).toBe(false);
|
|
91
|
+
expect(TypeScriptSourceParser.shouldGenerateSchema("boolean")).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return true for inline objects", () => {
|
|
95
|
+
expect(TypeScriptSourceParser.shouldGenerateSchema("{}")).toBe(true);
|
|
96
|
+
expect(TypeScriptSourceParser.shouldGenerateSchema("{ foo: string }")).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should return true for named types", () => {
|
|
100
|
+
expect(TypeScriptSourceParser.shouldGenerateSchema("User")).toBe(true);
|
|
101
|
+
expect(TypeScriptSourceParser.shouldGenerateSchema("CreateDocInput")).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("unwrapToolResultType", () => {
|
|
106
|
+
it("should extract type from ToolResult<T>", () => {
|
|
107
|
+
expect(TypeScriptSourceParser.unwrapToolResultType("ToolResult<DocOutput>")).toBe("DocOutput");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should handle complex generic types", () => {
|
|
111
|
+
expect(TypeScriptSourceParser.unwrapToolResultType("ToolResult<Array<Doc>>")).toBe("Array<Doc>");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should return as-is if not ToolResult", () => {
|
|
115
|
+
expect(TypeScriptSourceParser.unwrapToolResultType("DocOutput")).toBe("DocOutput");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("convertInlineObjectToInterface", () => {
|
|
120
|
+
it("should convert empty object", () => {
|
|
121
|
+
const result = TypeScriptSourceParser.convertInlineObjectToInterface("{}", "MyInput");
|
|
122
|
+
expect(result).toBe("export interface MyInput {}");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should convert single property", () => {
|
|
126
|
+
const result = TypeScriptSourceParser.convertInlineObjectToInterface("{ foo: string }", "MyInput");
|
|
127
|
+
expect(result).toContain("export interface MyInput");
|
|
128
|
+
expect(result).toContain("foo: string;");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should convert multiple properties", () => {
|
|
132
|
+
const result = TypeScriptSourceParser.convertInlineObjectToInterface("{ foo: string; bar: number }", "MyInput");
|
|
133
|
+
expect(result).toContain("export interface MyInput");
|
|
134
|
+
expect(result).toContain("foo: string;");
|
|
135
|
+
expect(result).toContain("bar: number;");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should handle properties with commas", () => {
|
|
139
|
+
const result = TypeScriptSourceParser.convertInlineObjectToInterface("{ foo: string, bar: number }", "MyInput");
|
|
140
|
+
expect(result).toContain("foo: string");
|
|
141
|
+
expect(result).toContain("bar: number");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("findTypeDefinition", () => {
|
|
146
|
+
it("should find interface definitions", () => {
|
|
147
|
+
const source = `
|
|
148
|
+
interface User {
|
|
149
|
+
name: string;
|
|
150
|
+
age: number;
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "User");
|
|
154
|
+
|
|
155
|
+
expect(result).not.toBeNull();
|
|
156
|
+
expect(result?.kind).toBe("interface");
|
|
157
|
+
expect(result?.name).toBe("User");
|
|
158
|
+
expect(result?.definition).toContain("interface User");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should find type alias definitions", () => {
|
|
162
|
+
const source = `
|
|
163
|
+
type UserId = string;
|
|
164
|
+
`;
|
|
165
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "UserId");
|
|
166
|
+
|
|
167
|
+
expect(result).not.toBeNull();
|
|
168
|
+
expect(result?.kind).toBe("type");
|
|
169
|
+
expect(result?.name).toBe("UserId");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should find interfaces with JSDoc", () => {
|
|
173
|
+
const source = `
|
|
174
|
+
/**
|
|
175
|
+
* User model
|
|
176
|
+
*/
|
|
177
|
+
interface User {
|
|
178
|
+
name: string;
|
|
179
|
+
}
|
|
180
|
+
`;
|
|
181
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "User");
|
|
182
|
+
|
|
183
|
+
expect(result).not.toBeNull();
|
|
184
|
+
expect(result?.definition).toContain("/**");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should return null for non-existent types", () => {
|
|
188
|
+
const source = `interface User { name: string; }`;
|
|
189
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "NonExistent");
|
|
190
|
+
|
|
191
|
+
expect(result).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should handle interfaces with generics", () => {
|
|
195
|
+
const source = `
|
|
196
|
+
interface List<T> {
|
|
197
|
+
items: T[];
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
200
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "List");
|
|
201
|
+
|
|
202
|
+
expect(result).not.toBeNull();
|
|
203
|
+
expect(result?.definition).toContain("interface List<T>");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should handle interfaces with extends", () => {
|
|
207
|
+
const source = `
|
|
208
|
+
interface Admin extends User {
|
|
209
|
+
role: string;
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
212
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "Admin");
|
|
213
|
+
|
|
214
|
+
expect(result).not.toBeNull();
|
|
215
|
+
expect(result?.definition).toContain("extends User");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should handle interfaces with nested objects", () => {
|
|
219
|
+
const source = `
|
|
220
|
+
interface ToolOutput {
|
|
221
|
+
content: string;
|
|
222
|
+
metadata?: {
|
|
223
|
+
title: string;
|
|
224
|
+
documentId: string;
|
|
225
|
+
modifiedTime?: string | null;
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "ToolOutput");
|
|
230
|
+
|
|
231
|
+
expect(result).not.toBeNull();
|
|
232
|
+
expect(result?.kind).toBe("interface");
|
|
233
|
+
expect(result?.definition).toContain("interface ToolOutput");
|
|
234
|
+
expect(result?.definition).toContain("content: string");
|
|
235
|
+
expect(result?.definition).toContain("metadata?:");
|
|
236
|
+
expect(result?.definition).toContain("title: string");
|
|
237
|
+
expect(result?.definition).toContain("modifiedTime?: string | null");
|
|
238
|
+
|
|
239
|
+
// Critical: Must include BOTH closing braces
|
|
240
|
+
const braceCount = (result?.definition.match(/}/g) || []).length;
|
|
241
|
+
expect(braceCount).toBe(2); // One for nested object, one for interface
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should handle deeply nested objects", () => {
|
|
245
|
+
const source = `
|
|
246
|
+
interface Complex {
|
|
247
|
+
level1: {
|
|
248
|
+
level2: {
|
|
249
|
+
level3: string;
|
|
250
|
+
};
|
|
251
|
+
other: number;
|
|
252
|
+
};
|
|
253
|
+
sibling: boolean;
|
|
254
|
+
}
|
|
255
|
+
`;
|
|
256
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "Complex");
|
|
257
|
+
|
|
258
|
+
expect(result).not.toBeNull();
|
|
259
|
+
expect(result?.definition).toContain("level3: string");
|
|
260
|
+
expect(result?.definition).toContain("sibling: boolean");
|
|
261
|
+
|
|
262
|
+
// Must have 3 closing braces: level2's object, level1's object, and the interface itself
|
|
263
|
+
const braceCount = (result?.definition.match(/}/g) || []).length;
|
|
264
|
+
expect(braceCount).toBe(3);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Regression: issue #77 - object type alias without a trailing semicolon
|
|
268
|
+
it("should find object type alias without trailing semicolon", () => {
|
|
269
|
+
const source = `
|
|
270
|
+
type Params = {
|
|
271
|
+
id: string
|
|
272
|
+
jti: string
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const handler = async () => {}
|
|
276
|
+
`;
|
|
277
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "Params");
|
|
278
|
+
|
|
279
|
+
expect(result).not.toBeNull();
|
|
280
|
+
expect(result?.kind).toBe("type");
|
|
281
|
+
expect(result?.name).toBe("Params");
|
|
282
|
+
expect(result?.definition).toContain("id: string");
|
|
283
|
+
expect(result?.definition).toContain("jti: string");
|
|
284
|
+
// Must stop at the object's closing brace, not run into the handler body
|
|
285
|
+
expect(result?.definition).not.toContain("const handler");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should find object type alias without trailing semicolon at EOF", () => {
|
|
289
|
+
const source = `type Params = {\n id: string\n jti: string\n}`;
|
|
290
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "Params");
|
|
291
|
+
|
|
292
|
+
expect(result).not.toBeNull();
|
|
293
|
+
expect(result?.kind).toBe("type");
|
|
294
|
+
expect(result?.definition).toContain("id: string");
|
|
295
|
+
expect(result?.definition).toContain("jti: string");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should capture full union type alias spanning braces without trailing semicolon", () => {
|
|
299
|
+
const source = `
|
|
300
|
+
type Shape = { a: string } | { b: number }
|
|
301
|
+
|
|
302
|
+
const x = 1
|
|
303
|
+
`;
|
|
304
|
+
const result = TypeScriptSourceParser.findTypeDefinition(source, "Shape");
|
|
305
|
+
|
|
306
|
+
expect(result).not.toBeNull();
|
|
307
|
+
expect(result?.definition).toContain("a: string");
|
|
308
|
+
expect(result?.definition).toContain("b: number");
|
|
309
|
+
expect(result?.definition).not.toContain("const x");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("renameTypeDefinition", () => {
|
|
314
|
+
it("should rename interface", () => {
|
|
315
|
+
const def = "interface User { name: string; }";
|
|
316
|
+
const result = TypeScriptSourceParser.renameTypeDefinition(def, "User", "Person");
|
|
317
|
+
|
|
318
|
+
expect(result).toBe("export interface Person { name: string; }");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should rename type alias", () => {
|
|
322
|
+
const def = "type UserId = string;";
|
|
323
|
+
const result = TypeScriptSourceParser.renameTypeDefinition(def, "UserId", "PersonId");
|
|
324
|
+
|
|
325
|
+
expect(result).toBe("export type PersonId = string;");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("should not export if exportIt is false", () => {
|
|
329
|
+
const def = "interface User { name: string; }";
|
|
330
|
+
const result = TypeScriptSourceParser.renameTypeDefinition(def, "User", "Person", false);
|
|
331
|
+
|
|
332
|
+
expect(result).toBe("interface Person { name: string; }");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("extractReferencedTypes", () => {
|
|
337
|
+
it("should extract type references from interface", () => {
|
|
338
|
+
const def = `
|
|
339
|
+
interface Document {
|
|
340
|
+
author: User;
|
|
341
|
+
tags: Tag[];
|
|
342
|
+
metadata: Metadata;
|
|
343
|
+
}
|
|
344
|
+
`;
|
|
345
|
+
const result = TypeScriptSourceParser.extractReferencedTypes(def);
|
|
346
|
+
|
|
347
|
+
expect(result).toContain("User");
|
|
348
|
+
expect(result).toContain("Tag");
|
|
349
|
+
expect(result).toContain("Metadata");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("should not extract built-in types", () => {
|
|
353
|
+
const def = `
|
|
354
|
+
interface Doc {
|
|
355
|
+
title: string;
|
|
356
|
+
count: number;
|
|
357
|
+
date: Date;
|
|
358
|
+
}
|
|
359
|
+
`;
|
|
360
|
+
const result = TypeScriptSourceParser.extractReferencedTypes(def);
|
|
361
|
+
|
|
362
|
+
expect(result).not.toContain("Date");
|
|
363
|
+
expect(result.length).toBe(0);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("should not extract types from JSDoc comments", () => {
|
|
367
|
+
const def = `
|
|
368
|
+
/**
|
|
369
|
+
* Title for the document
|
|
370
|
+
* Optional user info
|
|
371
|
+
*/
|
|
372
|
+
interface Doc {
|
|
373
|
+
title: string;
|
|
374
|
+
}
|
|
375
|
+
`;
|
|
376
|
+
const result = TypeScriptSourceParser.extractReferencedTypes(def);
|
|
377
|
+
|
|
378
|
+
expect(result).not.toContain("Title");
|
|
379
|
+
expect(result).not.toContain("Optional");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("should handle union types", () => {
|
|
383
|
+
const def = `
|
|
384
|
+
interface Result {
|
|
385
|
+
data: Success | Error;
|
|
386
|
+
}
|
|
387
|
+
`;
|
|
388
|
+
const result = TypeScriptSourceParser.extractReferencedTypes(def);
|
|
389
|
+
|
|
390
|
+
expect(result).toContain("Success");
|
|
391
|
+
expect(result).toContain("Error");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("should handle intersection types", () => {
|
|
395
|
+
const def = `
|
|
396
|
+
interface Combined {
|
|
397
|
+
data: Base & Extra;
|
|
398
|
+
}
|
|
399
|
+
`;
|
|
400
|
+
const result = TypeScriptSourceParser.extractReferencedTypes(def);
|
|
401
|
+
|
|
402
|
+
expect(result).toContain("Base");
|
|
403
|
+
expect(result).toContain("Extra");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("should handle Array generics", () => {
|
|
407
|
+
const def = `
|
|
408
|
+
interface List {
|
|
409
|
+
items: Array<Item>;
|
|
410
|
+
}
|
|
411
|
+
`;
|
|
412
|
+
const result = TypeScriptSourceParser.extractReferencedTypes(def);
|
|
413
|
+
|
|
414
|
+
expect(result).toContain("Item");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("should not include duplicates", () => {
|
|
418
|
+
const def = `
|
|
419
|
+
interface Doc {
|
|
420
|
+
author: User;
|
|
421
|
+
editor: User;
|
|
422
|
+
}
|
|
423
|
+
`;
|
|
424
|
+
const result = TypeScriptSourceParser.extractReferencedTypes(def);
|
|
425
|
+
|
|
426
|
+
expect(result.filter((t: string) => t === "User").length).toBe(1);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe("isInlineObjectType", () => {
|
|
431
|
+
it("should return true for inline objects", () => {
|
|
432
|
+
expect(TypeScriptSourceParser.isInlineObjectType("{}")).toBe(true);
|
|
433
|
+
expect(TypeScriptSourceParser.isInlineObjectType("{ foo: string }")).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("should return false for named types", () => {
|
|
437
|
+
expect(TypeScriptSourceParser.isInlineObjectType("User")).toBe(false);
|
|
438
|
+
expect(TypeScriptSourceParser.isInlineObjectType("CreateDocInput")).toBe(false);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
describe("isBuiltInType", () => {
|
|
443
|
+
it("should return true for built-in types", () => {
|
|
444
|
+
expect(TypeScriptSourceParser.isBuiltInType("String")).toBe(true);
|
|
445
|
+
expect(TypeScriptSourceParser.isBuiltInType("Array")).toBe(true);
|
|
446
|
+
expect(TypeScriptSourceParser.isBuiltInType("Promise")).toBe(true);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("should return false for custom types", () => {
|
|
450
|
+
expect(TypeScriptSourceParser.isBuiltInType("User")).toBe(false);
|
|
451
|
+
expect(TypeScriptSourceParser.isBuiltInType("CustomType")).toBe(false);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
describe("parseJsDocDescription", () => {
|
|
456
|
+
it("should parse single-line JSDoc comment", () => {
|
|
457
|
+
const result = TypeScriptSourceParser.parseJsDocDescription("/** Max cars to retrieve */");
|
|
458
|
+
expect(result).toBe("Max cars to retrieve");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("should parse multi-line JSDoc comment", () => {
|
|
462
|
+
const jsDoc = `/**
|
|
463
|
+
* Max cars to retrieve
|
|
464
|
+
* from the database
|
|
465
|
+
*/`;
|
|
466
|
+
const result = TypeScriptSourceParser.parseJsDocDescription(jsDoc);
|
|
467
|
+
expect(result).toBe("Max cars to retrieve from the database");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("should return empty string for empty input", () => {
|
|
471
|
+
expect(TypeScriptSourceParser.parseJsDocDescription("")).toBe("");
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("should handle JSDoc with extra whitespace", () => {
|
|
475
|
+
const jsDoc = `/**
|
|
476
|
+
* Max items
|
|
477
|
+
* to process
|
|
478
|
+
*/`;
|
|
479
|
+
const result = TypeScriptSourceParser.parseJsDocDescription(jsDoc);
|
|
480
|
+
expect(result).toBe("Max items to process");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("should handle JSDoc without leading asterisks", () => {
|
|
484
|
+
const result = TypeScriptSourceParser.parseJsDocDescription("/** Simple description */");
|
|
485
|
+
expect(result).toBe("Simple description");
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe("extractPropertiesFromObjectType", () => {
|
|
490
|
+
it("should extract properties with JSDoc comments", () => {
|
|
491
|
+
const objectBody = `
|
|
492
|
+
/** Max cars to retrieve */
|
|
493
|
+
limit: string;
|
|
494
|
+
/** Current page number */
|
|
495
|
+
page: number;
|
|
496
|
+
`;
|
|
497
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType(objectBody);
|
|
498
|
+
|
|
499
|
+
expect(result).toEqual([
|
|
500
|
+
{ name: "limit", description: "Max cars to retrieve" },
|
|
501
|
+
{ name: "page", description: "Current page number" }
|
|
502
|
+
]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("should extract properties without JSDoc comments", () => {
|
|
506
|
+
const objectBody = `
|
|
507
|
+
limit: string;
|
|
508
|
+
page: number;
|
|
509
|
+
`;
|
|
510
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType(objectBody);
|
|
511
|
+
|
|
512
|
+
expect(result).toEqual([
|
|
513
|
+
{ name: "limit", description: "" },
|
|
514
|
+
{ name: "page", description: "" }
|
|
515
|
+
]);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("should handle optional properties", () => {
|
|
519
|
+
const objectBody = `
|
|
520
|
+
/** Max items */
|
|
521
|
+
limit: string;
|
|
522
|
+
page?: number;
|
|
523
|
+
`;
|
|
524
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType(objectBody);
|
|
525
|
+
|
|
526
|
+
expect(result).toEqual([
|
|
527
|
+
{ name: "limit", description: "Max items" },
|
|
528
|
+
{ name: "page", description: "" }
|
|
529
|
+
]);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("should handle mixed JSDoc presence", () => {
|
|
533
|
+
const objectBody = `
|
|
534
|
+
/** Max items */
|
|
535
|
+
limit: string;
|
|
536
|
+
page: number;
|
|
537
|
+
/** Sort order */
|
|
538
|
+
sort?: string;
|
|
539
|
+
`;
|
|
540
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType(objectBody);
|
|
541
|
+
|
|
542
|
+
expect(result).toEqual([
|
|
543
|
+
{ name: "limit", description: "Max items" },
|
|
544
|
+
{ name: "page", description: "" },
|
|
545
|
+
{ name: "sort", description: "Sort order" }
|
|
546
|
+
]);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("should handle empty object body", () => {
|
|
550
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType("");
|
|
551
|
+
expect(result).toEqual([]);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("should handle complex property types", () => {
|
|
555
|
+
const objectBody = `
|
|
556
|
+
/** Array of IDs */
|
|
557
|
+
ids: string[];
|
|
558
|
+
/** Nested object */
|
|
559
|
+
metadata: { key: string; value: string };
|
|
560
|
+
/** Union type */
|
|
561
|
+
status: "active" | "inactive";
|
|
562
|
+
`;
|
|
563
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType(objectBody);
|
|
564
|
+
|
|
565
|
+
expect(result).toEqual([
|
|
566
|
+
{ name: "ids", description: "Array of IDs" },
|
|
567
|
+
{ name: "metadata", description: "Nested object" },
|
|
568
|
+
{ name: "status", description: "Union type" }
|
|
569
|
+
]);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("should handle multi-line JSDoc comments", () => {
|
|
573
|
+
const objectBody = `
|
|
574
|
+
/**
|
|
575
|
+
* Maximum number of items
|
|
576
|
+
* to retrieve from the API
|
|
577
|
+
*/
|
|
578
|
+
limit: string;
|
|
579
|
+
page: number;
|
|
580
|
+
`;
|
|
581
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType(objectBody);
|
|
582
|
+
|
|
583
|
+
expect(result).toEqual([
|
|
584
|
+
{ name: "limit", description: "Maximum number of items to retrieve from the API" },
|
|
585
|
+
{ name: "page", description: "" }
|
|
586
|
+
]);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Regression: issue #77 - newline-separated properties without `;`/`,` delimiters
|
|
590
|
+
it("should extract all properties separated only by newlines", () => {
|
|
591
|
+
const objectBody = `
|
|
592
|
+
callId: string
|
|
593
|
+
userId: string
|
|
594
|
+
`;
|
|
595
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType(objectBody);
|
|
596
|
+
|
|
597
|
+
expect(result).toEqual([
|
|
598
|
+
{ name: "callId", description: "" },
|
|
599
|
+
{ name: "userId", description: "" }
|
|
600
|
+
]);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("should extract newline-separated properties with JSDoc and no delimiters", () => {
|
|
604
|
+
const objectBody = `
|
|
605
|
+
/** The call id */
|
|
606
|
+
callId: string
|
|
607
|
+
/** The user id */
|
|
608
|
+
userId: string
|
|
609
|
+
`;
|
|
610
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType(objectBody);
|
|
611
|
+
|
|
612
|
+
expect(result).toEqual([
|
|
613
|
+
{ name: "callId", description: "The call id" },
|
|
614
|
+
{ name: "userId", description: "The user id" }
|
|
615
|
+
]);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("should not split multi-line union types separated by leading pipes", () => {
|
|
619
|
+
const objectBody = `
|
|
620
|
+
status:
|
|
621
|
+
| "active"
|
|
622
|
+
| "inactive"
|
|
623
|
+
limit: number
|
|
624
|
+
`;
|
|
625
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType(objectBody);
|
|
626
|
+
|
|
627
|
+
expect(result).toEqual([
|
|
628
|
+
{ name: "status", description: "" },
|
|
629
|
+
{ name: "limit", description: "" }
|
|
630
|
+
]);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("should handle newline-separated nested objects without delimiters", () => {
|
|
634
|
+
const objectBody = `
|
|
635
|
+
meta: {
|
|
636
|
+
title: string
|
|
637
|
+
}
|
|
638
|
+
limit: number
|
|
639
|
+
`;
|
|
640
|
+
const result = TypeScriptSourceParser.extractPropertiesFromObjectType(objectBody);
|
|
641
|
+
|
|
642
|
+
expect(result).toEqual([
|
|
643
|
+
{ name: "meta", description: "" },
|
|
644
|
+
{ name: "limit", description: "" }
|
|
645
|
+
]);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
describe("copyTypeWithDependencies", () => {
|
|
650
|
+
it("should copy a simple interface with no dependencies", () => {
|
|
651
|
+
const source = `
|
|
652
|
+
interface User {
|
|
653
|
+
name: string;
|
|
654
|
+
age: number;
|
|
655
|
+
}
|
|
656
|
+
`;
|
|
657
|
+
const result = TypeScriptSourceParser.copyTypeWithDependencies(source, "User", "UserSchema", new Set());
|
|
658
|
+
|
|
659
|
+
expect(result.mainDefinition).toContain("export interface UserSchema");
|
|
660
|
+
expect(result.mainDefinition).toContain("name: string");
|
|
661
|
+
expect(result.dependencies.length).toBe(0);
|
|
662
|
+
expect(result.copiedTypes.has("User")).toBe(true);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("should copy interface with dependencies recursively", () => {
|
|
666
|
+
const source = `
|
|
667
|
+
interface User {
|
|
668
|
+
profile: Profile;
|
|
669
|
+
}
|
|
670
|
+
interface Profile {
|
|
671
|
+
name: string;
|
|
672
|
+
}
|
|
673
|
+
`;
|
|
674
|
+
const result = TypeScriptSourceParser.copyTypeWithDependencies(source, "User", "UserSchema", new Set());
|
|
675
|
+
|
|
676
|
+
expect(result.mainDefinition).toContain("export interface UserSchema");
|
|
677
|
+
expect(result.dependencies.length).toBe(1);
|
|
678
|
+
expect(result.dependencies[0]).toContain("export interface Profile");
|
|
679
|
+
expect(result.copiedTypes.has("User")).toBe(true);
|
|
680
|
+
expect(result.copiedTypes.has("Profile")).toBe(true);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("should handle inline object types", () => {
|
|
684
|
+
const source = "";
|
|
685
|
+
const result = TypeScriptSourceParser.copyTypeWithDependencies(source, "{ foo: string }", "FooSchema", new Set());
|
|
686
|
+
|
|
687
|
+
expect(result.mainDefinition).toContain("export interface FooSchema");
|
|
688
|
+
expect(result.mainDefinition).toContain("foo: string");
|
|
689
|
+
expect(result.dependencies.length).toBe(0);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("should not copy already-copied types", () => {
|
|
693
|
+
const source = `
|
|
694
|
+
interface User {
|
|
695
|
+
profile: Profile;
|
|
696
|
+
}
|
|
697
|
+
interface Profile {
|
|
698
|
+
name: string;
|
|
699
|
+
}
|
|
700
|
+
`;
|
|
701
|
+
const copiedTypes = new Set(["Profile"]);
|
|
702
|
+
const result = TypeScriptSourceParser.copyTypeWithDependencies(source, "User", "UserSchema", copiedTypes);
|
|
703
|
+
|
|
704
|
+
expect(result.mainDefinition).toContain("export interface UserSchema");
|
|
705
|
+
expect(result.dependencies.length).toBe(0); // Profile already copied
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("should skip built-in types", () => {
|
|
709
|
+
const source = `
|
|
710
|
+
interface User {
|
|
711
|
+
name: string;
|
|
712
|
+
createdAt: Date;
|
|
713
|
+
}
|
|
714
|
+
`;
|
|
715
|
+
const result = TypeScriptSourceParser.copyTypeWithDependencies(source, "User", "UserSchema", new Set());
|
|
716
|
+
|
|
717
|
+
expect(result.mainDefinition).toContain("export interface UserSchema");
|
|
718
|
+
expect(result.dependencies.length).toBe(0); // Date is built-in
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("should return null mainDefinition for non-existent type", () => {
|
|
722
|
+
const source = "interface Foo { bar: string; }";
|
|
723
|
+
const result = TypeScriptSourceParser.copyTypeWithDependencies(source, "NonExistent", "Schema", new Set());
|
|
724
|
+
|
|
725
|
+
expect(result.mainDefinition).toBeNull();
|
|
726
|
+
expect(result.dependencies.length).toBe(0);
|
|
727
|
+
expect(result.imports.length).toBe(0);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it("should extract imports for referenced types from other files", () => {
|
|
731
|
+
const source = `
|
|
732
|
+
import { TextSpan } from './TextSpan';
|
|
733
|
+
import { Element } from '../Canvas';
|
|
734
|
+
|
|
735
|
+
interface UpdateInput {
|
|
736
|
+
elementId: string;
|
|
737
|
+
content: string | TextSpan[];
|
|
738
|
+
element: Element;
|
|
739
|
+
}
|
|
740
|
+
`;
|
|
741
|
+
const result = TypeScriptSourceParser.copyTypeWithDependencies(source, "UpdateInput", "UpdateInputSchema", new Set());
|
|
742
|
+
|
|
743
|
+
expect(result.mainDefinition).toContain("export interface UpdateInputSchema");
|
|
744
|
+
expect(result.mainDefinition).toContain("TextSpan");
|
|
745
|
+
expect(result.mainDefinition).toContain("Element");
|
|
746
|
+
expect(result.imports.length).toBe(2);
|
|
747
|
+
expect(result.imports).toContain('import { TextSpan } from \'./TextSpan\'');
|
|
748
|
+
expect(result.imports).toContain('import { Element } from \'../Canvas\'');
|
|
749
|
+
expect(result.copiedTypes.has("UpdateInput")).toBe(true);
|
|
750
|
+
expect(result.copiedTypes.has("TextSpan")).toBe(true);
|
|
751
|
+
expect(result.copiedTypes.has("Element")).toBe(true);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it("should handle mixed local and imported types", () => {
|
|
755
|
+
const source = `
|
|
756
|
+
import { Address } from './Address';
|
|
757
|
+
|
|
758
|
+
interface Profile {
|
|
759
|
+
name: string;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
interface User {
|
|
763
|
+
profile: Profile;
|
|
764
|
+
address: Address;
|
|
765
|
+
}
|
|
766
|
+
`;
|
|
767
|
+
const result = TypeScriptSourceParser.copyTypeWithDependencies(source, "User", "UserSchema", new Set());
|
|
768
|
+
|
|
769
|
+
expect(result.mainDefinition).toContain("export interface UserSchema");
|
|
770
|
+
expect(result.dependencies.length).toBe(1); // Profile is local
|
|
771
|
+
expect(result.dependencies[0]).toContain("export interface Profile");
|
|
772
|
+
expect(result.imports.length).toBe(1); // Address is imported
|
|
773
|
+
expect(result.imports[0]).toContain('import { Address } from \'./Address\'');
|
|
774
|
+
expect(result.copiedTypes.has("User")).toBe(true);
|
|
775
|
+
expect(result.copiedTypes.has("Profile")).toBe(true);
|
|
776
|
+
expect(result.copiedTypes.has("Address")).toBe(true);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it("should not duplicate imports", () => {
|
|
780
|
+
const source = `
|
|
781
|
+
import { TextSpan } from './TextSpan';
|
|
782
|
+
|
|
783
|
+
interface UpdateInput {
|
|
784
|
+
title: TextSpan;
|
|
785
|
+
content: TextSpan[];
|
|
786
|
+
}
|
|
787
|
+
`;
|
|
788
|
+
const result = TypeScriptSourceParser.copyTypeWithDependencies(source, "UpdateInput", "UpdateInputSchema", new Set());
|
|
789
|
+
|
|
790
|
+
expect(result.imports.length).toBe(1); // Only one import for TextSpan
|
|
791
|
+
expect(result.imports[0]).toContain('import { TextSpan } from \'./TextSpan\'');
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
describe("extractImportForType", () => {
|
|
796
|
+
it("should extract import type statements", () => {
|
|
797
|
+
const source = `
|
|
798
|
+
import type { User } from '../schemas/User';
|
|
799
|
+
import type Profile from '../schemas/Profile';
|
|
800
|
+
`;
|
|
801
|
+
const userImport = TypeScriptSourceParser.extractImportForType(source, 'User');
|
|
802
|
+
const profileImport = TypeScriptSourceParser.extractImportForType(source, 'Profile');
|
|
803
|
+
|
|
804
|
+
expect(userImport).toBe("import type { User } from '../schemas/User'");
|
|
805
|
+
expect(profileImport).toBe("import type Profile from '../schemas/Profile'");
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
describe("extractImportForType (original tests)", () => {
|
|
809
|
+
it("should extract named import", () => {
|
|
810
|
+
const source = `
|
|
811
|
+
import { User } from '../schemas/User';
|
|
812
|
+
import Profile from '../schemas/Profile';
|
|
813
|
+
`;
|
|
814
|
+
const result = TypeScriptSourceParser.extractImportForType(source, "User");
|
|
815
|
+
|
|
816
|
+
expect(result).toBe("import { User } from '../schemas/User'");
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it("should extract default import", () => {
|
|
820
|
+
const source = `
|
|
821
|
+
import { User } from '../schemas/User';
|
|
822
|
+
import Profile from '../schemas/Profile';
|
|
823
|
+
`;
|
|
824
|
+
const result = TypeScriptSourceParser.extractImportForType(source, "Profile");
|
|
825
|
+
|
|
826
|
+
expect(result).toBe("import Profile from '../schemas/Profile'");
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it("should return null for non-existent import", () => {
|
|
830
|
+
const source = "import { User } from '../schemas/User';";
|
|
831
|
+
const result = TypeScriptSourceParser.extractImportForType(source, "NonExistent");
|
|
832
|
+
|
|
833
|
+
expect(result).toBeNull();
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it("should handle multiple named imports", () => {
|
|
837
|
+
const source = "import { User, Profile, Settings } from '../types';";
|
|
838
|
+
const result = TypeScriptSourceParser.extractImportForType(source, "Profile");
|
|
839
|
+
|
|
840
|
+
expect(result).toBe("import { User, Profile, Settings } from '../types'");
|
|
841
|
+
});
|
|
842
|
+
}); // end of nested describe
|
|
843
|
+
}); // end of extractImportForType
|
|
844
|
+
|
|
845
|
+
describe("adjustImportPathForSchemas", () => {
|
|
846
|
+
it("should adjust path for schemas directory", () => {
|
|
847
|
+
const importStmt = 'import { User } from "../schemas/User"';
|
|
848
|
+
const result = TypeScriptSourceParser.adjustImportPathForSchemas(importStmt);
|
|
849
|
+
|
|
850
|
+
expect(result).toBe('import { User } from "../../src/schemas/User"');
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it("should handle double-dot paths", () => {
|
|
854
|
+
const importStmt = 'import Profile from "../../schemas/Profile"';
|
|
855
|
+
const result = TypeScriptSourceParser.adjustImportPathForSchemas(importStmt);
|
|
856
|
+
|
|
857
|
+
expect(result).toBe('import Profile from "../../src/schemas/Profile"');
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it("should adjust relative imports from clients directory", () => {
|
|
861
|
+
const importStmt = 'import { Client } from "../clients/SupermetricsClient"';
|
|
862
|
+
const result = TypeScriptSourceParser.adjustImportPathForSchemas(importStmt);
|
|
863
|
+
|
|
864
|
+
expect(result).toBe('import { Client } from "../../src/clients/SupermetricsClient"');
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it("should adjust relative imports from any directory", () => {
|
|
868
|
+
const importStmt = 'import { Util } from "../utils/helper"';
|
|
869
|
+
const result = TypeScriptSourceParser.adjustImportPathForSchemas(importStmt);
|
|
870
|
+
|
|
871
|
+
expect(result).toBe('import { Util } from "../../src/utils/helper"');
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it("should adjust relative imports from nested directories", () => {
|
|
875
|
+
const importStmt = 'import { Type } from "../../types/User"';
|
|
876
|
+
const result = TypeScriptSourceParser.adjustImportPathForSchemas(importStmt);
|
|
877
|
+
|
|
878
|
+
expect(result).toBe('import { Type } from "../../src/types/User"');
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it("should not modify package imports", () => {
|
|
882
|
+
const importStmt = 'import { User } from "@company/shared"';
|
|
883
|
+
const result = TypeScriptSourceParser.adjustImportPathForSchemas(importStmt);
|
|
884
|
+
|
|
885
|
+
expect(result).toBe('import { User } from "@company/shared"');
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it("should handle single quotes", () => {
|
|
889
|
+
const importStmt = "import { User } from '../schemas/User'";
|
|
890
|
+
const result = TypeScriptSourceParser.adjustImportPathForSchemas(importStmt);
|
|
891
|
+
|
|
892
|
+
expect(result).toBe("import { User } from '../../src/schemas/User'");
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it("should preserve subdirectory structure", () => {
|
|
896
|
+
const importStmt = 'import { DeleteReq } from "../../schemas/User/DeleteReq"';
|
|
897
|
+
const result = TypeScriptSourceParser.adjustImportPathForSchemas(importStmt);
|
|
898
|
+
|
|
899
|
+
expect(result).toBe('import { DeleteReq } from "../../src/schemas/User/DeleteReq"');
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it("should preserve nested subdirectory structure", () => {
|
|
903
|
+
const importStmt = 'import { Type } from "../schemas/Management/User/Type"';
|
|
904
|
+
const result = TypeScriptSourceParser.adjustImportPathForSchemas(importStmt);
|
|
905
|
+
|
|
906
|
+
expect(result).toBe('import { Type } from "../../src/schemas/Management/User/Type"');
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
describe("consolidateImports", () => {
|
|
911
|
+
it("should consolidate duplicate imports from same module", () => {
|
|
912
|
+
const imports = [
|
|
913
|
+
'import { Ad } from "../../src/schemas/Ad"',
|
|
914
|
+
'import { AdStatus } from "../../src/schemas/Ad"',
|
|
915
|
+
'import { User } from "../../src/schemas/User"',
|
|
916
|
+
];
|
|
917
|
+
const result = TypeScriptSourceParser.consolidateImports(imports);
|
|
918
|
+
|
|
919
|
+
expect(result).toContain('import { Ad, AdStatus } from "../../src/schemas/Ad"');
|
|
920
|
+
expect(result).toContain('import { User } from "../../src/schemas/User"');
|
|
921
|
+
expect(result.length).toBe(2);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it("should handle multiple imports from same module", () => {
|
|
925
|
+
const imports = [
|
|
926
|
+
'import { Ad } from "../../src/schemas/Ad"',
|
|
927
|
+
'import { AdStatus, CreateAdRequest } from "../../src/schemas/Ad"',
|
|
928
|
+
'import { AdProgressResponse } from "../../src/schemas/Ad"',
|
|
929
|
+
];
|
|
930
|
+
const result = TypeScriptSourceParser.consolidateImports(imports);
|
|
931
|
+
|
|
932
|
+
expect(result).toContain('import { Ad, AdProgressResponse, AdStatus, CreateAdRequest } from "../../src/schemas/Ad"');
|
|
933
|
+
expect(result.length).toBe(1);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
it("should preserve default imports", () => {
|
|
937
|
+
const imports = [
|
|
938
|
+
'import React from "react"',
|
|
939
|
+
'import { useState } from "react"',
|
|
940
|
+
];
|
|
941
|
+
const result = TypeScriptSourceParser.consolidateImports(imports);
|
|
942
|
+
|
|
943
|
+
expect(result).toContain('import React, { useState } from "react"');
|
|
944
|
+
expect(result.length).toBe(1);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it("should handle single quotes", () => {
|
|
948
|
+
const imports = [
|
|
949
|
+
"import { Ad } from '../../src/schemas/Ad'",
|
|
950
|
+
"import { AdStatus } from '../../src/schemas/Ad'",
|
|
951
|
+
];
|
|
952
|
+
const result = TypeScriptSourceParser.consolidateImports(imports);
|
|
953
|
+
|
|
954
|
+
expect(result[0]).toContain('from "../../src/schemas/Ad"');
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it("should deduplicate named imports", () => {
|
|
958
|
+
const imports = [
|
|
959
|
+
'import { Ad } from "../../src/schemas/Ad"',
|
|
960
|
+
'import { Ad, AdStatus } from "../../src/schemas/Ad"',
|
|
961
|
+
];
|
|
962
|
+
const result = TypeScriptSourceParser.consolidateImports(imports);
|
|
963
|
+
|
|
964
|
+
// Ad should only appear once in the named imports (not counting the module path)
|
|
965
|
+
const importStatement = result.find((s) => s.includes("../../src/schemas/Ad"))!;
|
|
966
|
+
// Extract just the import list part: { Ad, AdStatus }
|
|
967
|
+
const importListMatch = importStatement.match(/\{([^}]+)\}/);
|
|
968
|
+
expect(importListMatch).toBeTruthy();
|
|
969
|
+
const importList = importListMatch![1];
|
|
970
|
+
const adMatches = (importList.match(/\bAd\b/g) || []).length;
|
|
971
|
+
expect(adMatches).toBe(1); // "Ad" appears once, "AdStatus" doesn't count
|
|
972
|
+
expect(importStatement).toContain("AdStatus");
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it("should handle imports from different modules", () => {
|
|
976
|
+
const imports = [
|
|
977
|
+
'import { User } from "../../src/schemas/User"',
|
|
978
|
+
'import { Client } from "../../src/clients/SupermetricsClient"',
|
|
979
|
+
'import { BudgetPeriod } from "../../src/schemas/BudgetPeriod"',
|
|
980
|
+
];
|
|
981
|
+
const result = TypeScriptSourceParser.consolidateImports(imports);
|
|
982
|
+
|
|
983
|
+
expect(result.length).toBe(3);
|
|
984
|
+
expect(result).toContain('import { User } from "../../src/schemas/User"');
|
|
985
|
+
expect(result).toContain('import { Client } from "../../src/clients/SupermetricsClient"');
|
|
986
|
+
expect(result).toContain('import { BudgetPeriod } from "../../src/schemas/BudgetPeriod"');
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it("should sort named imports alphabetically", () => {
|
|
990
|
+
const imports = [
|
|
991
|
+
'import { ZebraType } from "../../src/schemas/Types"',
|
|
992
|
+
'import { AppleType } from "../../src/schemas/Types"',
|
|
993
|
+
'import { BananaType } from "../../src/schemas/Types"',
|
|
994
|
+
];
|
|
995
|
+
const result = TypeScriptSourceParser.consolidateImports(imports);
|
|
996
|
+
|
|
997
|
+
expect(result[0]).toBe('import { AppleType, BananaType, ZebraType } from "../../src/schemas/Types"');
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it("should handle import type statements", () => {
|
|
1001
|
+
const imports = [
|
|
1002
|
+
'import type { User } from "../../src/schemas/User"',
|
|
1003
|
+
'import type { Profile } from "../../src/schemas/User"',
|
|
1004
|
+
];
|
|
1005
|
+
const result = TypeScriptSourceParser.consolidateImports(imports);
|
|
1006
|
+
|
|
1007
|
+
// Note: We consolidate by removing the 'type' keyword for runtime schema generation
|
|
1008
|
+
expect(result[0]).toBe('import { Profile, User } from "../../src/schemas/User"');
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
it("should consolidate mixed import and import type", () => {
|
|
1012
|
+
const imports = [
|
|
1013
|
+
'import { User } from "../../src/schemas/User"',
|
|
1014
|
+
'import type { UserRole } from "../../src/schemas/User"',
|
|
1015
|
+
];
|
|
1016
|
+
const result = TypeScriptSourceParser.consolidateImports(imports);
|
|
1017
|
+
|
|
1018
|
+
expect(result[0]).toBe('import { User, UserRole } from "../../src/schemas/User"');
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
describe("extractPropertyMetadata", () => {
|
|
1023
|
+
it("should extract properties from interface definition", () => {
|
|
1024
|
+
const source = `
|
|
1025
|
+
interface Query {
|
|
1026
|
+
/** Max cars to retrieve */
|
|
1027
|
+
limit: string;
|
|
1028
|
+
page?: number;
|
|
1029
|
+
}
|
|
1030
|
+
`;
|
|
1031
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Query");
|
|
1032
|
+
|
|
1033
|
+
expect(result).toEqual([
|
|
1034
|
+
{ name: "limit", description: "Max cars to retrieve" },
|
|
1035
|
+
{ name: "page", description: "" }
|
|
1036
|
+
]);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it("should extract properties from type alias", () => {
|
|
1040
|
+
const source = `
|
|
1041
|
+
type Query = {
|
|
1042
|
+
/** Max items */
|
|
1043
|
+
limit: string;
|
|
1044
|
+
/** Current page */
|
|
1045
|
+
page: number;
|
|
1046
|
+
};
|
|
1047
|
+
`;
|
|
1048
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Query");
|
|
1049
|
+
|
|
1050
|
+
expect(result).toEqual([
|
|
1051
|
+
{ name: "limit", description: "Max items" },
|
|
1052
|
+
{ name: "page", description: "Current page" }
|
|
1053
|
+
]);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it("should return null for non-existent type", () => {
|
|
1057
|
+
const source = `
|
|
1058
|
+
interface User {
|
|
1059
|
+
name: string;
|
|
1060
|
+
}
|
|
1061
|
+
`;
|
|
1062
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Query");
|
|
1063
|
+
expect(result).toBeNull();
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
// Regression: issue #77 - semicolon-free type alias used as handler Params
|
|
1067
|
+
it("should extract all params from a semicolon-free type alias (issue #77 case A)", () => {
|
|
1068
|
+
const source = `
|
|
1069
|
+
type Params = {
|
|
1070
|
+
callId: string
|
|
1071
|
+
userId: string
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const handler: Handler<Ctx, void, void, Params> = async () => {}
|
|
1075
|
+
`;
|
|
1076
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Params");
|
|
1077
|
+
|
|
1078
|
+
expect(result).toEqual([
|
|
1079
|
+
{ name: "callId", description: "" },
|
|
1080
|
+
{ name: "userId", description: "" }
|
|
1081
|
+
]);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it("should extract all params from an inline semicolon-free type alias (issue #77 case B)", () => {
|
|
1085
|
+
const source = `
|
|
1086
|
+
type Params = { id: string; jti: string }
|
|
1087
|
+
|
|
1088
|
+
const handler: Handler<Ctx, void, {}, Params> = async () => {}
|
|
1089
|
+
`;
|
|
1090
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Params");
|
|
1091
|
+
|
|
1092
|
+
expect(result).toEqual([
|
|
1093
|
+
{ name: "id", description: "" },
|
|
1094
|
+
{ name: "jti", description: "" }
|
|
1095
|
+
]);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it("should handle empty interface", () => {
|
|
1099
|
+
const source = `
|
|
1100
|
+
interface Query {}
|
|
1101
|
+
`;
|
|
1102
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Query");
|
|
1103
|
+
expect(result).toEqual([]);
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it("should handle nested object properties", () => {
|
|
1107
|
+
const source = `
|
|
1108
|
+
interface Query {
|
|
1109
|
+
/** Filter options */
|
|
1110
|
+
filter: {
|
|
1111
|
+
/** Start date */
|
|
1112
|
+
startDate: string;
|
|
1113
|
+
/** End date */
|
|
1114
|
+
endDate: string;
|
|
1115
|
+
};
|
|
1116
|
+
/** Max items */
|
|
1117
|
+
limit: number;
|
|
1118
|
+
}
|
|
1119
|
+
`;
|
|
1120
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Query");
|
|
1121
|
+
|
|
1122
|
+
expect(result).toEqual([
|
|
1123
|
+
{ name: "filter", description: "Filter options" },
|
|
1124
|
+
{ name: "limit", description: "Max items" }
|
|
1125
|
+
]);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it("should handle properties without JSDoc", () => {
|
|
1129
|
+
const source = `
|
|
1130
|
+
interface Params {
|
|
1131
|
+
id: string;
|
|
1132
|
+
category: string;
|
|
1133
|
+
}
|
|
1134
|
+
`;
|
|
1135
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Params");
|
|
1136
|
+
|
|
1137
|
+
expect(result).toEqual([
|
|
1138
|
+
{ name: "id", description: "" },
|
|
1139
|
+
{ name: "category", description: "" }
|
|
1140
|
+
]);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it("should handle interface with generics", () => {
|
|
1144
|
+
const source = `
|
|
1145
|
+
interface Query<T> {
|
|
1146
|
+
/** Max items */
|
|
1147
|
+
limit: string;
|
|
1148
|
+
data: T;
|
|
1149
|
+
}
|
|
1150
|
+
`;
|
|
1151
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Query");
|
|
1152
|
+
|
|
1153
|
+
expect(result).toEqual([
|
|
1154
|
+
{ name: "limit", description: "Max items" },
|
|
1155
|
+
{ name: "data", description: "" }
|
|
1156
|
+
]);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it("should handle interface with extends", () => {
|
|
1160
|
+
const source = `
|
|
1161
|
+
interface Query extends BaseQuery {
|
|
1162
|
+
/** Max items */
|
|
1163
|
+
limit: string;
|
|
1164
|
+
}
|
|
1165
|
+
`;
|
|
1166
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Query");
|
|
1167
|
+
|
|
1168
|
+
expect(result).toEqual([
|
|
1169
|
+
{ name: "limit", description: "Max items" }
|
|
1170
|
+
]);
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
it("should extract from real-world handler example", () => {
|
|
1174
|
+
const source = `
|
|
1175
|
+
import { GetHandler, RouteProps } from "@flink-app/flink";
|
|
1176
|
+
|
|
1177
|
+
type Query = {
|
|
1178
|
+
/**
|
|
1179
|
+
* Max cars to retrieve
|
|
1180
|
+
*/
|
|
1181
|
+
limit: string;
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
const GetCars: GetHandler<any, CarListRes, any, Query> = async ({ ctx, req }) => {
|
|
1185
|
+
return { data: { cars: [] } };
|
|
1186
|
+
};
|
|
1187
|
+
`;
|
|
1188
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Query");
|
|
1189
|
+
|
|
1190
|
+
expect(result).toEqual([
|
|
1191
|
+
{ name: "limit", description: "Max cars to retrieve" }
|
|
1192
|
+
]);
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
it("should handle properties with complex types", () => {
|
|
1196
|
+
const source = `
|
|
1197
|
+
interface Query {
|
|
1198
|
+
/** Array of IDs */
|
|
1199
|
+
ids: string[];
|
|
1200
|
+
/** Date range */
|
|
1201
|
+
range: { start: Date; end: Date };
|
|
1202
|
+
/** Status filter */
|
|
1203
|
+
status: "active" | "inactive" | "pending";
|
|
1204
|
+
}
|
|
1205
|
+
`;
|
|
1206
|
+
const result = TypeScriptSourceParser.extractPropertyMetadata(source, "Query");
|
|
1207
|
+
|
|
1208
|
+
expect(result).toEqual([
|
|
1209
|
+
{ name: "ids", description: "Array of IDs" },
|
|
1210
|
+
{ name: "range", description: "Date range" },
|
|
1211
|
+
{ name: "status", description: "Status filter" }
|
|
1212
|
+
]);
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
});
|