@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.
Files changed (280) hide show
  1. package/CHANGELOG.md +1051 -0
  2. package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
  3. package/SIMPLE_AST_FEASIBILITY.md +570 -0
  4. package/bin/flink.ts +13 -2
  5. package/cli/build.ts +24 -44
  6. package/cli/clean.ts +13 -25
  7. package/cli/cli-utils.ts +190 -17
  8. package/cli/dev.ts +252 -0
  9. package/cli/loadEnvFiles.ts +116 -0
  10. package/cli/run.ts +45 -62
  11. package/dist/bin/flink.js +61 -2
  12. package/dist/cli/build.js +20 -25
  13. package/dist/cli/clean.js +12 -10
  14. package/dist/cli/cli-utils.d.ts +34 -3
  15. package/dist/cli/cli-utils.js +193 -12
  16. package/dist/cli/dev.d.ts +2 -0
  17. package/dist/cli/dev.js +279 -0
  18. package/dist/cli/loadEnvFiles.d.ts +30 -0
  19. package/dist/cli/loadEnvFiles.js +113 -0
  20. package/dist/cli/run.js +47 -46
  21. package/dist/src/DependencyTracker.d.ts +44 -0
  22. package/dist/src/DependencyTracker.js +239 -0
  23. package/dist/src/FlinkApp.d.ts +163 -10
  24. package/dist/src/FlinkApp.js +847 -184
  25. package/dist/src/FlinkContext.d.ts +41 -0
  26. package/dist/src/FlinkErrors.d.ts +19 -6
  27. package/dist/src/FlinkErrors.js +36 -42
  28. package/dist/src/FlinkHttpHandler.d.ts +219 -26
  29. package/dist/src/FlinkHttpHandler.js +37 -1
  30. package/dist/src/FlinkJob.d.ts +10 -0
  31. package/dist/src/FlinkLog.d.ts +82 -18
  32. package/dist/src/FlinkLog.js +165 -13
  33. package/dist/src/FlinkLogFactory.d.ts +288 -0
  34. package/dist/src/FlinkLogFactory.js +619 -0
  35. package/dist/src/FlinkRepo.d.ts +10 -2
  36. package/dist/src/FlinkRepo.js +11 -1
  37. package/dist/src/FlinkRequestContext.d.ts +63 -0
  38. package/dist/src/FlinkRequestContext.js +74 -0
  39. package/dist/src/FlinkResponse.d.ts +6 -0
  40. package/dist/src/FlinkService.d.ts +38 -0
  41. package/dist/src/FlinkService.js +46 -0
  42. package/dist/src/LeaderElection.d.ts +45 -0
  43. package/dist/src/LeaderElection.js +269 -0
  44. package/dist/src/SchemaCache.d.ts +84 -0
  45. package/dist/src/SchemaCache.js +289 -0
  46. package/dist/src/TypeScriptCompiler.d.ts +161 -51
  47. package/dist/src/TypeScriptCompiler.js +1253 -617
  48. package/dist/src/TypeScriptUtils.js +4 -0
  49. package/dist/src/ai/AgentRunner.d.ts +39 -0
  50. package/dist/src/ai/AgentRunner.js +760 -0
  51. package/dist/src/ai/ConversationAgent.d.ts +279 -0
  52. package/dist/src/ai/ConversationAgent.js +404 -0
  53. package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
  54. package/dist/src/ai/ConversationFlinkAgent.js +404 -0
  55. package/dist/src/ai/FlinkAgent.d.ts +690 -0
  56. package/dist/src/ai/FlinkAgent.js +729 -0
  57. package/dist/src/ai/FlinkTool.d.ts +135 -0
  58. package/dist/src/ai/FlinkTool.js +2 -0
  59. package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
  60. package/dist/src/ai/InMemoryConversationAgent.js +209 -0
  61. package/dist/src/ai/LLMAdapter.d.ts +148 -0
  62. package/dist/src/ai/LLMAdapter.js +2 -0
  63. package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
  64. package/dist/src/ai/PersistentFlinkAgent.js +403 -0
  65. package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
  66. package/dist/src/ai/SubAgentExecutor.js +223 -0
  67. package/dist/src/ai/ToolExecutor.d.ts +64 -0
  68. package/dist/src/ai/ToolExecutor.js +497 -0
  69. package/dist/src/ai/agentInstructions.d.ts +68 -0
  70. package/dist/src/ai/agentInstructions.js +286 -0
  71. package/dist/src/ai/index.d.ts +8 -0
  72. package/dist/src/ai/index.js +26 -0
  73. package/dist/src/ai/instructionFileLoader.d.ts +44 -0
  74. package/dist/src/ai/instructionFileLoader.js +179 -0
  75. package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
  76. package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
  77. package/dist/src/handlers/StreamWriterFactory.js +83 -0
  78. package/dist/src/index.d.ts +14 -0
  79. package/dist/src/index.js +17 -0
  80. package/dist/src/loadPluginSchemas.d.ts +45 -0
  81. package/dist/src/loadPluginSchemas.js +143 -0
  82. package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
  83. package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
  84. package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
  85. package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
  86. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
  87. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
  88. package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
  89. package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
  90. package/dist/src/schema-extraction/index.d.ts +2 -0
  91. package/dist/src/schema-extraction/index.js +20 -0
  92. package/dist/src/schema-extraction/types.d.ts +31 -0
  93. package/dist/src/schema-extraction/types.js +2 -0
  94. package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
  95. package/dist/src/utils/loadFlinkConfig.js +77 -0
  96. package/dist/src/utils.d.ts +30 -0
  97. package/dist/src/utils.js +52 -0
  98. package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
  99. package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
  100. package/dist/src/workers/WorkerPool.d.ts +60 -0
  101. package/dist/src/workers/WorkerPool.js +306 -0
  102. package/examples/logging-hierarchical-example.ts +125 -0
  103. package/package.json +29 -4
  104. package/readme.md +499 -0
  105. package/spec/AgentDescendantDetection.spec.ts +335 -0
  106. package/spec/AgentDuplicateDetection.spec.ts +112 -0
  107. package/spec/AgentObserver.spec.ts +266 -0
  108. package/spec/AgentRunner.spec.ts +1062 -0
  109. package/spec/AsyncLocalStorageContext.spec.ts +223 -0
  110. package/spec/ConversationHooks.spec.ts +257 -0
  111. package/spec/FlinkAgent.spec.ts +681 -0
  112. package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
  113. package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
  114. package/spec/FlinkApp.onError.spec.ts +1 -2
  115. package/spec/FlinkApp.query.spec.ts +107 -0
  116. package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
  117. package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
  118. package/spec/FlinkApp.validationMode.spec.ts +155 -0
  119. package/spec/FlinkJob.spec.ts +171 -0
  120. package/spec/FlinkLogFactory.spec.ts +337 -0
  121. package/spec/FlinkRepo.spec.ts +1 -1
  122. package/spec/LeaderElection.spec.ts +174 -0
  123. package/spec/StreamingIntegration.spec.ts +139 -0
  124. package/spec/ToolExecutor.spec.ts +465 -0
  125. package/spec/TypeScriptCompiler.spec.ts +1 -1
  126. package/spec/TypeScriptSourceParser.spec.ts +1215 -0
  127. package/spec/TypeScriptTokenizer.spec.ts +366 -0
  128. package/spec/ai/ContextCompaction.spec.ts +405 -0
  129. package/spec/ai/ConversationAgent.spec.ts +520 -0
  130. package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
  131. package/spec/ai/agentInstructions.spec.ts +358 -0
  132. package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
  133. package/spec/fixtures/agent-instructions/simple.md +3 -0
  134. package/spec/fixtures/agent-instructions/template.md +18 -0
  135. package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
  136. package/spec/mock-project/dist/.tsbuildinfo +1 -0
  137. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
  138. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
  139. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
  140. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
  141. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
  142. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
  143. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
  144. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
  145. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -0
  146. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
  147. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
  148. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +57 -0
  149. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
  150. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
  151. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
  152. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
  153. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
  154. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
  155. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
  156. package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -0
  157. package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
  158. package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
  159. package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
  160. package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
  161. package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
  162. package/spec/mock-project/dist/src/FlinkApp.js +1000 -0
  163. package/spec/mock-project/dist/src/FlinkContext.js +2 -0
  164. package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
  165. package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
  166. package/spec/mock-project/dist/src/FlinkJob.js +2 -0
  167. package/spec/mock-project/dist/src/FlinkLog.js +119 -0
  168. package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -0
  169. package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
  170. package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
  171. package/spec/mock-project/dist/src/FlinkRequestContext.js +74 -0
  172. package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
  173. package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
  174. package/spec/mock-project/dist/src/ai/AgentRunner.js +632 -0
  175. package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
  176. package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
  177. package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
  178. package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
  179. package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
  180. package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
  181. package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
  182. package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
  183. package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -0
  184. package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
  185. package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
  186. package/spec/mock-project/dist/src/handlers/GetCar.js +26 -52
  187. package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
  188. package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
  189. package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
  190. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
  191. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
  192. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
  193. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
  194. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
  195. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
  196. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
  197. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
  198. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
  199. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
  200. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
  201. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
  202. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
  203. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
  204. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
  205. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
  206. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
  207. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
  208. package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
  209. package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
  210. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
  211. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
  212. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
  213. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
  214. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
  215. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
  216. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
  217. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
  218. package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
  219. package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
  220. package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
  221. package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
  222. package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
  223. package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
  224. package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
  225. package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
  226. package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
  227. package/spec/mock-project/dist/src/index.js +52 -76
  228. package/spec/mock-project/dist/src/index.js.map +1 -0
  229. package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
  230. package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
  231. package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
  232. package/spec/mock-project/dist/src/schemas/Car.js +3 -1
  233. package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
  234. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
  235. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
  236. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
  237. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
  238. package/spec/mock-project/dist/src/utils.js +290 -0
  239. package/spec/mock-project/tsconfig.json +6 -1
  240. package/spec/schema-generation-nested-objects.spec.ts +97 -0
  241. package/spec/testHelpers.ts +49 -0
  242. package/spec/utils.caseConversion.spec.ts +78 -0
  243. package/spec/utils.spec.ts +13 -13
  244. package/src/DependencyTracker.ts +166 -0
  245. package/src/FlinkApp.ts +919 -155
  246. package/src/FlinkContext.ts +43 -0
  247. package/src/FlinkErrors.ts +32 -12
  248. package/src/FlinkHttpHandler.ts +246 -28
  249. package/src/FlinkJob.ts +11 -0
  250. package/src/FlinkLog.ts +119 -12
  251. package/src/FlinkLogFactory.ts +699 -0
  252. package/src/FlinkRepo.ts +10 -3
  253. package/src/FlinkRequestContext.ts +95 -0
  254. package/src/FlinkResponse.ts +6 -0
  255. package/src/FlinkService.ts +49 -0
  256. package/src/LeaderElection.ts +203 -0
  257. package/src/SchemaCache.ts +232 -0
  258. package/src/TypeScriptCompiler.ts +1347 -610
  259. package/src/TypeScriptUtils.ts +5 -0
  260. package/src/ai/AgentRunner.ts +646 -0
  261. package/src/ai/ConversationAgent.ts +413 -0
  262. package/src/ai/FlinkAgent.ts +1069 -0
  263. package/src/ai/FlinkTool.ts +165 -0
  264. package/src/ai/InMemoryConversationAgent.ts +149 -0
  265. package/src/ai/LLMAdapter.ts +126 -0
  266. package/src/ai/ToolExecutor.ts +485 -0
  267. package/src/ai/agentInstructions.ts +245 -0
  268. package/src/ai/index.ts +8 -0
  269. package/src/ai/instructionFileLoader.ts +156 -0
  270. package/src/auth/FlinkAuthPlugin.ts +2 -1
  271. package/src/handlers/StreamWriterFactory.ts +84 -0
  272. package/src/index.ts +14 -0
  273. package/src/loadPluginSchemas.ts +141 -0
  274. package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
  275. package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
  276. package/src/schema-extraction/index.ts +2 -0
  277. package/src/schema-extraction/types.ts +34 -0
  278. package/src/utils/loadFlinkConfig.ts +89 -0
  279. package/src/utils.ts +52 -0
  280. 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 updateOne(id: string | ObjectId, model: PartialModel<Model>): Promise<Model | null> {
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
- private objectIdToString<T>(doc: T & { _id?: any }) {
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
+ }
@@ -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
+ }