@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/FlinkRepo.ts
CHANGED
|
@@ -16,7 +16,7 @@ export abstract class FlinkRepo<C extends FlinkContext, Model extends Document>
|
|
|
16
16
|
this._ctx = ctx as C;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
get ctx() {
|
|
19
|
+
get ctx(): C {
|
|
20
20
|
if (!this._ctx) throw new Error("Missing FlinkContext");
|
|
21
21
|
return this._ctx;
|
|
22
22
|
}
|
|
@@ -51,7 +51,7 @@ export abstract class FlinkRepo<C extends FlinkContext, Model extends Document>
|
|
|
51
51
|
return { ...model, _id: result.insertedId.toString() };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
async
|
|
54
|
+
async updateById(id: string | ObjectId, model: PartialModel<Model>): Promise<Model | null> {
|
|
55
55
|
const oid = this.buildId(id);
|
|
56
56
|
|
|
57
57
|
const { _id, ...modelWithoutId } = model;
|
|
@@ -66,6 +66,13 @@ export abstract class FlinkRepo<C extends FlinkContext, Model extends Document>
|
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Use `updateById` instead. This will be removed in a future major version.
|
|
71
|
+
*/
|
|
72
|
+
async updateOne(id: string | ObjectId, model: PartialModel<Model>): Promise<Model | null> {
|
|
73
|
+
return this.updateById(id, model);
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
async updateMany<U = PartialModel<Model>>(query: any, model: U): Promise<number> {
|
|
70
77
|
const { _id, ...modelWithoutId } = model as any;
|
|
71
78
|
|
|
@@ -103,7 +110,7 @@ export abstract class FlinkRepo<C extends FlinkContext, Model extends Document>
|
|
|
103
110
|
return oid;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
|
|
113
|
+
protected objectIdToString<T>(doc: T & { _id?: any }) {
|
|
107
114
|
if (doc && doc._id) {
|
|
108
115
|
doc._id = doc._id.toString();
|
|
109
116
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Request-scoped context available throughout handler → agent → tool execution
|
|
5
|
+
*/
|
|
6
|
+
export interface RequestContext {
|
|
7
|
+
/** Unique request identifier (from req.reqId) */
|
|
8
|
+
reqId: string;
|
|
9
|
+
|
|
10
|
+
/** Authenticated user object (from req.user) */
|
|
11
|
+
user?: any;
|
|
12
|
+
|
|
13
|
+
/** Resolved user permissions from auth plugin (from req.userPermissions) */
|
|
14
|
+
userPermissions?: string[];
|
|
15
|
+
|
|
16
|
+
/** HTTP method (GET, POST, etc.) */
|
|
17
|
+
method?: string;
|
|
18
|
+
|
|
19
|
+
/** Request path */
|
|
20
|
+
path?: string;
|
|
21
|
+
|
|
22
|
+
/** Request start timestamp */
|
|
23
|
+
timestamp: number;
|
|
24
|
+
|
|
25
|
+
/** Whether this is a streaming request (SSE/NDJSON) */
|
|
26
|
+
isStreaming?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* AsyncLocalStorage instance for request-scoped context
|
|
31
|
+
* Available throughout the entire request lifecycle including async operations
|
|
32
|
+
*/
|
|
33
|
+
export const requestContext = new AsyncLocalStorage<RequestContext>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the current request context
|
|
37
|
+
* Returns undefined if called outside of a request context
|
|
38
|
+
*/
|
|
39
|
+
export function getRequestContext(): RequestContext | undefined {
|
|
40
|
+
return requestContext.getStore();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the authenticated user from request context
|
|
45
|
+
* Returns undefined if no user is authenticated or outside request context
|
|
46
|
+
*/
|
|
47
|
+
export function getRequestUser<T = any>(): T | undefined {
|
|
48
|
+
return requestContext.getStore()?.user;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get user permissions from request context
|
|
53
|
+
* Returns empty array if no permissions available
|
|
54
|
+
*/
|
|
55
|
+
export function getRequestPermissions(): string[] {
|
|
56
|
+
return requestContext.getStore()?.userPermissions || [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get request ID from context
|
|
61
|
+
* Returns undefined if called outside of request context
|
|
62
|
+
*/
|
|
63
|
+
export function getReqId(): string | undefined {
|
|
64
|
+
return requestContext.getStore()?.reqId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if current user has a specific permission
|
|
69
|
+
* @param permission - Permission string to check
|
|
70
|
+
* @returns true if user has the permission, false otherwise
|
|
71
|
+
*/
|
|
72
|
+
export function hasPermission(permission: string): boolean {
|
|
73
|
+
const permissions = getRequestPermissions();
|
|
74
|
+
return permissions.includes(permission);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if current user has all of the specified permissions (AND logic)
|
|
79
|
+
* @param requiredPermissions - Array of permission strings (all required)
|
|
80
|
+
* @returns true if user has all permissions, false otherwise
|
|
81
|
+
*/
|
|
82
|
+
export function hasAllPermissions(requiredPermissions: string[]): boolean {
|
|
83
|
+
const permissions = getRequestPermissions();
|
|
84
|
+
return requiredPermissions.every(p => permissions.includes(p));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if current user has any of the specified permissions (OR logic)
|
|
89
|
+
* @param requiredPermissions - Array of permission strings (any required)
|
|
90
|
+
* @returns true if user has at least one permission, false otherwise
|
|
91
|
+
*/
|
|
92
|
+
export function hasAnyPermission(requiredPermissions: string[]): boolean {
|
|
93
|
+
const permissions = getRequestPermissions();
|
|
94
|
+
return requiredPermissions.some(p => permissions.includes(p));
|
|
95
|
+
}
|
package/src/FlinkResponse.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface FlinkResponse<T = any> {
|
|
|
34
34
|
title: string;
|
|
35
35
|
detail?: string;
|
|
36
36
|
code?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Optional structured payload with additional error context.
|
|
39
|
+
* Must be JSON-serializable; non-serializable values are stripped
|
|
40
|
+
* with a warning before sending the response.
|
|
41
|
+
*/
|
|
42
|
+
meta?: unknown;
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
/**
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { FlinkContext } from "./FlinkContext";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for Flink services - optional business logic layer.
|
|
5
|
+
*
|
|
6
|
+
* Services provide a place for shared business logic that can be used by
|
|
7
|
+
* handlers, jobs, agents, and other services via `ctx.services`.
|
|
8
|
+
*
|
|
9
|
+
* Context (`this.ctx`) is not available in the constructor - use `onInit()`
|
|
10
|
+
* for any setup that requires context.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* class CarService extends FlinkService<AppCtx> {
|
|
15
|
+
* async onInit() {
|
|
16
|
+
* // Called after all services are instantiated and ctx is fully wired
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* async getWithOwner(carId: string) {
|
|
20
|
+
* const car = await this.ctx.repos.carRepo.getById(carId);
|
|
21
|
+
* if (!car) throw notFound("Car not found");
|
|
22
|
+
* return car;
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export abstract class FlinkService<C extends FlinkContext> {
|
|
28
|
+
private _ctx?: C;
|
|
29
|
+
|
|
30
|
+
set ctx(ctx: FlinkContext) {
|
|
31
|
+
this._ctx = ctx as C;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get ctx(): C {
|
|
35
|
+
if (!this._ctx) {
|
|
36
|
+
throw new Error("FlinkService: ctx is not available in constructor. Use onInit() for setup logic.");
|
|
37
|
+
}
|
|
38
|
+
return this._ctx;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Optional async initialization hook called after all services are
|
|
43
|
+
* instantiated and ctx is fully wired (repos, plugins, agents, services all available).
|
|
44
|
+
*
|
|
45
|
+
* All service onInit() methods run in parallel via Promise.all.
|
|
46
|
+
* Do not depend on another service's onInit() having completed.
|
|
47
|
+
*/
|
|
48
|
+
onInit?(): Promise<void>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Collection, Db } from "mongodb";
|
|
2
|
+
import { v4 } from "uuid";
|
|
3
|
+
import { FlinkLogFactory } from "./FlinkLogFactory";
|
|
4
|
+
|
|
5
|
+
const log = FlinkLogFactory.createLogger("flink.scheduler");
|
|
6
|
+
|
|
7
|
+
export interface LeaderElectionOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Duration in milliseconds before a leader's lease expires.
|
|
10
|
+
* If the leader fails to heartbeat within this time, another instance can take over.
|
|
11
|
+
* @default 15000
|
|
12
|
+
*/
|
|
13
|
+
leaseDurationMs?: number;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Interval in milliseconds between heartbeats sent by the leader.
|
|
17
|
+
* Should be significantly less than leaseDurationMs (typically 1/3).
|
|
18
|
+
* @default 5000
|
|
19
|
+
*/
|
|
20
|
+
heartbeatIntervalMs?: number;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Name of the MongoDB collection used for leader election.
|
|
24
|
+
* @default "_flink_leader"
|
|
25
|
+
*/
|
|
26
|
+
collectionName?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface LeaderRecord {
|
|
30
|
+
_id: string;
|
|
31
|
+
instanceId: string;
|
|
32
|
+
lastHeartbeat: Date;
|
|
33
|
+
claimedAt: Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const LOCK_NAME = "job-scheduler";
|
|
37
|
+
|
|
38
|
+
export class LeaderElection {
|
|
39
|
+
private instanceId = v4();
|
|
40
|
+
private _isLeader = false;
|
|
41
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
42
|
+
private collection: Collection<LeaderRecord>;
|
|
43
|
+
private leaseDurationMs: number;
|
|
44
|
+
private heartbeatIntervalMs: number;
|
|
45
|
+
private onBecameLeader?: () => void | Promise<void>;
|
|
46
|
+
private onLostLeadership?: () => void | Promise<void>;
|
|
47
|
+
private stopped = false;
|
|
48
|
+
private transitioning = false;
|
|
49
|
+
|
|
50
|
+
constructor(db: Db, opts?: LeaderElectionOptions) {
|
|
51
|
+
const collectionName = opts?.collectionName || "_flink_leader";
|
|
52
|
+
this.leaseDurationMs = opts?.leaseDurationMs || 15000;
|
|
53
|
+
this.heartbeatIntervalMs = opts?.heartbeatIntervalMs || 5000;
|
|
54
|
+
this.collection = db.collection<LeaderRecord>(collectionName);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get isLeader() {
|
|
58
|
+
return this._isLeader;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Start the leader election process.
|
|
63
|
+
* @param onBecameLeader Called when this instance becomes the leader
|
|
64
|
+
* @param onLostLeadership Called when this instance loses leadership
|
|
65
|
+
*/
|
|
66
|
+
async start(onBecameLeader: () => void | Promise<void>, onLostLeadership: () => void | Promise<void>) {
|
|
67
|
+
this.onBecameLeader = onBecameLeader;
|
|
68
|
+
this.onLostLeadership = onLostLeadership;
|
|
69
|
+
this.stopped = false;
|
|
70
|
+
|
|
71
|
+
// Ensure TTL index exists for cleanup
|
|
72
|
+
const ttlSeconds = Math.ceil((this.leaseDurationMs * 2) / 1000);
|
|
73
|
+
try {
|
|
74
|
+
await this.collection.createIndex({ lastHeartbeat: 1 }, { expireAfterSeconds: ttlSeconds });
|
|
75
|
+
} catch (err: any) {
|
|
76
|
+
if (err.codeName === "IndexOptionsConflict" || err.code === 85) {
|
|
77
|
+
log.debug("TTL index options changed, recreating index");
|
|
78
|
+
await this.collection.dropIndex("lastHeartbeat_1");
|
|
79
|
+
await this.collection.createIndex({ lastHeartbeat: 1 }, { expireAfterSeconds: ttlSeconds });
|
|
80
|
+
} else {
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
log.info(`Leader election started (instance: ${this.instanceId.substring(0, 8)})`);
|
|
86
|
+
|
|
87
|
+
// Run first election attempt immediately
|
|
88
|
+
await this.tryClaimLeadership();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Stop the leader election and release leadership if held.
|
|
93
|
+
*/
|
|
94
|
+
async stop() {
|
|
95
|
+
this.stopped = true;
|
|
96
|
+
|
|
97
|
+
if (this.timer) {
|
|
98
|
+
clearTimeout(this.timer);
|
|
99
|
+
this.timer = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (this._isLeader) {
|
|
103
|
+
try {
|
|
104
|
+
await this.collection.deleteOne({
|
|
105
|
+
_id: LOCK_NAME as any,
|
|
106
|
+
instanceId: this.instanceId,
|
|
107
|
+
});
|
|
108
|
+
log.info("Leadership released on shutdown");
|
|
109
|
+
} catch (err) {
|
|
110
|
+
log.error(`Failed to release leadership on shutdown: ${err}`);
|
|
111
|
+
}
|
|
112
|
+
this._isLeader = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async tryClaimLeadership() {
|
|
117
|
+
if (this.stopped || this.transitioning) return;
|
|
118
|
+
|
|
119
|
+
const now = new Date();
|
|
120
|
+
const leaseExpiry = new Date(now.getTime() - this.leaseDurationMs);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await this.collection.findOneAndUpdate(
|
|
124
|
+
{
|
|
125
|
+
_id: LOCK_NAME as any,
|
|
126
|
+
$or: [
|
|
127
|
+
{ instanceId: this.instanceId },
|
|
128
|
+
{ lastHeartbeat: { $lt: leaseExpiry } },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
$set: {
|
|
133
|
+
instanceId: this.instanceId,
|
|
134
|
+
lastHeartbeat: now,
|
|
135
|
+
},
|
|
136
|
+
$setOnInsert: {
|
|
137
|
+
claimedAt: now,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{ upsert: true, returnDocument: "after" }
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const gotLock = result && (result as any).instanceId === this.instanceId;
|
|
144
|
+
|
|
145
|
+
if (gotLock && !this._isLeader) {
|
|
146
|
+
log.info(`This instance became the leader (instance: ${this.instanceId.substring(0, 8)})`);
|
|
147
|
+
this._isLeader = true;
|
|
148
|
+
this.transitioning = true;
|
|
149
|
+
try {
|
|
150
|
+
await this.onBecameLeader?.();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
log.error(`Error in onBecameLeader callback: ${err}`);
|
|
153
|
+
} finally {
|
|
154
|
+
this.transitioning = false;
|
|
155
|
+
}
|
|
156
|
+
} else if (!gotLock && this._isLeader) {
|
|
157
|
+
log.warn(`This instance lost leadership (instance: ${this.instanceId.substring(0, 8)})`);
|
|
158
|
+
this._isLeader = false;
|
|
159
|
+
this.transitioning = true;
|
|
160
|
+
try {
|
|
161
|
+
await this.onLostLeadership?.();
|
|
162
|
+
} catch (err) {
|
|
163
|
+
log.error(`Error in onLostLeadership callback: ${err}`);
|
|
164
|
+
} finally {
|
|
165
|
+
this.transitioning = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
if (err.code === 11000) {
|
|
170
|
+
// Duplicate key - another instance claimed first
|
|
171
|
+
if (this._isLeader) {
|
|
172
|
+
log.warn(`This instance lost leadership (instance: ${this.instanceId.substring(0, 8)})`);
|
|
173
|
+
this._isLeader = false;
|
|
174
|
+
try {
|
|
175
|
+
await this.onLostLeadership?.();
|
|
176
|
+
} catch (cbErr) {
|
|
177
|
+
log.error(`Error in onLostLeadership callback: ${cbErr}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
log.error(`Leader election error: ${err}`);
|
|
182
|
+
// On error, assume we lost leadership to be safe
|
|
183
|
+
if (this._isLeader) {
|
|
184
|
+
this._isLeader = false;
|
|
185
|
+
try {
|
|
186
|
+
await this.onLostLeadership?.();
|
|
187
|
+
} catch (cbErr) {
|
|
188
|
+
log.error(`Error in onLostLeadership callback: ${cbErr}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Schedule next attempt
|
|
195
|
+
if (!this.stopped) {
|
|
196
|
+
const nextInterval = this._isLeader
|
|
197
|
+
? this.heartbeatIntervalMs
|
|
198
|
+
: this.heartbeatIntervalMs * 2;
|
|
199
|
+
|
|
200
|
+
this.timer = setTimeout(() => this.tryClaimLeadership(), nextInterval);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import type { JSONSchema7 } from 'json-schema';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cache entry for a single schema
|
|
8
|
+
*/
|
|
9
|
+
export interface SchemaCacheEntry {
|
|
10
|
+
schemaName: string;
|
|
11
|
+
schemaFile: string;
|
|
12
|
+
contentHash: string;
|
|
13
|
+
dependencyHashes: Record<string, string>;
|
|
14
|
+
jsonSchema: JSONSchema7;
|
|
15
|
+
generatedAt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Cache file format
|
|
20
|
+
*/
|
|
21
|
+
export interface SchemaCacheFile {
|
|
22
|
+
version: string;
|
|
23
|
+
tsVersion: string;
|
|
24
|
+
generated: string;
|
|
25
|
+
entries: Record<string, SchemaCacheEntry>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Manages persistent caching of generated JSON schemas
|
|
30
|
+
*/
|
|
31
|
+
export class SchemaCache {
|
|
32
|
+
private static CACHE_VERSION = '1.0.0';
|
|
33
|
+
private cacheFile: string;
|
|
34
|
+
private entries: Map<string, SchemaCacheEntry> = new Map();
|
|
35
|
+
private dirty = false;
|
|
36
|
+
|
|
37
|
+
constructor(private projectRoot: string) {
|
|
38
|
+
this.cacheFile = path.join(projectRoot, '.flink', 'schema-cache.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compute SHA-256 hash of file content
|
|
43
|
+
*/
|
|
44
|
+
static async hashFile(filePath: string): Promise<string> {
|
|
45
|
+
try {
|
|
46
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
47
|
+
return SchemaCache.hashContent(content);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// File doesn't exist or can't be read
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compute SHA-256 hash of string content
|
|
56
|
+
*/
|
|
57
|
+
static hashContent(content: string): string {
|
|
58
|
+
return createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load cache from disk
|
|
63
|
+
*/
|
|
64
|
+
async load(): Promise<void> {
|
|
65
|
+
try {
|
|
66
|
+
// Ensure .flink directory exists
|
|
67
|
+
const flinkDir = path.dirname(this.cacheFile);
|
|
68
|
+
await fs.mkdir(flinkDir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
// Read cache file
|
|
71
|
+
const data = await fs.readFile(this.cacheFile, 'utf-8');
|
|
72
|
+
const cache: SchemaCacheFile = JSON.parse(data);
|
|
73
|
+
|
|
74
|
+
// Validate cache version
|
|
75
|
+
if (cache.version !== SchemaCache.CACHE_VERSION) {
|
|
76
|
+
console.log('[SchemaCache] Cache version mismatch, invalidating cache');
|
|
77
|
+
this.entries.clear();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate TypeScript version (major.minor)
|
|
82
|
+
const currentTsVersion = this.getTsVersion();
|
|
83
|
+
if (cache.tsVersion !== currentTsVersion) {
|
|
84
|
+
console.log('[SchemaCache] TypeScript version changed, invalidating cache');
|
|
85
|
+
this.entries.clear();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Load entries
|
|
90
|
+
this.entries = new Map(Object.entries(cache.entries));
|
|
91
|
+
console.log(`[SchemaCache] Loaded ${this.entries.size} cached schemas`);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
94
|
+
console.warn('[SchemaCache] Failed to load cache:', error);
|
|
95
|
+
}
|
|
96
|
+
this.entries.clear();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Save cache to disk
|
|
102
|
+
*/
|
|
103
|
+
async save(): Promise<void> {
|
|
104
|
+
if (!this.dirty) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const cache: SchemaCacheFile = {
|
|
110
|
+
version: SchemaCache.CACHE_VERSION,
|
|
111
|
+
tsVersion: this.getTsVersion(),
|
|
112
|
+
generated: new Date().toISOString(),
|
|
113
|
+
entries: Object.fromEntries(this.entries),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const flinkDir = path.dirname(this.cacheFile);
|
|
117
|
+
await fs.mkdir(flinkDir, { recursive: true });
|
|
118
|
+
await fs.writeFile(this.cacheFile, JSON.stringify(cache, null, 2), 'utf-8');
|
|
119
|
+
|
|
120
|
+
console.log(`[SchemaCache] Saved ${this.entries.size} schemas to cache`);
|
|
121
|
+
this.dirty = false;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.warn('[SchemaCache] Failed to save cache:', error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get cached entry for schema
|
|
129
|
+
*/
|
|
130
|
+
get(schemaName: string): SchemaCacheEntry | undefined {
|
|
131
|
+
return this.entries.get(schemaName);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Add or update cache entry
|
|
136
|
+
*/
|
|
137
|
+
set(entry: SchemaCacheEntry): void {
|
|
138
|
+
this.entries.set(entry.schemaName, entry);
|
|
139
|
+
this.dirty = true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Remove cache entry
|
|
144
|
+
*/
|
|
145
|
+
delete(schemaName: string): void {
|
|
146
|
+
if (this.entries.delete(schemaName)) {
|
|
147
|
+
this.dirty = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Clear all cache entries
|
|
153
|
+
*/
|
|
154
|
+
clear(): void {
|
|
155
|
+
this.entries.clear();
|
|
156
|
+
this.dirty = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if schema needs regeneration
|
|
161
|
+
* Returns { needed: boolean, reason?: string }
|
|
162
|
+
*/
|
|
163
|
+
async needsRegeneration(
|
|
164
|
+
schemaName: string,
|
|
165
|
+
schemaFile: string,
|
|
166
|
+
dependencies: Map<string, string>
|
|
167
|
+
): Promise<{ needed: boolean; reason?: string }> {
|
|
168
|
+
const cached = this.entries.get(schemaName);
|
|
169
|
+
|
|
170
|
+
// No cache entry
|
|
171
|
+
if (!cached) {
|
|
172
|
+
return { needed: true, reason: 'no cache entry' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Schema file changed
|
|
176
|
+
const currentHash = await SchemaCache.hashFile(schemaFile);
|
|
177
|
+
if (cached.contentHash !== currentHash) {
|
|
178
|
+
return { needed: true, reason: 'schema file changed' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Dependency set changed
|
|
182
|
+
const cachedDeps = Object.keys(cached.dependencyHashes).sort();
|
|
183
|
+
const currentDeps = Array.from(dependencies.keys()).sort();
|
|
184
|
+
if (JSON.stringify(cachedDeps) !== JSON.stringify(currentDeps)) {
|
|
185
|
+
return { needed: true, reason: 'dependency set changed' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check each dependency hash
|
|
189
|
+
for (const [depPath, depHash] of Array.from(dependencies.entries())) {
|
|
190
|
+
const cachedHash = cached.dependencyHashes[depPath];
|
|
191
|
+
if (cachedHash !== depHash) {
|
|
192
|
+
return { needed: true, reason: `dependency changed: ${depPath}` };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Cache is valid
|
|
197
|
+
return { needed: false };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get TypeScript version (major.minor)
|
|
202
|
+
*/
|
|
203
|
+
private getTsVersion(): string {
|
|
204
|
+
try {
|
|
205
|
+
const tsPackageJson = require('typescript/package.json');
|
|
206
|
+
const [major, minor] = tsPackageJson.version.split('.');
|
|
207
|
+
return `${major}.${minor}`;
|
|
208
|
+
} catch {
|
|
209
|
+
return 'unknown';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get cache statistics
|
|
215
|
+
*/
|
|
216
|
+
getStats(): {
|
|
217
|
+
totalEntries: number;
|
|
218
|
+
oldestEntry?: string;
|
|
219
|
+
newestEntry?: string;
|
|
220
|
+
} {
|
|
221
|
+
if (this.entries.size === 0) {
|
|
222
|
+
return { totalEntries: 0 };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const timestamps = Array.from(this.entries.values()).map(e => new Date(e.generatedAt).getTime());
|
|
226
|
+
return {
|
|
227
|
+
totalEntries: this.entries.size,
|
|
228
|
+
oldestEntry: new Date(Math.min(...timestamps)).toISOString(),
|
|
229
|
+
newestEntry: new Date(Math.max(...timestamps)).toISOString(),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|