@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
package/src/FlinkApp.ts
CHANGED
|
@@ -9,18 +9,32 @@ import morgan from "morgan";
|
|
|
9
9
|
import ms from "ms";
|
|
10
10
|
import { AsyncTask, CronJob, SimpleIntervalJob, ToadScheduler } from "toad-scheduler";
|
|
11
11
|
import { v4 } from "uuid";
|
|
12
|
+
import { FlinkAgentFile } from "./ai/FlinkAgent";
|
|
13
|
+
import { FlinkToolFile } from "./ai/FlinkTool";
|
|
14
|
+
import { AgentObserver } from "./ai/FlinkAgent";
|
|
15
|
+
import { LLMAdapter } from "./ai/LLMAdapter";
|
|
16
|
+
import { ToolExecutor } from "./ai/ToolExecutor";
|
|
12
17
|
import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
|
|
13
18
|
import { FlinkContext } from "./FlinkContext";
|
|
14
19
|
import { FlinkError, internalServerError, notFound, unauthorized } from "./FlinkErrors";
|
|
15
|
-
import { FlinkRequest, Handler, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps } from "./FlinkHttpHandler";
|
|
16
|
-
import { FlinkJobFile } from "./FlinkJob";
|
|
20
|
+
import { FlinkRequest, Handler, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps, ValidationMode } from "./FlinkHttpHandler";
|
|
21
|
+
import { FlinkJobFile, FlinkJobProps } from "./FlinkJob";
|
|
22
|
+
import { LeaderElection, LeaderElectionOptions } from "./LeaderElection";
|
|
17
23
|
import { log } from "./FlinkLog";
|
|
24
|
+
import { FlinkLogFactory } from "./FlinkLogFactory";
|
|
18
25
|
import { FlinkPlugin } from "./FlinkPlugin";
|
|
19
26
|
import { FlinkRepo } from "./FlinkRepo";
|
|
27
|
+
import { FlinkService } from "./FlinkService";
|
|
20
28
|
import { FlinkResponse } from "./FlinkResponse";
|
|
29
|
+
import { requestContext } from "./FlinkRequestContext";
|
|
30
|
+
import { StreamWriterFactory } from "./handlers/StreamWriterFactory";
|
|
21
31
|
import generateMockData from "./mock-data-generator";
|
|
22
32
|
import { formatValidationErrors, getPathParams, isError } from "./utils";
|
|
23
33
|
|
|
34
|
+
const initLog = FlinkLogFactory.createLogger("flink.init");
|
|
35
|
+
const perfLog = FlinkLogFactory.createLogger("flink.perf");
|
|
36
|
+
const schedulerLog = FlinkLogFactory.createLogger("flink.scheduler");
|
|
37
|
+
|
|
24
38
|
const ajv = new Ajv();
|
|
25
39
|
addFormats(ajv);
|
|
26
40
|
|
|
@@ -45,6 +59,7 @@ export const expressFn = express;
|
|
|
45
59
|
export const autoRegisteredHandlers: {
|
|
46
60
|
handler: HandlerFile;
|
|
47
61
|
assumedHttpMethod: HttpMethod;
|
|
62
|
+
__file?: string;
|
|
48
63
|
}[] = [];
|
|
49
64
|
|
|
50
65
|
/**
|
|
@@ -63,7 +78,28 @@ export const autoRegisteredRepos: {
|
|
|
63
78
|
*/
|
|
64
79
|
export const autoRegisteredJobs: FlinkJobFile[] = [];
|
|
65
80
|
|
|
66
|
-
|
|
81
|
+
/**
|
|
82
|
+
* This will be populated at compile time when the apps tools
|
|
83
|
+
* are picked up by TypeScript compiler
|
|
84
|
+
*/
|
|
85
|
+
export const autoRegisteredTools: FlinkToolFile[] = [];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* This will be populated at compile time when the apps agents
|
|
89
|
+
* are picked up by TypeScript compiler
|
|
90
|
+
*/
|
|
91
|
+
export const autoRegisteredAgents: FlinkAgentFile<any, any>[] = [];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* This will be populated at compile time when the apps services
|
|
95
|
+
* are picked up by TypeScript compiler
|
|
96
|
+
*/
|
|
97
|
+
export const autoRegisteredServices: {
|
|
98
|
+
serviceInstanceName: string;
|
|
99
|
+
Service: any;
|
|
100
|
+
}[] = [];
|
|
101
|
+
|
|
102
|
+
export interface FlinkOptions<C extends FlinkContext = FlinkContext> {
|
|
67
103
|
/**
|
|
68
104
|
* Name of application, will only show in logs and in HTTP header.
|
|
69
105
|
*/
|
|
@@ -156,28 +192,62 @@ export interface FlinkOptions {
|
|
|
156
192
|
*/
|
|
157
193
|
enabled?: boolean;
|
|
158
194
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Enable leader election for horizontally scaled deployments.
|
|
197
|
+
*
|
|
198
|
+
* When enabled, only one instance (the leader) will run scheduled jobs.
|
|
199
|
+
* If the leader goes down, another instance automatically takes over.
|
|
200
|
+
*
|
|
201
|
+
* Requires a database connection (`db` option) since leader election
|
|
202
|
+
* state is persisted in MongoDB. If no database is configured, a warning
|
|
203
|
+
* will be logged and jobs will run on all instances (no leader election).
|
|
204
|
+
*
|
|
205
|
+
* Set to `true` for default settings, or pass an options object to customize.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* // Use defaults (15s lease, 5s heartbeat)
|
|
210
|
+
* scheduling: { leaderElection: true }
|
|
211
|
+
*
|
|
212
|
+
* // Custom settings
|
|
213
|
+
* scheduling: {
|
|
214
|
+
* leaderElection: {
|
|
215
|
+
* leaseDurationMs: 30000,
|
|
216
|
+
* heartbeatIntervalMs: 10000,
|
|
217
|
+
* }
|
|
218
|
+
* }
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
leaderElection?: boolean | LeaderElectionOptions;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* AI configuration for agents and tools
|
|
226
|
+
* Register LLM adapters with custom IDs (e.g., "anthropic", "openai", "anthropic-eu", etc.)
|
|
227
|
+
* This allows multiple adapters of the same type with different configurations
|
|
228
|
+
*/
|
|
229
|
+
ai?: {
|
|
230
|
+
llms?: { [id: string]: LLMAdapter };
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Global agent observer for app-level tracing, APM, cost accounting, dev tools, etc.
|
|
234
|
+
*
|
|
235
|
+
* Fires for every agent execution in the app. Observer callbacks are invoked
|
|
236
|
+
* fire-and-forget — they may return a Promise but the framework does not await
|
|
237
|
+
* them, and any thrown/rejected errors are caught and logged without affecting
|
|
238
|
+
* agent execution.
|
|
239
|
+
*
|
|
240
|
+
* Events: `onRun` (pre-loop), `onLlmCall` (per step, pre-adapter call),
|
|
241
|
+
* `onStep` (per step end), `onFinish` (post-loop, including error path).
|
|
242
|
+
*
|
|
243
|
+
* For agent-local business logic (conversation persistence, guardrails) use the
|
|
244
|
+
* per-agent `beforeRun` / `onStep` / `afterRun` hooks on `FlinkAgent` instead.
|
|
245
|
+
*/
|
|
246
|
+
observer?: AgentObserver;
|
|
176
247
|
};
|
|
177
248
|
|
|
178
249
|
/**
|
|
179
250
|
* If true, the HTTP server will be disabled.
|
|
180
|
-
* Only useful when starting a Flink app for testing purposes.
|
|
181
251
|
*/
|
|
182
252
|
disableHttpServer?: boolean;
|
|
183
253
|
|
|
@@ -199,17 +269,26 @@ export interface FlinkOptions {
|
|
|
199
269
|
};
|
|
200
270
|
|
|
201
271
|
/**
|
|
202
|
-
* Optional callback invoked when an error occurs
|
|
272
|
+
* Optional callback invoked when an error occurs while serving a request.
|
|
203
273
|
* The error response and request context are passed for custom
|
|
204
274
|
* error logging or monitoring. This is a side-effect only and
|
|
205
275
|
* will not modify the response flow.
|
|
206
276
|
*
|
|
277
|
+
* Invoked for:
|
|
278
|
+
* - Handler-thrown errors (FlinkErrors and unhandled exceptions)
|
|
279
|
+
* - Request validation failures (400 Bad request)
|
|
280
|
+
* - Response validation failures (500 Bad response)
|
|
281
|
+
*
|
|
282
|
+
* Not invoked for errors in streaming (SSE/NDJSON) handlers, which are
|
|
283
|
+
* delivered to the client via `stream.error()` instead.
|
|
284
|
+
*
|
|
207
285
|
* Supports both synchronous and asynchronous callbacks. Any errors
|
|
208
286
|
* thrown or rejected by the callback will be caught and logged
|
|
209
|
-
* without affecting the error response to the client.
|
|
287
|
+
* without affecting the error response to the client. Async callbacks
|
|
288
|
+
* are fire-and-forget — they are not awaited before the response is sent.
|
|
210
289
|
*
|
|
211
290
|
* @param error - The error response with status and error details
|
|
212
|
-
* @param context - Request context including method, path, and
|
|
291
|
+
* @param context - Request context including method, path, request ID and the app context
|
|
213
292
|
*
|
|
214
293
|
* @example
|
|
215
294
|
* ```ts
|
|
@@ -225,12 +304,14 @@ export interface FlinkOptions {
|
|
|
225
304
|
* }
|
|
226
305
|
* }
|
|
227
306
|
*
|
|
228
|
-
* // Asynchronous callback
|
|
307
|
+
* // Asynchronous callback using the app context
|
|
229
308
|
* onError: async (error, context) => {
|
|
230
309
|
* if (error.status >= 500) {
|
|
231
|
-
* await
|
|
232
|
-
* error,
|
|
233
|
-
*
|
|
310
|
+
* await context.ctx.repos.errorLogRepo.create({
|
|
311
|
+
* status: error.status,
|
|
312
|
+
* detail: error.error?.detail,
|
|
313
|
+
* route: `${context.method} ${context.path}`,
|
|
314
|
+
* reqId: context.reqId,
|
|
234
315
|
* });
|
|
235
316
|
* }
|
|
236
317
|
* }
|
|
@@ -243,6 +324,7 @@ export interface FlinkOptions {
|
|
|
243
324
|
method: HttpMethod;
|
|
244
325
|
path: string;
|
|
245
326
|
reqId: string;
|
|
327
|
+
ctx: C;
|
|
246
328
|
}
|
|
247
329
|
) => void | Promise<void>;
|
|
248
330
|
}
|
|
@@ -278,6 +360,8 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
278
360
|
|
|
279
361
|
private _ctx?: C;
|
|
280
362
|
private dbOpts?: FlinkOptions["db"];
|
|
363
|
+
private schemaManifest?: any;
|
|
364
|
+
private schemaAjv?: Ajv;
|
|
281
365
|
private debug = false;
|
|
282
366
|
private onDbConnection?: FlinkOptions["onDbConnection"];
|
|
283
367
|
|
|
@@ -290,9 +374,15 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
290
374
|
private schedulingOptions?: FlinkOptions["scheduling"];
|
|
291
375
|
private disableHttpServer = false;
|
|
292
376
|
private expressServer: any; // for simplicity, we don't want to import types from express/node here
|
|
293
|
-
private onError?: FlinkOptions["onError"];
|
|
377
|
+
private onError?: FlinkOptions<C>["onError"];
|
|
294
378
|
|
|
295
379
|
private repos: { [x: string]: FlinkRepo<C, any> } = {};
|
|
380
|
+
private services: { [x: string]: FlinkService<C> } = {};
|
|
381
|
+
|
|
382
|
+
private llmAdapters: Map<string, LLMAdapter> = new Map();
|
|
383
|
+
private agentObserver?: AgentObserver;
|
|
384
|
+
private tools: { [x: string]: ToolExecutor<C> } = {};
|
|
385
|
+
private agents: { [x: string]: any } = {}; // FlinkAgent<C> instances
|
|
296
386
|
|
|
297
387
|
/**
|
|
298
388
|
* Internal cache used to track registered handlers and potentially any overlapping routes
|
|
@@ -300,10 +390,17 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
300
390
|
private handlerRouteCache = new Map<string, string>();
|
|
301
391
|
|
|
302
392
|
public scheduler?: ToadScheduler;
|
|
393
|
+
private allInstanceScheduler?: ToadScheduler;
|
|
394
|
+
private leaderElection?: LeaderElection;
|
|
303
395
|
|
|
304
396
|
private accessLog: { enabled: boolean; format: string };
|
|
305
397
|
|
|
306
|
-
constructor(opts: FlinkOptions) {
|
|
398
|
+
constructor(opts: FlinkOptions<C>) {
|
|
399
|
+
// Load config file and initialize logging
|
|
400
|
+
const { loadFlinkConfig } = require("./utils/loadFlinkConfig");
|
|
401
|
+
const flinkConfig = loadFlinkConfig();
|
|
402
|
+
FlinkLogFactory.configure(flinkConfig?.logging);
|
|
403
|
+
|
|
307
404
|
this.name = opts.name;
|
|
308
405
|
this.port = opts.port || 3333;
|
|
309
406
|
this.dbOpts = opts.db;
|
|
@@ -314,14 +411,23 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
314
411
|
this.rawContentTypes = Array.isArray(opts.rawContentTypes)
|
|
315
412
|
? opts.rawContentTypes
|
|
316
413
|
: typeof opts.rawContentTypes === "string"
|
|
317
|
-
|
|
318
|
-
|
|
414
|
+
? [opts.rawContentTypes]
|
|
415
|
+
: undefined;
|
|
319
416
|
this.auth = opts.auth;
|
|
320
417
|
this.jsonOptions = opts.jsonOptions || { limit: "1mb" };
|
|
321
418
|
this.schedulingOptions = opts.scheduling;
|
|
322
419
|
this.disableHttpServer = !!opts.disableHttpServer;
|
|
323
420
|
this.accessLog = { enabled: true, format: "dev", ...opts.accessLog };
|
|
324
421
|
this.onError = opts.onError;
|
|
422
|
+
|
|
423
|
+
// Register LLM adapters if configured
|
|
424
|
+
if (opts.ai?.llms) {
|
|
425
|
+
// Convert plain object to Map for internal use
|
|
426
|
+
this.llmAdapters = new Map(Object.entries(opts.ai.llms));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Register global agent observer if configured
|
|
430
|
+
this.agentObserver = opts.ai?.observer;
|
|
325
431
|
}
|
|
326
432
|
|
|
327
433
|
get ctx() {
|
|
@@ -333,26 +439,35 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
333
439
|
|
|
334
440
|
async start() {
|
|
335
441
|
const startTime = Date.now();
|
|
336
|
-
let offsetTime = 0;
|
|
337
442
|
|
|
443
|
+
const dbStartTime = Date.now();
|
|
338
444
|
await this.initDb();
|
|
445
|
+
perfLog.debug(`Init db took ${Date.now() - dbStartTime}ms`);
|
|
339
446
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
log.bgColorLog("cyan", `Init db took ${offsetTime - startTime} ms`);
|
|
343
|
-
}
|
|
344
|
-
|
|
447
|
+
// Build initial context (without agents - they'll be added later)
|
|
448
|
+
const contextStartTime = Date.now();
|
|
345
449
|
await this.buildContext();
|
|
450
|
+
perfLog.debug(`Build context took ${Date.now() - contextStartTime}ms`);
|
|
346
451
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
452
|
+
// Register tools (needs context for ToolExecutor)
|
|
453
|
+
const toolsStartTime = Date.now();
|
|
454
|
+
await this.registerAutoRegisterableTools();
|
|
455
|
+
perfLog.debug(`Register tools took ${Date.now() - toolsStartTime}ms`);
|
|
351
456
|
|
|
352
|
-
|
|
457
|
+
// Register agents (creates agent instances)
|
|
458
|
+
const agentsStartTime = Date.now();
|
|
459
|
+
await this.registerAutoRegisterableAgents();
|
|
460
|
+
perfLog.debug(`Register agents took ${Date.now() - agentsStartTime}ms`);
|
|
461
|
+
|
|
462
|
+
// Initialize agents now that context and tools are ready
|
|
463
|
+
const agentInitStartTime = Date.now();
|
|
464
|
+
await this.initializeAgents();
|
|
465
|
+
perfLog.debug(`Initialize agents took ${Date.now() - agentInitStartTime}ms`);
|
|
466
|
+
|
|
467
|
+
if (this.isSchedulingEnabled && !this.leaderElectionConfig) {
|
|
353
468
|
this.scheduler = new ToadScheduler();
|
|
354
|
-
} else {
|
|
355
|
-
|
|
469
|
+
} else if (!this.isSchedulingEnabled) {
|
|
470
|
+
schedulerLog.info("Scheduling is disabled");
|
|
356
471
|
}
|
|
357
472
|
|
|
358
473
|
if (!this.disableHttpServer) {
|
|
@@ -379,6 +494,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
379
494
|
|
|
380
495
|
// TODO: Add better more fine grained control when plugins are initialized, i.e. in what order
|
|
381
496
|
|
|
497
|
+
const pluginsStartTime = Date.now();
|
|
382
498
|
for (const plugin of this.plugins) {
|
|
383
499
|
let db;
|
|
384
500
|
|
|
@@ -390,22 +506,23 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
390
506
|
await plugin.init(this, db);
|
|
391
507
|
}
|
|
392
508
|
|
|
393
|
-
|
|
509
|
+
initLog.info(`Initialized plugin '${plugin.id}'`);
|
|
510
|
+
}
|
|
511
|
+
if (this.plugins.length > 0) {
|
|
512
|
+
perfLog.debug(`Initialize plugins took ${Date.now() - pluginsStartTime}ms (${this.plugins.length} plugins)`);
|
|
394
513
|
}
|
|
395
514
|
|
|
515
|
+
const handlersStartTime = Date.now();
|
|
396
516
|
await this.registerAutoRegisterableHandlers();
|
|
397
|
-
|
|
398
|
-
if (this.debug) {
|
|
399
|
-
log.bgColorLog("cyan", `Register handlers took ${Date.now() - offsetTime} ms`);
|
|
400
|
-
offsetTime = Date.now();
|
|
401
|
-
}
|
|
517
|
+
perfLog.debug(`Register handlers took ${Date.now() - handlersStartTime}ms`);
|
|
402
518
|
|
|
403
519
|
if (this.isSchedulingEnabled) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
520
|
+
if (this.leaderElectionConfig) {
|
|
521
|
+
await this.startLeaderElection();
|
|
522
|
+
} else {
|
|
523
|
+
const jobsStartTime = Date.now();
|
|
524
|
+
await this.registerAutoRegisterableJobs();
|
|
525
|
+
perfLog.debug(`Register jobs took ${Date.now() - jobsStartTime}ms`);
|
|
409
526
|
}
|
|
410
527
|
}
|
|
411
528
|
|
|
@@ -422,7 +539,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
422
539
|
});
|
|
423
540
|
|
|
424
541
|
if (this.disableHttpServer) {
|
|
425
|
-
|
|
542
|
+
initLog.info("🚧 HTTP server is disabled, but flink app is running");
|
|
426
543
|
this.started = true;
|
|
427
544
|
} else {
|
|
428
545
|
this.expressServer = this.expressApp?.listen(this.port, () => {
|
|
@@ -431,16 +548,27 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
431
548
|
});
|
|
432
549
|
}
|
|
433
550
|
|
|
551
|
+
const totalStartTime = Date.now() - startTime;
|
|
552
|
+
perfLog.info(`✓ FlinkApp started in ${totalStartTime}ms`);
|
|
553
|
+
|
|
434
554
|
return this;
|
|
435
555
|
}
|
|
436
556
|
|
|
437
557
|
async stop() {
|
|
438
558
|
log.info("🛑 Stopping Flink app...");
|
|
439
559
|
|
|
560
|
+
if (this.leaderElection) {
|
|
561
|
+
await this.leaderElection.stop();
|
|
562
|
+
}
|
|
563
|
+
|
|
440
564
|
if (this.scheduler) {
|
|
441
565
|
await this.scheduler.stop();
|
|
442
566
|
}
|
|
443
567
|
|
|
568
|
+
if (this.allInstanceScheduler) {
|
|
569
|
+
await this.allInstanceScheduler.stop();
|
|
570
|
+
}
|
|
571
|
+
|
|
444
572
|
if (this.expressServer) {
|
|
445
573
|
return new Promise<void>((resolve, reject) => {
|
|
446
574
|
const int = setTimeout(() => {
|
|
@@ -490,6 +618,21 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
490
618
|
log.warn(`${methodAndPath} overlaps existing route`);
|
|
491
619
|
}
|
|
492
620
|
|
|
621
|
+
// Use direct schemas from routeProps if provided, otherwise fall back to manifest lookup
|
|
622
|
+
let reqSchema = routeProps.reqSchema;
|
|
623
|
+
let resSchema = routeProps.resSchema;
|
|
624
|
+
let queryMetadata: any[] = [];
|
|
625
|
+
let paramsMetadata: any[] = [];
|
|
626
|
+
|
|
627
|
+
if (!reqSchema || !resSchema) {
|
|
628
|
+
const schemaManifest = this.loadSchemaManifest();
|
|
629
|
+
const metadata = handler.__file ? schemaManifest.handlers[handler.__file] : undefined;
|
|
630
|
+
if (!reqSchema) reqSchema = this.resolveSchema(metadata?.reqSchemaName);
|
|
631
|
+
if (!resSchema) resSchema = this.resolveSchema(metadata?.resSchemaName);
|
|
632
|
+
queryMetadata = metadata?.queryMetadata || [];
|
|
633
|
+
paramsMetadata = metadata?.paramsMetadata || [];
|
|
634
|
+
}
|
|
635
|
+
|
|
493
636
|
const handlerConfig: HandlerConfigWithMethod = {
|
|
494
637
|
routeProps: {
|
|
495
638
|
...routeProps,
|
|
@@ -497,21 +640,13 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
497
640
|
path: routeProps.path!,
|
|
498
641
|
},
|
|
499
642
|
schema: {
|
|
500
|
-
reqSchema
|
|
501
|
-
resSchema
|
|
643
|
+
reqSchema,
|
|
644
|
+
resSchema,
|
|
502
645
|
},
|
|
503
|
-
queryMetadata
|
|
504
|
-
paramsMetadata
|
|
646
|
+
queryMetadata,
|
|
647
|
+
paramsMetadata,
|
|
505
648
|
};
|
|
506
649
|
|
|
507
|
-
if (handler.__schemas?.reqSchema && !handlerConfig.schema?.reqSchema) {
|
|
508
|
-
log.warn(`Expected request schema ${handler.__schemas.reqSchema} for handler ${methodAndPath} but no such schema was found`);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (handler.__schemas?.resSchema && !handlerConfig.schema?.resSchema) {
|
|
512
|
-
log.warn(`Expected response schema ${handler.__schemas.resSchema} for handler ${methodAndPath} but no such schema was found`);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
650
|
this.registerHandler(handlerConfig, handler.default);
|
|
516
651
|
}
|
|
517
652
|
|
|
@@ -519,7 +654,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
519
654
|
this.handlers.push(handlerConfig);
|
|
520
655
|
|
|
521
656
|
const { routeProps, schema = {} } = handlerConfig;
|
|
522
|
-
const { method } = routeProps;
|
|
657
|
+
const { method, streamFormat } = routeProps;
|
|
523
658
|
|
|
524
659
|
if (!method) {
|
|
525
660
|
log.error(`Route ${routeProps.path} is missing http method`);
|
|
@@ -535,12 +670,45 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
535
670
|
let validateReq: ValidateFunction<any> | undefined;
|
|
536
671
|
let validateRes: ValidateFunction<any> | undefined;
|
|
537
672
|
|
|
538
|
-
|
|
539
|
-
|
|
673
|
+
// Select AJV instance (use schemaAjv for v2.0 manifests, fallback to global ajv)
|
|
674
|
+
const ajvInstance = this.schemaAjv || ajv;
|
|
675
|
+
|
|
676
|
+
// Determine validation mode (default to Validate if not specified)
|
|
677
|
+
const validationMode = routeProps.validation || ValidationMode.Validate;
|
|
678
|
+
|
|
679
|
+
// Compile request schema if validation mode requires it
|
|
680
|
+
if (schema.reqSchema && validationMode !== ValidationMode.SkipValidation && validationMode !== ValidationMode.ValidateResponse) {
|
|
681
|
+
// For v2.0 manifests with $id, use getSchema() if available
|
|
682
|
+
if (schema.reqSchema.$id && this.schemaAjv) {
|
|
683
|
+
validateReq = this.schemaAjv.getSchema(schema.reqSchema.$id);
|
|
684
|
+
if (!validateReq) {
|
|
685
|
+
log.warn(`Schema ${schema.reqSchema.$id} not found in AJV registry, compiling inline`);
|
|
686
|
+
validateReq = ajvInstance.compile(schema.reqSchema);
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
validateReq = ajvInstance.compile(schema.reqSchema);
|
|
690
|
+
}
|
|
540
691
|
}
|
|
541
692
|
|
|
542
|
-
|
|
543
|
-
|
|
693
|
+
// Skip response validation for streaming handlers (responses are stream chunks, not final JSON)
|
|
694
|
+
// Skip response validation for non-JSON response types (html, csv, etc.)
|
|
695
|
+
if (
|
|
696
|
+
!streamFormat &&
|
|
697
|
+
!routeProps.responseType &&
|
|
698
|
+
schema.resSchema &&
|
|
699
|
+
validationMode !== ValidationMode.SkipValidation &&
|
|
700
|
+
validationMode !== ValidationMode.ValidateRequest
|
|
701
|
+
) {
|
|
702
|
+
// For v2.0 manifests with $id, use getSchema() if available
|
|
703
|
+
if (schema.resSchema.$id && this.schemaAjv) {
|
|
704
|
+
validateRes = this.schemaAjv.getSchema(schema.resSchema.$id);
|
|
705
|
+
if (!validateRes) {
|
|
706
|
+
log.warn(`Schema ${schema.resSchema.$id} not found in AJV registry, compiling inline`);
|
|
707
|
+
validateRes = ajvInstance.compile(schema.resSchema);
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
validateRes = ajvInstance.compile(schema.resSchema);
|
|
711
|
+
}
|
|
544
712
|
}
|
|
545
713
|
|
|
546
714
|
this.expressApp => {
|
|
@@ -557,18 +725,23 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
557
725
|
const formattedErrors = formatValidationErrors(validateReq.errors, req.body);
|
|
558
726
|
log.warn(`[${req.reqId}] ${methodAndRoute}: Bad request\n${formattedErrors}`);
|
|
559
727
|
|
|
560
|
-
|
|
728
|
+
const errorResponse: FlinkResponse<FlinkError> = {
|
|
561
729
|
status: 400,
|
|
562
730
|
error: {
|
|
563
731
|
id: v4(),
|
|
564
732
|
title: "Bad request",
|
|
565
733
|
detail: formattedErrors,
|
|
566
734
|
},
|
|
567
|
-
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
this.invokeOnError(errorResponse, req as FlinkRequest, method!, routeProps.path);
|
|
738
|
+
|
|
739
|
+
return res.status(400).json(errorResponse);
|
|
568
740
|
}
|
|
569
741
|
}
|
|
570
742
|
|
|
571
|
-
|
|
743
|
+
// Skip mock API for streaming handlers
|
|
744
|
+
if (routeProps.mockApi && schema.resSchema && !streamFormat) {
|
|
572
745
|
log.warn(`Mock response for ${req.method.toUpperCase()} ${req.path}`);
|
|
573
746
|
|
|
574
747
|
const data = generateMockData(schema.resSchema);
|
|
@@ -580,16 +753,63 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
580
753
|
return;
|
|
581
754
|
}
|
|
582
755
|
|
|
756
|
+
// Normalize query parameters to predictable string or string[] types
|
|
757
|
+
// Express query parser can produce numbers, booleans, objects, etc.
|
|
758
|
+
// We normalize everything to strings or string arrays for consistency
|
|
759
|
+
if (req.query && typeof req.query === "object") {
|
|
760
|
+
const normalizedQuery: Record<string, string | string[]> = {};
|
|
761
|
+
for (const [key, value] of Object.entries(req.query)) {
|
|
762
|
+
if (Array.isArray(value)) {
|
|
763
|
+
// Handle array values (e.g., ?tag=a&tag=b)
|
|
764
|
+
normalizedQuery[key] = value.map((v) => String(v));
|
|
765
|
+
} else if (value !== undefined && value !== null) {
|
|
766
|
+
// Convert single values to strings
|
|
767
|
+
normalizedQuery[key] = String(value);
|
|
768
|
+
}
|
|
769
|
+
// Skip undefined/null values - they won't appear in the normalized query
|
|
770
|
+
}
|
|
771
|
+
req.query = normalizedQuery;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Create stream writer if streaming handler
|
|
775
|
+
const stream = streamFormat ? StreamWriterFactory.create(res, streamFormat) : undefined;
|
|
776
|
+
|
|
583
777
|
let handlerRes: FlinkResponse<any>;
|
|
584
778
|
|
|
585
779
|
try {
|
|
586
|
-
//
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
780
|
+
// Wrap handler execution in AsyncLocalStorage context
|
|
781
|
+
// Context persists through entire async chain: handler → agent → tools
|
|
782
|
+
const flinkReq = req as FlinkRequest;
|
|
783
|
+
handlerRes = await requestContext.run(
|
|
784
|
+
{
|
|
785
|
+
reqId: flinkReq.reqId,
|
|
786
|
+
user: flinkReq.user,
|
|
787
|
+
userPermissions: flinkReq.userPermissions,
|
|
788
|
+
method: method,
|
|
789
|
+
path: routeProps.path,
|
|
790
|
+
timestamp: Date.now(),
|
|
791
|
+
isStreaming: !!streamFormat,
|
|
792
|
+
},
|
|
793
|
+
async () => {
|
|
794
|
+
return await handler({
|
|
795
|
+
req: flinkReq,
|
|
796
|
+
ctx: this.ctx,
|
|
797
|
+
origin: routeProps.origin,
|
|
798
|
+
stream,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
);
|
|
592
802
|
} catch (err: any) {
|
|
803
|
+
// Handle errors for streaming handlers
|
|
804
|
+
if (streamFormat && stream) {
|
|
805
|
+
log.error(`Streaming handler error on ${req.method.toUpperCase()} ${req.path}: ${err.message}`, {
|
|
806
|
+
error: err,
|
|
807
|
+
path: req.path,
|
|
808
|
+
method: req.method,
|
|
809
|
+
});
|
|
810
|
+
stream.error(err);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
593
813
|
let errorResponse: FlinkResponse<FlinkError>;
|
|
594
814
|
|
|
595
815
|
// duck typing to check if it is a FlinkError
|
|
@@ -609,50 +829,78 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
609
829
|
errorResponse = internalServerError(err as any);
|
|
610
830
|
}
|
|
611
831
|
|
|
612
|
-
|
|
613
|
-
if (this.onError) {
|
|
614
|
-
try {
|
|
615
|
-
const result = this.onError(errorResponse, {
|
|
616
|
-
req: req as FlinkRequest,
|
|
617
|
-
method: method!,
|
|
618
|
-
path: routeProps.path,
|
|
619
|
-
reqId: req.reqId,
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
// Handle async callbacks - don't wait for them
|
|
623
|
-
if (result instanceof Promise) {
|
|
624
|
-
result.catch((callbackErr) => {
|
|
625
|
-
log.error(`onError callback rejected with: ${callbackErr}`);
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
} catch (callbackErr) {
|
|
629
|
-
log.error(`onError callback threw an exception: ${callbackErr}`);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
832
|
+
this.invokeOnError(errorResponse, req as FlinkRequest, method!, routeProps.path);
|
|
632
833
|
|
|
633
834
|
return res.status(errorResponse.status || 500).json(errorResponse);
|
|
634
835
|
}
|
|
635
836
|
|
|
837
|
+
// Skip response handling for streaming handlers (stream controls response lifecycle)
|
|
838
|
+
if (streamFormat) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Ensure handlerRes is defined for non-streaming handlers
|
|
843
|
+
if (!handlerRes) {
|
|
844
|
+
return res.status(204).send();
|
|
845
|
+
}
|
|
846
|
+
|
|
636
847
|
if (validateRes && !isError(handlerRes)) {
|
|
637
|
-
|
|
848
|
+
if (handlerRes.data === undefined) {
|
|
849
|
+
if (handlerRes.status !== 204) {
|
|
850
|
+
const detail =
|
|
851
|
+
"Response schema is defined but handler returned no data";
|
|
852
|
+
log.warn(`[${req.reqId}] ${methodAndRoute}: Bad response - ${detail}`);
|
|
638
853
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
854
|
+
const errorResponse: FlinkResponse<FlinkError> = {
|
|
855
|
+
status: 500,
|
|
856
|
+
error: { id: v4(), title: "Bad response", detail },
|
|
857
|
+
};
|
|
642
858
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
859
|
+
this.invokeOnError(errorResponse, req as FlinkRequest, method!, routeProps.path);
|
|
860
|
+
|
|
861
|
+
return res.status(500).json(errorResponse);
|
|
862
|
+
}
|
|
863
|
+
} else {
|
|
864
|
+
const valid = validateRes(JSON.parse(JSON.stringify(handlerRes.data)));
|
|
865
|
+
|
|
866
|
+
if (!valid) {
|
|
867
|
+
const formattedErrors = formatValidationErrors(validateRes.errors, handlerRes.data);
|
|
868
|
+
log.warn(`[${req.reqId}] ${methodAndRoute}: Bad response\n${formattedErrors}`);
|
|
869
|
+
|
|
870
|
+
const errorResponse: FlinkResponse<FlinkError> = {
|
|
871
|
+
status: 500,
|
|
872
|
+
error: {
|
|
873
|
+
id: v4(),
|
|
874
|
+
title: "Bad response",
|
|
875
|
+
detail: formattedErrors,
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
this.invokeOnError(errorResponse, req as FlinkRequest, method!, routeProps.path);
|
|
880
|
+
|
|
881
|
+
return res.status(500).json(errorResponse);
|
|
882
|
+
}
|
|
651
883
|
}
|
|
652
884
|
}
|
|
653
885
|
|
|
654
886
|
res.set(handlerRes.headers);
|
|
655
887
|
|
|
888
|
+
if (routeProps.responseType) {
|
|
889
|
+
return res
|
|
890
|
+
.status(handlerRes.status || 200)
|
|
891
|
+
.type(routeProps.responseType)
|
|
892
|
+
.send(handlerRes.data);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (handlerRes.error?.meta !== undefined) {
|
|
896
|
+
try {
|
|
897
|
+
JSON.stringify(handlerRes.error.meta);
|
|
898
|
+
} catch (e) {
|
|
899
|
+
log.warn(`[${handlerRes.reqId}] error.meta stripped from error ${handlerRes.error.id}: not JSON-serializable (${(e as Error).message})`);
|
|
900
|
+
delete handlerRes.error.meta;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
656
904
|
res.status(handlerRes.status || 200).json(handlerRes);
|
|
657
905
|
});
|
|
658
906
|
|
|
@@ -661,9 +909,222 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
661
909
|
return process.exit(1); // TODO: Do we need to exit?
|
|
662
910
|
} else {
|
|
663
911
|
this.handlerRouteCache.set(methodAndRoute, JSON.stringify(routeProps));
|
|
664
|
-
|
|
912
|
+
initLog.info(`Registered ${streamFormat ? "streaming " : ""}route ${methodAndRoute}${streamFormat ? ` (${streamFormat})` : ""}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Load schema manifest from .flink directory.
|
|
919
|
+
* Returns empty structure if manifest doesn't exist (dev mode without build).
|
|
920
|
+
*
|
|
921
|
+
* The manifest contains:
|
|
922
|
+
* - definitions: ALL JSON Schema type definitions (supports $ref resolution)
|
|
923
|
+
* - handlers: Handler metadata with schema names (references to definitions)
|
|
924
|
+
* - tools: Tool metadata with schema names (references to definitions)
|
|
925
|
+
*/
|
|
926
|
+
private loadSchemaManifest(): {
|
|
927
|
+
version?: string;
|
|
928
|
+
definitions?: Record<string, any>;
|
|
929
|
+
schemas?: Record<string, any>;
|
|
930
|
+
handlers: Record<
|
|
931
|
+
string,
|
|
932
|
+
{
|
|
933
|
+
reqSchemaName?: string;
|
|
934
|
+
resSchemaName?: string;
|
|
935
|
+
queryMetadata?: any[];
|
|
936
|
+
paramsMetadata?: any[];
|
|
937
|
+
assumedMethod?: string;
|
|
938
|
+
}
|
|
939
|
+
>;
|
|
940
|
+
tools: Record<
|
|
941
|
+
string,
|
|
942
|
+
{
|
|
943
|
+
inputSchemaName?: string;
|
|
944
|
+
outputSchemaName?: string;
|
|
945
|
+
inputTypeHint?: "void" | "any" | "named";
|
|
946
|
+
outputTypeHint?: "void" | "any" | "named";
|
|
947
|
+
}
|
|
948
|
+
>;
|
|
949
|
+
} {
|
|
950
|
+
// Return cached manifest if already loaded
|
|
951
|
+
if (this.schemaManifest) {
|
|
952
|
+
return this.schemaManifest;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const fs = require("fs");
|
|
956
|
+
const path = require("path");
|
|
957
|
+
const manifestPath = path.join(process.cwd(), "dist/.flink/schema-manifest.json");
|
|
958
|
+
|
|
959
|
+
if (!fs.existsSync(manifestPath)) {
|
|
960
|
+
log.warn("Schema manifest not found at dist/.flink/schema-manifest.json - handlers/tools may not have validation schemas");
|
|
961
|
+
const emptyManifest = { definitions: {}, handlers: {}, tools: {} };
|
|
962
|
+
this.schemaManifest = emptyManifest;
|
|
963
|
+
return emptyManifest;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
try {
|
|
967
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
968
|
+
|
|
969
|
+
// Check version for backward compatibility
|
|
970
|
+
if (manifest.version === "2.0") {
|
|
971
|
+
// New format with schema universe and AJV global registry
|
|
972
|
+
this.schemaManifest = manifest;
|
|
973
|
+
|
|
974
|
+
// Initialize AJV and register all schemas
|
|
975
|
+
this.schemaAjv = new Ajv({
|
|
976
|
+
strict: false, // Allow additional properties by default
|
|
977
|
+
allErrors: true, // Return all validation errors, not just first
|
|
978
|
+
});
|
|
979
|
+
addFormats(this.schemaAjv);
|
|
980
|
+
|
|
981
|
+
// Register all schemas in the universe
|
|
982
|
+
const schemas = manifest.schemas || {};
|
|
983
|
+
for (const schema of Object.values(schemas)) {
|
|
984
|
+
if (schema && typeof schema === "object" && (schema as any).$id) {
|
|
985
|
+
// Skip schemas with unresolved generic type parameters (T, U, V, etc.)
|
|
986
|
+
if (this.hasUnresolvedTypeParams(schema, Object.keys(schemas))) {
|
|
987
|
+
log.debug(`Skipping registration of generic schema: ${(schema as any).$id}`);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
this.schemaAjv.addSchema(schema);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
log.debug(
|
|
995
|
+
`Loaded schema manifest v2.0: ${Object.keys(schemas).length} schemas, ${Object.keys(manifest.handlers).length} handlers, ${
|
|
996
|
+
Object.keys(manifest.tools).length
|
|
997
|
+
} tools`
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
return manifest;
|
|
1001
|
+
} else {
|
|
1002
|
+
// Old format (v1.0) - still supported for migration
|
|
1003
|
+
log.debug(
|
|
1004
|
+
`Loaded schema manifest v1.0: ${Object.keys(manifest.definitions || {}).length} definitions, ${Object.keys(manifest.handlers).length} handlers, ${
|
|
1005
|
+
Object.keys(manifest.tools).length
|
|
1006
|
+
} tools`
|
|
1007
|
+
);
|
|
1008
|
+
this.schemaManifest = manifest;
|
|
1009
|
+
return manifest;
|
|
1010
|
+
}
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
log.error("Failed to parse schema manifest:", error);
|
|
1013
|
+
const errorManifest = { definitions: {}, handlers: {}, tools: {} };
|
|
1014
|
+
this.schemaManifest = errorManifest;
|
|
1015
|
+
return errorManifest;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Get the AJV instance for validation (v2.0 manifests).
|
|
1021
|
+
* Returns undefined for v1.0 manifests.
|
|
1022
|
+
*/
|
|
1023
|
+
public getSchemaAjv(): Ajv | undefined {
|
|
1024
|
+
return this.schemaAjv;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Register plugin schemas into the app's AJV instance.
|
|
1029
|
+
* Schema $id values are prefixed with `pluginId::` to avoid collisions
|
|
1030
|
+
* with the app's own schemas or other plugins.
|
|
1031
|
+
*
|
|
1032
|
+
* @param pluginId Unique identifier for the plugin (used as namespace prefix)
|
|
1033
|
+
* @param schemas Record of schema name to JSON Schema object (already prefixed)
|
|
1034
|
+
*/
|
|
1035
|
+
public registerSchemas(pluginId: string, schemas: Record<string, any>): void {
|
|
1036
|
+
// Ensure schema manifest and AJV are initialized
|
|
1037
|
+
this.loadSchemaManifest();
|
|
1038
|
+
|
|
1039
|
+
if (!this.schemaAjv) {
|
|
1040
|
+
// If no v2.0 manifest exists, create a fresh AJV instance
|
|
1041
|
+
this.schemaAjv = new Ajv({
|
|
1042
|
+
strict: false,
|
|
1043
|
+
allErrors: true,
|
|
1044
|
+
});
|
|
1045
|
+
addFormats(this.schemaAjv);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
for (const schema of Object.values(schemas)) {
|
|
1049
|
+
if (schema && typeof schema === "object" && schema.$id) {
|
|
1050
|
+
// Skip if already registered
|
|
1051
|
+
if (this.schemaAjv.getSchema(schema.$id)) {
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
this.schemaAjv.addSchema(schema);
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
log.warn(`Failed to register plugin schema ${schema.$id}: ${err}`);
|
|
1058
|
+
}
|
|
665
1059
|
}
|
|
666
1060
|
}
|
|
1061
|
+
|
|
1062
|
+
log.debug(`Registered ${Object.keys(schemas).length} schemas from plugin '${pluginId}'`);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Check if a schema has unresolved generic type parameter references.
|
|
1067
|
+
* Generic type parameters like T, U, V are single uppercase letters.
|
|
1068
|
+
*
|
|
1069
|
+
* @param schema JSON schema object
|
|
1070
|
+
* @param registeredSchemaIds List of schema IDs in the manifest
|
|
1071
|
+
* @returns true if schema references generic type params that don't exist
|
|
1072
|
+
*/
|
|
1073
|
+
private hasUnresolvedTypeParams(schema: any, registeredSchemaIds: string[]): boolean {
|
|
1074
|
+
const schemaIdSet = new Set(registeredSchemaIds);
|
|
1075
|
+
|
|
1076
|
+
const checkRefs = (obj: any): boolean => {
|
|
1077
|
+
if (!obj || typeof obj !== "object") {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (Array.isArray(obj)) {
|
|
1082
|
+
return obj.some((item) => checkRefs(item));
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1086
|
+
if (key === "$ref" && typeof value === "string") {
|
|
1087
|
+
// Check if ref is a single uppercase letter (generic type param)
|
|
1088
|
+
// and not in the registered schema IDs
|
|
1089
|
+
if (/^[A-Z]$/.test(value) && !schemaIdSet.has(value)) {
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
} else if (typeof value === "object") {
|
|
1093
|
+
if (checkRefs(value)) {
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
return false;
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
return checkRefs(schema);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Resolve schema by name from manifest.
|
|
1107
|
+
* Works with both v1.0 (definitions) and v2.0 (schema universe) formats.
|
|
1108
|
+
*
|
|
1109
|
+
* @param schemaName Schema name or $id
|
|
1110
|
+
* @returns JSON schema object or undefined
|
|
1111
|
+
*/
|
|
1112
|
+
private resolveSchema(schemaName: string | undefined): any {
|
|
1113
|
+
if (!schemaName) return undefined;
|
|
1114
|
+
|
|
1115
|
+
const manifest = this.loadSchemaManifest();
|
|
1116
|
+
|
|
1117
|
+
// v2.0 manifest - return schema from universe
|
|
1118
|
+
if (manifest.version === "2.0" && manifest.schemas) {
|
|
1119
|
+
return manifest.schemas[schemaName];
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// v1.0 manifest - return from definitions
|
|
1123
|
+
if (manifest.definitions) {
|
|
1124
|
+
return manifest.definitions[schemaName];
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return undefined;
|
|
667
1128
|
}
|
|
668
1129
|
|
|
669
1130
|
/**
|
|
@@ -673,30 +1134,70 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
673
1134
|
* Will not register any handlers added programmatically.
|
|
674
1135
|
*/
|
|
675
1136
|
private async registerAutoRegisterableHandlers() {
|
|
676
|
-
|
|
1137
|
+
// Load schema manifest once
|
|
1138
|
+
const schemaManifest = this.loadSchemaManifest();
|
|
1139
|
+
const schemaCount =
|
|
1140
|
+
schemaManifest.version === "2.0" ? Object.keys(schemaManifest.schemas || {}).length : Object.keys(schemaManifest.definitions || {}).length;
|
|
1141
|
+
|
|
1142
|
+
log.debug(`Registering ${schemaCount} schemas with AJV (manifest version: ${schemaManifest.version || "1.0"})`);
|
|
1143
|
+
|
|
1144
|
+
for (const { handler, assumedHttpMethod, __file } of autoRegisteredHandlers.sort((a, b) => {
|
|
1145
|
+
const orderDiff = (a.handler.Route?.order || 0) - (b.handler.Route?.order || 0);
|
|
1146
|
+
if (orderDiff !== 0) return orderDiff;
|
|
1147
|
+
// Static segments must be registered before parameterized ones to avoid
|
|
1148
|
+
// Express matching e.g. GET /jobs/by-tags with the /jobs/:id route.
|
|
1149
|
+
const aHasParam = a.handler.Route?.path?.includes("/:") ? 1 : 0;
|
|
1150
|
+
const bHasParam = b.handler.Route?.path?.includes("/:") ? 1 : 0;
|
|
1151
|
+
return aHasParam - bHasParam;
|
|
1152
|
+
})) {
|
|
677
1153
|
if (!handler.Route) {
|
|
678
|
-
log.error(`Missing Props in handler ${
|
|
1154
|
+
log.error(`Missing Props in handler ${__file}`);
|
|
679
1155
|
continue;
|
|
680
1156
|
}
|
|
681
1157
|
|
|
682
1158
|
if (!handler.default) {
|
|
683
|
-
log.error(`Missing exported handler function in handler ${
|
|
1159
|
+
log.error(`Missing exported handler function in handler ${__file}`);
|
|
684
1160
|
continue;
|
|
685
1161
|
}
|
|
686
1162
|
|
|
687
|
-
|
|
1163
|
+
// Look up ALL metadata by file path from the schema manifest
|
|
1164
|
+
const metadata = schemaManifest.handlers[__file || ""];
|
|
1165
|
+
|
|
1166
|
+
// Use direct schemas from Route if provided, otherwise resolve from manifest
|
|
1167
|
+
const reqSchema = handler.Route.reqSchema || this.resolveSchema(metadata?.reqSchemaName);
|
|
1168
|
+
const resSchema = handler.Route.resSchema || this.resolveSchema(metadata?.resSchemaName);
|
|
1169
|
+
|
|
1170
|
+
// Validation warnings
|
|
1171
|
+
if (
|
|
1172
|
+
!metadata &&
|
|
1173
|
+
(handler.Route.validation === ValidationMode.Validate ||
|
|
1174
|
+
handler.Route.validation === ValidationMode.ValidateRequest ||
|
|
1175
|
+
handler.Route.validation === ValidationMode.ValidateResponse)
|
|
1176
|
+
) {
|
|
1177
|
+
log.warn(`Handler ${__file} expects validation but no metadata found in manifest`);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Warn if schema name doesn't resolve
|
|
1181
|
+
if (metadata?.reqSchemaName && !reqSchema) {
|
|
1182
|
+
log.warn(`Handler ${__file} references reqSchema "${metadata.reqSchemaName}" but not found in schema universe`);
|
|
1183
|
+
}
|
|
1184
|
+
if (metadata?.resSchemaName && !resSchema) {
|
|
1185
|
+
log.warn(`Handler ${__file} references resSchema "${metadata.resSchemaName}" but not found in schema universe`);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (!!metadata?.paramsMetadata?.length) {
|
|
688
1189
|
const pathParams = getPathParams(handler.Route.path);
|
|
689
1190
|
|
|
690
|
-
for (const param of
|
|
1191
|
+
for (const param of metadata.paramsMetadata) {
|
|
691
1192
|
if (!pathParams.includes(param.name)) {
|
|
692
|
-
log.error(`Handler ${
|
|
1193
|
+
log.error(`Handler ${__file} has param ${param.name} but it is not present in the path '${handler.Route.path}'`);
|
|
693
1194
|
throw new Error("Invalid/missing handler path param");
|
|
694
1195
|
}
|
|
695
1196
|
}
|
|
696
1197
|
|
|
697
|
-
if (pathParams.length !==
|
|
1198
|
+
if (pathParams.length !== metadata.paramsMetadata.length) {
|
|
698
1199
|
log.warn(
|
|
699
|
-
`Handler ${
|
|
1200
|
+
`Handler ${__file} has ${metadata.paramsMetadata.length} typed params but the path '${handler.Route.path}' has ${pathParams.length} params`
|
|
700
1201
|
);
|
|
701
1202
|
}
|
|
702
1203
|
}
|
|
@@ -705,55 +1206,58 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
705
1206
|
{
|
|
706
1207
|
routeProps: {
|
|
707
1208
|
...handler.Route,
|
|
708
|
-
method: handler.Route.method || assumedHttpMethod,
|
|
1209
|
+
method: handler.Route.method || assumedHttpMethod || metadata?.assumedMethod,
|
|
709
1210
|
origin: this.name,
|
|
710
1211
|
},
|
|
711
1212
|
schema: {
|
|
712
|
-
reqSchema
|
|
713
|
-
resSchema
|
|
1213
|
+
reqSchema,
|
|
1214
|
+
resSchema,
|
|
714
1215
|
},
|
|
715
|
-
queryMetadata:
|
|
716
|
-
paramsMetadata:
|
|
1216
|
+
queryMetadata: metadata?.queryMetadata || [],
|
|
1217
|
+
paramsMetadata: metadata?.paramsMetadata || [],
|
|
717
1218
|
},
|
|
718
1219
|
handler.default
|
|
719
1220
|
);
|
|
720
1221
|
}
|
|
721
1222
|
}
|
|
722
1223
|
|
|
723
|
-
private async registerAutoRegisterableJobs() {
|
|
1224
|
+
private async registerAutoRegisterableJobs(filter?: (jobProps: FlinkJobProps) => boolean) {
|
|
724
1225
|
if (!this.scheduler) {
|
|
725
1226
|
throw new Error("Scheduler not initialized"); // should never happen
|
|
726
1227
|
}
|
|
727
1228
|
|
|
728
1229
|
for (const { Job: jobProps, default: jobFn, __file } of autoRegisteredJobs) {
|
|
1230
|
+
if (filter && !filter(jobProps)) {
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
729
1233
|
if (jobProps.cron && jobProps.interval) {
|
|
730
|
-
|
|
1234
|
+
schedulerLog.error(`Cannot register job ${jobProps.id} - both cron and interval are set in ${__file}`);
|
|
731
1235
|
continue;
|
|
732
1236
|
}
|
|
733
1237
|
|
|
734
1238
|
if (jobProps.cron && jobProps.afterDelay) {
|
|
735
|
-
|
|
1239
|
+
schedulerLog.error(`Cannot register job ${jobProps.id} - both cron and afterDelay are set in ${__file}`);
|
|
736
1240
|
continue;
|
|
737
1241
|
}
|
|
738
1242
|
|
|
739
1243
|
if (jobProps.interval && jobProps.afterDelay) {
|
|
740
|
-
|
|
1244
|
+
schedulerLog.error(`Cannot register job ${jobProps.id} - both interval and afterDelay are set in ${__file}`);
|
|
741
1245
|
continue;
|
|
742
1246
|
}
|
|
743
1247
|
|
|
744
1248
|
if (this.scheduler.existsById(jobProps.id)) {
|
|
745
|
-
|
|
1249
|
+
schedulerLog.error(`Job with id ${jobProps.id} is already registered, found duplicate in ${__file}`);
|
|
746
1250
|
continue;
|
|
747
1251
|
}
|
|
748
1252
|
|
|
749
|
-
|
|
1253
|
+
schedulerLog.debug(`Registering job ${jobProps.id}: ${JSON.stringify(jobProps)} from ${__file}`);
|
|
750
1254
|
|
|
751
1255
|
const task = new AsyncTask(
|
|
752
1256
|
jobProps.id,
|
|
753
1257
|
async () => {
|
|
754
1258
|
await jobFn({ ctx: this.ctx });
|
|
755
1259
|
|
|
756
|
-
|
|
1260
|
+
schedulerLog.debug(`Job ${jobProps.id} completed`);
|
|
757
1261
|
|
|
758
1262
|
if (jobProps.afterDelay) {
|
|
759
1263
|
// afterDelay runs only once, so we remove the job
|
|
@@ -761,7 +1265,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
761
1265
|
}
|
|
762
1266
|
},
|
|
763
1267
|
(err) => {
|
|
764
|
-
|
|
1268
|
+
schedulerLog.error(`Job ${jobProps.id} threw unhandled exception ${err}`);
|
|
765
1269
|
console.error(err);
|
|
766
1270
|
}
|
|
767
1271
|
);
|
|
@@ -788,20 +1292,32 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
788
1292
|
|
|
789
1293
|
this.scheduler.addSimpleIntervalJob(job);
|
|
790
1294
|
} else if (jobProps.afterDelay !== undefined) {
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
1295
|
+
const delayMs = ms(jobProps.afterDelay);
|
|
1296
|
+
if (delayMs === 0) {
|
|
1297
|
+
setImmediate(async () => {
|
|
1298
|
+
try {
|
|
1299
|
+
await jobFn({ ctx: this.ctx });
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
schedulerLog.error(`Job ${jobProps.id} threw unhandled exception ${err}`);
|
|
1302
|
+
console.error(err);
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
} else {
|
|
1306
|
+
const job = new SimpleIntervalJob(
|
|
1307
|
+
{
|
|
1308
|
+
milliseconds: delayMs,
|
|
1309
|
+
runImmediately: false,
|
|
1310
|
+
},
|
|
1311
|
+
task,
|
|
1312
|
+
{
|
|
1313
|
+
id: jobProps.id,
|
|
1314
|
+
preventOverrun: jobProps.singleton,
|
|
1315
|
+
}
|
|
1316
|
+
);
|
|
1317
|
+
this.scheduler.addSimpleIntervalJob(job);
|
|
1318
|
+
}
|
|
803
1319
|
} else {
|
|
804
|
-
|
|
1320
|
+
schedulerLog.error(`Cannot register job ${jobProps.id} - no cron, interval or once set in ${__file}`);
|
|
805
1321
|
continue;
|
|
806
1322
|
}
|
|
807
1323
|
}
|
|
@@ -813,6 +1329,142 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
813
1329
|
// repoInstance.ctx = this.ctx;
|
|
814
1330
|
}
|
|
815
1331
|
|
|
1332
|
+
private async registerAutoRegisterableTools() {
|
|
1333
|
+
const { ToolExecutor } = require("./ai/ToolExecutor");
|
|
1334
|
+
const { getRepoInstanceName } = require("./utils");
|
|
1335
|
+
|
|
1336
|
+
// Load schema manifest once
|
|
1337
|
+
const schemaManifest = this.loadSchemaManifest();
|
|
1338
|
+
|
|
1339
|
+
for (const toolFile of autoRegisteredTools) {
|
|
1340
|
+
if (!toolFile.Tool) {
|
|
1341
|
+
log.error(`Missing FlinkToolProps export in tool ${toolFile.__file}`);
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (!toolFile.default) {
|
|
1346
|
+
log.error(`Missing exported tool function in tool ${toolFile.__file}`);
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
const toolId = toolFile.Tool.id;
|
|
1351
|
+
if (!toolId) {
|
|
1352
|
+
log.error(`Tool ${toolFile.__file} missing 'id' property`);
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const toolInstanceName = getRepoInstanceName(toolId);
|
|
1357
|
+
|
|
1358
|
+
// Look up schema names from manifest
|
|
1359
|
+
const metadata = schemaManifest.tools[toolFile.__file || ""];
|
|
1360
|
+
|
|
1361
|
+
// Resolve schemas using helper (works with both v1.0 and v2.0)
|
|
1362
|
+
const schemas = metadata
|
|
1363
|
+
? {
|
|
1364
|
+
inputSchema: this.resolveSchema(metadata.inputSchemaName),
|
|
1365
|
+
outputSchema: this.resolveSchema(metadata.outputSchemaName),
|
|
1366
|
+
inputTypeHint: metadata.inputTypeHint,
|
|
1367
|
+
outputTypeHint: metadata.outputTypeHint,
|
|
1368
|
+
}
|
|
1369
|
+
: undefined;
|
|
1370
|
+
|
|
1371
|
+
// Warn if schema name doesn't resolve
|
|
1372
|
+
if (metadata?.inputSchemaName && !schemas?.inputSchema) {
|
|
1373
|
+
log.warn(`Tool ${toolFile.__file} references inputSchema "${metadata.inputSchemaName}" but not found in schema universe`);
|
|
1374
|
+
}
|
|
1375
|
+
if (metadata?.outputSchemaName && !schemas?.outputSchema) {
|
|
1376
|
+
log.warn(`Tool ${toolFile.__file} references outputSchema "${metadata.outputSchemaName}" but not found in schema universe`);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Pass full schema universe so AJV can resolve $ref across schemas
|
|
1380
|
+
const allSchemas = schemaManifest.version === "2.0" ? schemaManifest.schemas : schemaManifest.definitions;
|
|
1381
|
+
|
|
1382
|
+
const toolExecutor = new ToolExecutor(
|
|
1383
|
+
toolFile.Tool,
|
|
1384
|
+
toolFile.default,
|
|
1385
|
+
this.ctx,
|
|
1386
|
+
schemas, // Auto-generated schemas from manifest (resolved from definitions)
|
|
1387
|
+
allSchemas
|
|
1388
|
+
);
|
|
1389
|
+
this.tools[toolInstanceName] = toolExecutor;
|
|
1390
|
+
|
|
1391
|
+
initLog.info(`Registered tool ${toolInstanceName} (${toolId})`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
private async registerAutoRegisterableAgents() {
|
|
1396
|
+
const { getRepoInstanceName, toKebabCase } = require("./utils");
|
|
1397
|
+
|
|
1398
|
+
for (const agentFile of autoRegisteredAgents) {
|
|
1399
|
+
// agentFile now exports a class, not a config object
|
|
1400
|
+
const AgentClass = agentFile.default;
|
|
1401
|
+
|
|
1402
|
+
if (!AgentClass) {
|
|
1403
|
+
log.error(`Missing default export in agent ${agentFile.__file}`);
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Instantiate agent (similar to repo instantiation)
|
|
1408
|
+
const agentInstance = new AgentClass();
|
|
1409
|
+
|
|
1410
|
+
// Derive instance name from class name (camelCase)
|
|
1411
|
+
const agentInstanceName = getRepoInstanceName(AgentClass.name);
|
|
1412
|
+
|
|
1413
|
+
// Get agent ID (kebab-case) - either explicit or derived
|
|
1414
|
+
const agentId = agentInstance.id;
|
|
1415
|
+
|
|
1416
|
+
// Check for duplicate instance name
|
|
1417
|
+
if (this.agents[agentInstanceName]) {
|
|
1418
|
+
const existingAgent = this.agents[agentInstanceName];
|
|
1419
|
+
throw new Error(
|
|
1420
|
+
`Duplicate agent instance name: "${agentInstanceName}". ` +
|
|
1421
|
+
`Agent class "${AgentClass.name}" conflicts with existing agent "${existingAgent.constructor.name}". ` +
|
|
1422
|
+
`Instance names are derived by lowercasing the first letter of the class name. ` +
|
|
1423
|
+
`Rename one of the classes or use a unique explicit 'id' property.`
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// Check for duplicate agent ID
|
|
1428
|
+
const existingAgentWithSameId = Object.values(this.agents).find((agent: any) => agent.id === agentId);
|
|
1429
|
+
|
|
1430
|
+
if (existingAgentWithSameId) {
|
|
1431
|
+
throw new Error(
|
|
1432
|
+
`Duplicate agent ID: "${agentId}". ` +
|
|
1433
|
+
`Agent class "${AgentClass.name}" conflicts with existing agent "${existingAgentWithSameId.constructor.name}". ` +
|
|
1434
|
+
`Change the 'id' property on one of them to resolve the conflict.`
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// Validate tools exist
|
|
1439
|
+
if (agentInstance.tools) {
|
|
1440
|
+
for (const toolRef of agentInstance.tools) {
|
|
1441
|
+
// Handle string IDs, tool file references, and tool props
|
|
1442
|
+
let toolId: string;
|
|
1443
|
+
if (typeof toolRef === "string") {
|
|
1444
|
+
toolId = toolRef;
|
|
1445
|
+
} else if ("Tool" in toolRef) {
|
|
1446
|
+
// FlinkToolFile - extract ID from Tool property
|
|
1447
|
+
toolId = toolRef.Tool.id;
|
|
1448
|
+
} else {
|
|
1449
|
+
// FlinkToolProps - extract ID directly
|
|
1450
|
+
toolId = toolRef.id;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const tool = this.tools[toolId];
|
|
1454
|
+
|
|
1455
|
+
if (!tool) {
|
|
1456
|
+
log.error(`Agent ${AgentClass.name} references tool ${toolId} which is not registered`);
|
|
1457
|
+
throw new Error(`Invalid tool reference in agent ${AgentClass.name}`);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Register agent (duplicate checks already performed above)
|
|
1463
|
+
this.agents[agentInstanceName] = agentInstance;
|
|
1464
|
+
initLog.info(`Registered agent ${agentInstanceName} (${AgentClass.name}) with ID: ${agentId}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
816
1468
|
/**
|
|
817
1469
|
* Constructs the app context. Will inject context in all components
|
|
818
1470
|
* except for handlers which are handled in later stage.
|
|
@@ -824,7 +1476,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
824
1476
|
|
|
825
1477
|
this.repos[repoInstanceName] = repoInstance;
|
|
826
1478
|
|
|
827
|
-
|
|
1479
|
+
initLog.info(`Registered repo ${repoInstanceName}`);
|
|
828
1480
|
}
|
|
829
1481
|
} else if (autoRegisteredRepos.length > 0) {
|
|
830
1482
|
log.warn(`No db configured but found repo(s)`);
|
|
@@ -838,15 +1490,47 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
838
1490
|
return out;
|
|
839
1491
|
}, {});
|
|
840
1492
|
|
|
1493
|
+
// Instantiate services (ctx not yet available - constructors must not access it)
|
|
1494
|
+
for (const { serviceInstanceName, Service } of autoRegisteredServices) {
|
|
1495
|
+
const serviceInstance: FlinkService<C> = new Service();
|
|
1496
|
+
this.services[serviceInstanceName] = serviceInstance;
|
|
1497
|
+
initLog.info(`Registered service ${serviceInstanceName}`);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
841
1500
|
this._ctx = {
|
|
842
1501
|
repos: this.repos,
|
|
843
1502
|
plugins: pluginCtx,
|
|
844
1503
|
auth: this.auth,
|
|
1504
|
+
agents: this.agents,
|
|
1505
|
+
services: this.services,
|
|
845
1506
|
} as C;
|
|
846
1507
|
|
|
1508
|
+
// Inject context into repos
|
|
847
1509
|
for (const repo of Object.values(this.repos)) {
|
|
848
1510
|
repo.ctx = this.ctx;
|
|
849
1511
|
}
|
|
1512
|
+
|
|
1513
|
+
// Inject context into services, then call onInit() in parallel
|
|
1514
|
+
for (const service of Object.values(this.services)) {
|
|
1515
|
+
service.ctx = this.ctx;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const servicesWithInit = Object.values(this.services).filter((s) => typeof s.onInit === "function");
|
|
1519
|
+
if (servicesWithInit.length > 0) {
|
|
1520
|
+
await Promise.all(servicesWithInit.map((s) => s.onInit!()));
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Initialize agents after they've been registered and context is ready
|
|
1526
|
+
* Must be called after registerAutoRegisterableAgents()
|
|
1527
|
+
*/
|
|
1528
|
+
private async initializeAgents() {
|
|
1529
|
+
// Inject context and initialize agents
|
|
1530
|
+
for (const agent of Object.values(this.agents)) {
|
|
1531
|
+
agent.ctx = this.ctx;
|
|
1532
|
+
agent.__init(this.llmAdapters, this.tools, this.agentObserver);
|
|
1533
|
+
}
|
|
850
1534
|
}
|
|
851
1535
|
|
|
852
1536
|
/**
|
|
@@ -888,7 +1572,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
888
1572
|
}
|
|
889
1573
|
} else if (plugin.db.uri) {
|
|
890
1574
|
try {
|
|
891
|
-
|
|
1575
|
+
initLog.debug(`Connecting to '${plugin.id}' db`);
|
|
892
1576
|
const client = await MongoClient.connect(plugin.db.uri, this.getMongoConnectionOptions());
|
|
893
1577
|
return client.db();
|
|
894
1578
|
} catch (err) {
|
|
@@ -902,7 +1586,37 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
902
1586
|
if (!this.auth) {
|
|
903
1587
|
throw new Error(`Attempting to authenticate request (${req.method} ${req.path}) but no authPlugin is set`);
|
|
904
1588
|
}
|
|
905
|
-
return await this.auth.authenticateRequest(req as FlinkRequest, permissions);
|
|
1589
|
+
return await this.auth.authenticateRequest(req as FlinkRequest, permissions, this._ctx);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Invokes the optional onError callback in a fire-and-forget manner.
|
|
1594
|
+
* Any error thrown or rejected by the callback is caught and logged so
|
|
1595
|
+
* it never affects the error response sent to the client.
|
|
1596
|
+
*/
|
|
1597
|
+
private invokeOnError(errorResponse: FlinkResponse<FlinkError>, req: FlinkRequest, method: HttpMethod, path: string) {
|
|
1598
|
+
if (!this.onError) {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
try {
|
|
1603
|
+
const result = this.onError(errorResponse, {
|
|
1604
|
+
req,
|
|
1605
|
+
method,
|
|
1606
|
+
path,
|
|
1607
|
+
reqId: req.reqId,
|
|
1608
|
+
ctx: this.ctx,
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
// Handle async callbacks - don't wait for them
|
|
1612
|
+
if (result instanceof Promise) {
|
|
1613
|
+
result.catch((callbackErr) => {
|
|
1614
|
+
log.error(`onError callback rejected with: ${callbackErr}`);
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
} catch (callbackErr) {
|
|
1618
|
+
log.error(`onError callback threw an exception: ${callbackErr}`);
|
|
1619
|
+
}
|
|
906
1620
|
}
|
|
907
1621
|
|
|
908
1622
|
public getRegisteredRoutes() {
|
|
@@ -913,6 +1627,56 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
913
1627
|
return this.schedulingOptions?.enabled !== false;
|
|
914
1628
|
}
|
|
915
1629
|
|
|
1630
|
+
private get leaderElectionConfig(): LeaderElectionOptions | undefined {
|
|
1631
|
+
const opt = this.schedulingOptions?.leaderElection;
|
|
1632
|
+
if (!opt) return undefined;
|
|
1633
|
+
return opt === true ? {} : opt;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
private async startLeaderElection() {
|
|
1637
|
+
if (!this.db) {
|
|
1638
|
+
schedulerLog.warn(
|
|
1639
|
+
"Leader election is enabled but no database is configured. " +
|
|
1640
|
+
"Leader election requires a MongoDB connection to coordinate between instances. " +
|
|
1641
|
+
"Either add a database connection via the `db` option, or remove `scheduling.leaderElection` from your config. " +
|
|
1642
|
+
"Jobs will run on ALL instances without leader election."
|
|
1643
|
+
);
|
|
1644
|
+
// Fall back to running jobs on all instances
|
|
1645
|
+
this.scheduler = new ToadScheduler();
|
|
1646
|
+
await this.registerAutoRegisterableJobs();
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// Register runOnAllInstances jobs immediately on a separate scheduler
|
|
1651
|
+
const hasAllInstanceJobs = autoRegisteredJobs.some((j) => j.Job.runOnAllInstances);
|
|
1652
|
+
if (hasAllInstanceJobs) {
|
|
1653
|
+
this.allInstanceScheduler = new ToadScheduler();
|
|
1654
|
+
this.scheduler = this.allInstanceScheduler;
|
|
1655
|
+
await this.registerAutoRegisterableJobs((job) => !!job.runOnAllInstances);
|
|
1656
|
+
this.scheduler = undefined;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const opts = this.leaderElectionConfig;
|
|
1660
|
+
this.leaderElection = new LeaderElection(this.db, opts);
|
|
1661
|
+
|
|
1662
|
+
await this.leaderElection.start(
|
|
1663
|
+
// onBecameLeader
|
|
1664
|
+
async () => {
|
|
1665
|
+
schedulerLog.info("This instance is now the leader - starting scheduled jobs");
|
|
1666
|
+
this.scheduler = new ToadScheduler();
|
|
1667
|
+
await this.registerAutoRegisterableJobs((job) => !job.runOnAllInstances);
|
|
1668
|
+
},
|
|
1669
|
+
// onLostLeadership
|
|
1670
|
+
() => {
|
|
1671
|
+
schedulerLog.info("This instance lost leadership - stopping scheduled jobs");
|
|
1672
|
+
if (this.scheduler) {
|
|
1673
|
+
this.scheduler.stop();
|
|
1674
|
+
this.scheduler = undefined;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
916
1680
|
private getMongoConnectionOptions() {
|
|
917
1681
|
if (!this.dbOpts) {
|
|
918
1682
|
throw new Error("No db configured");
|
|
@@ -921,14 +1685,14 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
921
1685
|
const { version: driverVersion } = require("mongodb/package.json");
|
|
922
1686
|
|
|
923
1687
|
if (driverVersion.startsWith("3")) {
|
|
924
|
-
|
|
1688
|
+
initLog.debug(`Using legacy mongodb connection options as mongo client is version ${driverVersion}`);
|
|
925
1689
|
return {
|
|
926
1690
|
useNewUrlParser: true,
|
|
927
1691
|
useUnifiedTopology: true,
|
|
928
1692
|
};
|
|
929
1693
|
}
|
|
930
1694
|
|
|
931
|
-
|
|
1695
|
+
initLog.debug(`Using modern MongoDB client options (driver version ${driverVersion})`);
|
|
932
1696
|
|
|
933
1697
|
return {
|
|
934
1698
|
serverApi: {
|