@memberjunction/server 6.2.0-edge.4 → 6.2.0-edge.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/README.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/SessionJanitor.d.ts +91 -30
  4. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  5. package/dist/agentSessions/SessionJanitor.js +311 -81
  6. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  7. package/dist/agentSessions/SessionManager.d.ts +5 -1
  8. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  9. package/dist/agentSessions/SessionManager.js +13 -1
  10. package/dist/agentSessions/SessionManager.js.map +1 -1
  11. package/dist/apiRoot.d.ts +32 -0
  12. package/dist/apiRoot.d.ts.map +1 -0
  13. package/dist/apiRoot.js +67 -0
  14. package/dist/apiRoot.js.map +1 -0
  15. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  16. package/dist/auth/AuthProviderCatalogRouter.js +25 -0
  17. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  18. package/dist/auth/LoginScreenEngine.d.ts +22 -0
  19. package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
  20. package/dist/auth/LoginScreenEngine.js +57 -0
  21. package/dist/auth/LoginScreenEngine.js.map +1 -0
  22. package/dist/auth/index.d.ts +1 -0
  23. package/dist/auth/index.d.ts.map +1 -1
  24. package/dist/auth/index.js +1 -0
  25. package/dist/auth/index.js.map +1 -1
  26. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  27. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  28. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  29. package/dist/config.d.ts +136 -22
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +43 -4
  32. package/dist/config.js.map +1 -1
  33. package/dist/coreRoutes.d.ts +29 -0
  34. package/dist/coreRoutes.d.ts.map +1 -0
  35. package/dist/coreRoutes.js +142 -0
  36. package/dist/coreRoutes.js.map +1 -0
  37. package/dist/generated/graphql-schemas/__mj.d.ts +369 -3
  38. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  39. package/dist/generated/graphql-schemas/__mj.js +1750 -7
  40. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  41. package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
  42. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  43. package/dist/generic/CacheInvalidationResolver.js +59 -19
  44. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  45. package/dist/generic/PushStatusResolver.js +1 -1
  46. package/dist/generic/PushStatusResolver.js.map +1 -1
  47. package/dist/generic/ResolverBase.d.ts +103 -44
  48. package/dist/generic/ResolverBase.d.ts.map +1 -1
  49. package/dist/generic/ResolverBase.js +264 -305
  50. package/dist/generic/ResolverBase.js.map +1 -1
  51. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  52. package/dist/generic/RunViewResolver.js +12 -0
  53. package/dist/generic/RunViewResolver.js.map +1 -1
  54. package/dist/globalCors.d.ts +33 -0
  55. package/dist/globalCors.d.ts.map +1 -0
  56. package/dist/globalCors.js +58 -0
  57. package/dist/globalCors.js.map +1 -0
  58. package/dist/index.d.ts +11 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +112 -160
  61. package/dist/index.js.map +1 -1
  62. package/dist/multiTenancy/index.d.ts.map +1 -1
  63. package/dist/multiTenancy/index.js +22 -2
  64. package/dist/multiTenancy/index.js.map +1 -1
  65. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -1
  66. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +2 -1
  67. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -1
  68. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -1
  69. package/dist/realtimeSessions/RealtimeVerifyRouter.js +2 -1
  70. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -1
  71. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  72. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  73. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  74. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  75. package/dist/realtimeSessions/index.d.ts +2 -0
  76. package/dist/realtimeSessions/index.d.ts.map +1 -1
  77. package/dist/realtimeSessions/index.js +2 -0
  78. package/dist/realtimeSessions/index.js.map +1 -1
  79. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
  80. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  81. package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
  82. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  83. package/dist/realtimeSessions/sessionAccess.d.ts +6 -0
  84. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  85. package/dist/realtimeSessions/sessionAccess.js +7 -3
  86. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  87. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  88. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  89. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  90. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  91. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  92. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  93. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  94. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  95. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  96. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  97. package/dist/resolvers/DatasetResolver.js +57 -8
  98. package/dist/resolvers/DatasetResolver.js.map +1 -1
  99. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  100. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  101. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  102. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  103. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +18 -3
  104. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  105. package/dist/resolvers/FetchEntityVectorsResolver.js +100 -35
  106. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  107. package/dist/resolvers/FileResolver.d.ts +82 -11
  108. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  109. package/dist/resolvers/FileResolver.js +279 -101
  110. package/dist/resolvers/FileResolver.js.map +1 -1
  111. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  112. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  113. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  114. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  115. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  116. package/dist/resolvers/GetDataResolver.js +6 -3
  117. package/dist/resolvers/GetDataResolver.js.map +1 -1
  118. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  119. package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
  120. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  121. package/dist/resolvers/QueryResolver.d.ts +2 -0
  122. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  123. package/dist/resolvers/QueryResolver.js +13 -0
  124. package/dist/resolvers/QueryResolver.js.map +1 -1
  125. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +2 -8
  126. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  127. package/dist/resolvers/RealtimeClientSessionResolver.js +10 -26
  128. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  129. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  130. package/dist/resolvers/RunAIAgentResolver.js +35 -6
  131. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  132. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  133. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  134. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  135. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  136. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  137. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  138. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  139. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  140. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  141. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  142. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  143. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  144. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  145. package/dist/resolvers/TestQuerySQLResolver.js +65 -13
  146. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  147. package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
  148. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  149. package/dist/resolvers/TransactionGroupResolver.js +102 -24
  150. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  151. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  152. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  153. package/dist/resolvers/agent-run-data-guard.js +72 -0
  154. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  155. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  156. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  157. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  158. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  159. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  160. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  161. package/dist/rest/ViewOperationsHandler.js +21 -12
  162. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  163. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  164. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  165. package/dist/rest/setupRESTEndpoints.js +6 -0
  166. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  167. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  168. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  169. package/dist/serverExtensionReservedRoots.js +14 -17
  170. package/dist/serverExtensionReservedRoots.js.map +1 -1
  171. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  172. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  173. package/dist/services/MJStorageBlobStore.js +25 -19
  174. package/dist/services/MJStorageBlobStore.js.map +1 -1
  175. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  176. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  177. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  178. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  179. package/dist/services/TaskGraphActionRunner.js +27 -5
  180. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  181. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  182. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  183. package/dist/services/TaskGraphAgentRunner.js +157 -9
  184. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  185. package/dist/services/TaskGraphContinuationDeliverer.d.ts +54 -2
  186. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  187. package/dist/services/TaskGraphContinuationDeliverer.js +598 -15
  188. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  189. package/dist/sharedCache.d.ts.map +1 -1
  190. package/dist/sharedCache.js +3 -0
  191. package/dist/sharedCache.js.map +1 -1
  192. package/package.json +99 -98
  193. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  194. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  195. package/src/__tests__/FetchEntityVectorsResolver.test.ts +163 -15
  196. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  197. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  198. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  199. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  200. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  201. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  202. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  203. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  204. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  205. package/src/__tests__/ResolverBase.clientOldValues.test.ts +209 -6
  206. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  207. package/src/__tests__/RunAIAgentResolver.branch.test.ts +1 -0
  208. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +5 -3
  209. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
  210. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  211. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  212. package/src/__tests__/SessionJanitor.test.ts +365 -10
  213. package/src/__tests__/SessionManager.test.ts +27 -0
  214. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  215. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  216. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +356 -22
  217. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  218. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  219. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  220. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  221. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  222. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  223. package/src/__tests__/apiRootBoot.test.ts +202 -0
  224. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  225. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  226. package/src/__tests__/globalCors.test.ts +217 -0
  227. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  228. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  229. package/src/__tests__/multiTenancy.test.ts +1 -1
  230. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  231. package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
  232. package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
  233. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  234. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -98
  235. package/src/__tests__/sharedCache.test.ts +5 -3
  236. package/src/agentSessions/SessionJanitor.ts +369 -93
  237. package/src/agentSessions/SessionManager.ts +13 -1
  238. package/src/apiRoot.ts +78 -0
  239. package/src/auth/AuthProviderCatalogRouter.ts +26 -0
  240. package/src/auth/LoginScreenEngine.ts +50 -0
  241. package/src/auth/index.ts +1 -0
  242. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  243. package/src/config.ts +46 -5
  244. package/src/coreRoutes.ts +166 -0
  245. package/src/generated/graphql-schemas/__mj.ts +1273 -10
  246. package/src/generic/CacheInvalidationResolver.ts +73 -18
  247. package/src/generic/PushStatusResolver.ts +1 -1
  248. package/src/generic/ResolverBase.ts +304 -325
  249. package/src/generic/RunViewResolver.ts +17 -0
  250. package/src/globalCors.ts +71 -0
  251. package/src/index.ts +116 -174
  252. package/src/multiTenancy/index.ts +24 -2
  253. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +2 -1
  254. package/src/realtimeSessions/RealtimeVerifyRouter.ts +2 -1
  255. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  256. package/src/realtimeSessions/index.ts +2 -0
  257. package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
  258. package/src/realtimeSessions/sessionAccess.ts +7 -3
  259. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  260. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  261. package/src/resolvers/DatasetResolver.ts +70 -10
  262. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  263. package/src/resolvers/FetchEntityVectorsResolver.ts +121 -39
  264. package/src/resolvers/FileResolver.ts +321 -106
  265. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  266. package/src/resolvers/GetDataResolver.ts +6 -3
  267. package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
  268. package/src/resolvers/QueryResolver.ts +11 -0
  269. package/src/resolvers/RealtimeClientSessionResolver.ts +10 -33
  270. package/src/resolvers/RunAIAgentResolver.ts +39 -4
  271. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  272. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  273. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  274. package/src/resolvers/TestQuerySQLResolver.ts +68 -13
  275. package/src/resolvers/TransactionGroupResolver.ts +131 -27
  276. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  277. package/src/resolvers/agent-run-data-guard.ts +89 -0
  278. package/src/rest/RealtimeRecordingHandler.ts +464 -0
  279. package/src/rest/ViewOperationsHandler.ts +29 -13
  280. package/src/rest/setupRESTEndpoints.ts +6 -0
  281. package/src/serverExtensionReservedRoots.ts +14 -17
  282. package/src/services/MJStorageBlobStore.ts +31 -30
  283. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  284. package/src/services/TaskGraphActionRunner.ts +25 -5
  285. package/src/services/TaskGraphAgentRunner.ts +188 -11
  286. package/src/services/TaskGraphContinuationDeliverer.ts +704 -14
  287. package/src/sharedCache.ts +3 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * @fileoverview Per-session timers that assemble a client-direct realtime recording from its shard ledger
3
+ * off the request path.
4
+ *
5
+ * The recording REST routes only record shards and the terminal signal; they never decode. When a route
6
+ * learns a session is ready (every expected shard present) it asks for an assembly now; when shards are
7
+ * missing it asks for one after the grace period, forced to `Partial`. This scheduler turns those requests
8
+ * into at most one pending timer and at most one running assembly per session in this process.
9
+ *
10
+ * Correctness across processes does not depend on it: every run starts with the ledger's atomic
11
+ * `ClaimAssembly`, so of two instances (or two timers) only the claim winner assembles. The per-session
12
+ * guard here only avoids wasted claims. A run that dies with the process leaves the session `Assembling`,
13
+ * and the session janitor's stale re-claim is the backstop for that and for any request dropped here.
14
+ *
15
+ * @module @memberjunction/server/realtimeSessions
16
+ */
17
+ import { DatabaseProviderBase, LogError, LogStatus, UserInfo } from '@memberjunction/core';
18
+ import { BaseSingleton, IShutdownable, ShutdownRegistry } from '@memberjunction/global';
19
+ import {
20
+ AssembleRealtimeRecordingFromLedger, AssembleRealtimeRecordingInput, AssembleRealtimeRecordingOutcome,
21
+ IRealtimeRecordingLedger, SqlRealtimeRecordingLedger,
22
+ } from '@memberjunction/ai-agents';
23
+
24
+ /** A cancellable one-shot timer. */
25
+ export interface RecordingAssemblyTimer {
26
+ Cancel(): void;
27
+ }
28
+
29
+ /** Time source of the scheduler; injectable so tests can drive it. */
30
+ export interface RecordingAssemblyClock {
31
+ /** Milliseconds since the epoch. */
32
+ Now(): number;
33
+ /** Runs `callback` once after `delayMs`. The timer must not keep the process alive. */
34
+ StartTimer(callback: () => void, delayMs: number): RecordingAssemblyTimer;
35
+ }
36
+
37
+ /** The narrow scheduling contract the recording routes depend on. */
38
+ export interface IRecordingAssemblyScheduler {
39
+ /**
40
+ * Requests an assembly of `sessionID` after `delayMs`. `forcePartial` assembles what exists even when
41
+ * shards are missing (the grace period ran out). See {@link RecordingAssemblyScheduler.ScheduleAssembly}.
42
+ */
43
+ ScheduleAssembly(sessionID: string, delayMs: number, forcePartial: boolean): void;
44
+ }
45
+
46
+ /** What {@link RecordingAssemblyScheduler.Start} needs. */
47
+ export interface RecordingAssemblySchedulerOptions {
48
+ /** The server's own provider; assembly is server-owned background work, not a request. */
49
+ Provider: DatabaseProviderBase;
50
+ /** The user assembly runs as (the system user). */
51
+ SystemUser: UserInfo;
52
+ /** Grace before a `Deferred` (incomplete) assembly is retried as forced `Partial`. */
53
+ GraceMs: () => number;
54
+ /** Defaults to {@link SqlRealtimeRecordingLedger}. */
55
+ CreateLedger?: (provider: DatabaseProviderBase, contextUser: UserInfo) => IRealtimeRecordingLedger;
56
+ /** Defaults to {@link AssembleRealtimeRecordingFromLedger}. */
57
+ Assemble?: (input: AssembleRealtimeRecordingInput) => Promise<AssembleRealtimeRecordingOutcome>;
58
+ /** Defaults to `Date.now` and unref'd `setTimeout`. */
59
+ Clock?: RecordingAssemblyClock;
60
+ }
61
+
62
+ interface PendingAssembly {
63
+ DueAt: number;
64
+ ForcePartial: boolean;
65
+ Timer: RecordingAssemblyTimer;
66
+ }
67
+
68
+ const NODE_CLOCK: RecordingAssemblyClock = {
69
+ Now: () => Date.now(),
70
+ StartTimer: (callback, delayMs) => {
71
+ const handle = setTimeout(callback, delayMs);
72
+ handle.unref?.();
73
+ return { Cancel: () => clearTimeout(handle) };
74
+ },
75
+ };
76
+
77
+ /**
78
+ * Process-wide scheduler of recording assemblies (see the file overview). Call {@link Start} once at
79
+ * server startup; requests made before it (or after {@link Shutdown}) are logged and dropped, leaving the
80
+ * session to the janitor.
81
+ */
82
+ export class RecordingAssemblyScheduler extends BaseSingleton<RecordingAssemblyScheduler> implements IShutdownable, IRecordingAssemblyScheduler {
83
+ private _options: Required<RecordingAssemblySchedulerOptions> | null = null;
84
+ private _registered = false;
85
+ /** Pending timers keyed by lowercase session id. */
86
+ private readonly pending = new Map<string, PendingAssembly>();
87
+ /** Lowercase ids of sessions with an assembly running in this process. */
88
+ private readonly running = new Set<string>();
89
+ /** Sessions asked for again while running, with the ForcePartial to re-run with. */
90
+ private readonly rerun = new Map<string, boolean>();
91
+
92
+ protected constructor() {
93
+ super();
94
+ }
95
+
96
+ /** Process-wide singleton accessor. */
97
+ public static get Instance(): RecordingAssemblyScheduler {
98
+ return super.getInstance<RecordingAssemblyScheduler>();
99
+ }
100
+
101
+ /** Identifier surfaced in graceful-shutdown logs. */
102
+ public readonly ShutdownName = 'RecordingAssemblyScheduler';
103
+
104
+ /** Configures the scheduler and enables it; calling again replaces the configuration. */
105
+ public Start(options: RecordingAssemblySchedulerOptions): void {
106
+ this._options = {
107
+ CreateLedger: (provider, contextUser) => new SqlRealtimeRecordingLedger(provider, contextUser),
108
+ Assemble: AssembleRealtimeRecordingFromLedger,
109
+ Clock: NODE_CLOCK,
110
+ ...options,
111
+ };
112
+ if (!this._registered) {
113
+ ShutdownRegistry.Instance.Register(this);
114
+ this._registered = true;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Requests an assembly of `sessionID` after `delayMs`. One pending timer per session: a request
120
+ * replaces the pending one only when it would fire sooner (at the same time, the merged request keeps
121
+ * `forcePartial` if either had it). A timer that fires while the session's previous run is still going
122
+ * is remembered and run right after it, never alongside it.
123
+ *
124
+ * Trade-off: a sooner un-forced request replaces a pending forced one. If that run comes back
125
+ * `Deferred` (still incomplete), the forced `Partial` is re-scheduled a full grace from then, so a
126
+ * forced Partial can land up to ~2× the grace after the first terminal signal. Accepted: the sooner
127
+ * run completes the recording outright whenever the missing shards did arrive.
128
+ */
129
+ public ScheduleAssembly(sessionID: string, delayMs: number, forcePartial: boolean): void {
130
+ const options = this._options;
131
+ if (!options) {
132
+ LogError(`RecordingAssemblyScheduler: not running; dropped the assembly request for session ${sessionID} (the session janitor will assemble it)`);
133
+ return;
134
+ }
135
+ const key = sessionID.toLowerCase();
136
+ const dueAt = options.Clock.Now() + Math.max(0, delayMs);
137
+ const existing = this.pending.get(key);
138
+ if (existing && existing.DueAt < dueAt) {
139
+ return;
140
+ }
141
+ if (existing && existing.DueAt === dueAt) {
142
+ existing.ForcePartial = existing.ForcePartial || forcePartial;
143
+ return;
144
+ }
145
+ existing?.Timer.Cancel();
146
+ const timer = options.Clock.StartTimer(() => this.onTimer(key, sessionID), dueAt - options.Clock.Now());
147
+ this.pending.set(key, { DueAt: dueAt, ForcePartial: forcePartial, Timer: timer });
148
+ }
149
+
150
+ /**
151
+ * Cancels every pending timer and stops accepting requests until the next {@link Start}. A running
152
+ * assembly is not awaited: if the process exits under it, the session stays `Assembling` and the
153
+ * janitor's stale re-claim finishes it. Idempotent.
154
+ */
155
+ public Shutdown(): void {
156
+ for (const entry of this.pending.values()) {
157
+ entry.Timer.Cancel();
158
+ }
159
+ this.pending.clear();
160
+ this.rerun.clear();
161
+ this._options = null;
162
+ }
163
+
164
+ private onTimer(key: string, sessionID: string): void {
165
+ const entry = this.pending.get(key);
166
+ if (!entry) {
167
+ return;
168
+ }
169
+ this.pending.delete(key);
170
+ if (this.running.has(key)) {
171
+ this.rerun.set(key, (this.rerun.get(key) ?? false) || entry.ForcePartial);
172
+ return;
173
+ }
174
+ void this.runUntilSettled(key, sessionID, entry.ForcePartial);
175
+ }
176
+
177
+ /** Runs the session's assembly, then any re-run requested meanwhile. Never rejects. */
178
+ private async runUntilSettled(key: string, sessionID: string, forcePartial: boolean): Promise<void> {
179
+ this.running.add(key);
180
+ try {
181
+ let next: boolean | undefined = forcePartial;
182
+ while (next !== undefined) {
183
+ await this.runOnce(sessionID, next);
184
+ next = this.rerun.get(key);
185
+ this.rerun.delete(key);
186
+ }
187
+ } finally {
188
+ this.running.delete(key);
189
+ }
190
+ }
191
+
192
+ /** One claim + assembly. Logs every failure with the session id; never throws. */
193
+ private async runOnce(sessionID: string, forcePartial: boolean): Promise<void> {
194
+ const options = this._options;
195
+ if (!options) {
196
+ LogStatus(`RecordingAssemblyScheduler: shut down before session ${sessionID} could assemble; the session janitor will assemble it`);
197
+ return;
198
+ }
199
+ try {
200
+ const ledger = options.CreateLedger(options.Provider, options.SystemUser);
201
+ const version = await ledger.ClaimAssembly(sessionID, null);
202
+ if (version === null) {
203
+ LogStatus(`RecordingAssemblyScheduler: session ${sessionID} not claimable (assembled, assembling elsewhere, or failed); skipping`);
204
+ return;
205
+ }
206
+ const outcome = await options.Assemble({
207
+ SessionID: sessionID, Version: version, Ledger: ledger,
208
+ ContextUser: options.SystemUser, Provider: options.Provider, ForcePartial: forcePartial,
209
+ });
210
+ this.handleOutcome(sessionID, version, outcome, options);
211
+ } catch (error) {
212
+ LogError(`RecordingAssemblyScheduler: assembly of session ${sessionID} failed: ${error instanceof Error ? error.message : String(error)}`);
213
+ }
214
+ }
215
+
216
+ private handleOutcome(
217
+ sessionID: string,
218
+ version: number,
219
+ outcome: AssembleRealtimeRecordingOutcome,
220
+ options: Required<RecordingAssemblySchedulerOptions>,
221
+ ): void {
222
+ if (outcome === 'Deferred') {
223
+ // Incomplete and not forced: give missing shards the grace period, then assemble what exists.
224
+ LogStatus(`RecordingAssemblyScheduler: session ${sessionID} (version ${version}) is missing shards; assembling as Partial after the grace period`);
225
+ this.ScheduleAssembly(sessionID, options.GraceMs(), true);
226
+ return;
227
+ }
228
+ if (outcome === 'Retry') {
229
+ LogError(`RecordingAssemblyScheduler: assembly of session ${sessionID} (version ${version}) hit a transient error; the session janitor re-claims it later`);
230
+ return;
231
+ }
232
+ LogStatus(`RecordingAssemblyScheduler: session ${sessionID} (version ${version}) assembly finished: ${outcome}`);
233
+ }
234
+ }
@@ -14,3 +14,5 @@ export * from './sessionConfigStamps.js';
14
14
  export * from './SessionVerificationPortsBase.js';
15
15
  export * from './RealtimeSessionVerificationService.js';
16
16
  export * from './RealtimeVerifyRouter.js';
17
+ export * from './ownedRealtimeSession.js';
18
+ export * from './RecordingAssemblyScheduler.js';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview The ownership gate for client-direct realtime session writes: the GraphQL mutations of
3
+ * `RealtimeClientSessionResolver` and the recording REST routes both load the session through
4
+ * {@link LoadOwnedRealtimeSession}, so "may this caller write to this session?" is decided in one place.
5
+ *
6
+ * @module @memberjunction/server/realtimeSessions
7
+ */
8
+ import { IMetadataProvider, LogError, UserInfo } from '@memberjunction/core';
9
+ import { UUIDsEqual } from '@memberjunction/global';
10
+ import type { MJAIAgentSessionEntity } from '@memberjunction/core-entities';
11
+
12
+ const SESSION_ENTITY = 'MJ: AI Agent Sessions';
13
+
14
+ /** The session does not exist, or the caller cannot read it. */
15
+ export class RealtimeSessionNotFoundError extends Error {
16
+ constructor(public readonly AgentSessionID: string) {
17
+ super(`Realtime session ${AgentSessionID} not found`);
18
+ this.name = 'RealtimeSessionNotFoundError';
19
+ }
20
+ }
21
+
22
+ /** The session exists but belongs to another user. */
23
+ export class RealtimeSessionOwnershipError extends Error {
24
+ constructor(public readonly AgentSessionID: string) {
25
+ super('Not authorized: you do not own this realtime session');
26
+ this.name = 'RealtimeSessionOwnershipError';
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Loads a session under the caller's identity and enforces ownership (`UserID === contextUser.ID`). It
32
+ * deliberately does NOT reject `Closed` sessions: the final channel-state flush and the recording terminal
33
+ * signal legitimately arrive after the session closed. A scoped-anonymous widget guest owns the session it
34
+ * minted, so the same check covers it; loading as the caller also applies the entity's row-level security.
35
+ *
36
+ * @throws RealtimeSessionNotFoundError when the row does not load
37
+ * @throws RealtimeSessionOwnershipError when the caller is not the owner (logged with both user ids)
38
+ */
39
+ export async function LoadOwnedRealtimeSession(
40
+ agentSessionId: string,
41
+ contextUser: UserInfo,
42
+ provider: IMetadataProvider,
43
+ ): Promise<MJAIAgentSessionEntity> {
44
+ const session = await provider.GetEntityObject<MJAIAgentSessionEntity>(SESSION_ENTITY, contextUser);
45
+ if (!(await session.Load(agentSessionId))) {
46
+ throw new RealtimeSessionNotFoundError(agentSessionId);
47
+ }
48
+ if (!UUIDsEqual(session.UserID, contextUser.ID)) {
49
+ LogError(
50
+ `LoadOwnedRealtimeSession: ownership check failed — user ${contextUser.ID} attempted to operate on session ${agentSessionId} owned by ${session.UserID}`,
51
+ );
52
+ throw new RealtimeSessionOwnershipError(agentSessionId);
53
+ }
54
+ return session;
55
+ }
@@ -115,11 +115,15 @@ export async function LoadSessionForAccess(
115
115
  if (!(await session.Load(agentSessionId))) {
116
116
  return null;
117
117
  }
118
- return { Session: session, ConversationExternalID: await loadConversationExternalID(session, contextUser, provider) };
118
+ return { Session: session, ConversationExternalID: await LoadSessionConversationExternalID(session, contextUser, provider) };
119
119
  }
120
120
 
121
- /** Reads the session conversation's `ExternalID`; null when there is no conversation or it cannot be read. */
122
- async function loadConversationExternalID(
121
+ /**
122
+ * Reads an already-loaded session's conversation `ExternalID` (the anonymous scope id) under `contextUser`;
123
+ * null when there is no conversation or it cannot be read (logged). For callers that loaded the session
124
+ * through another gate and only need the scope half of {@link AuthorizeSessionAccess}'s subject.
125
+ */
126
+ export async function LoadSessionConversationExternalID(
123
127
  session: MJAIAgentSessionEntity,
124
128
  contextUser: UserInfo,
125
129
  provider: IMetadataProvider,
@@ -46,7 +46,11 @@ export class ArtifactFileResolver extends ResolverBase {
46
46
  return this.buildDownloadUrl(artifactVersion.FileID, user, p);
47
47
  }
48
48
 
49
- /** Load the File + its storage account/provider and generate a signed URL. */
49
+ /**
50
+ * Load the File (as the caller) and generate a signed URL — after the storage gate: the caller must be able to read
51
+ * the account the file's provider resolves to and every `MJ: Files` row that tracks its object
52
+ * (`FileStorageEngine.ResolveFileObject`), so an artifact cannot sign an object in an account the caller is refused.
53
+ */
50
54
  private async buildDownloadUrl(
51
55
  fileId: string,
52
56
  user: ReturnType<ResolverBase['GetUserFromPayload']>,
@@ -58,14 +62,11 @@ export class ArtifactFileResolver extends ResolverBase {
58
62
  throw new Error(`File record ${fileId} not found`);
59
63
  }
60
64
 
61
- // Find the storage account for this file's provider using cached metadata
62
- await FileStorageEngine.Instance.Config(false, user!);
63
- const matchingAccounts = FileStorageEngine.Instance.GetAccountsByProviderID(fileEntity.ProviderID);
64
- if (matchingAccounts.length === 0) {
65
+ // Resolve the storage account for this file's provider, gated for the caller (throws on refusal)
66
+ const resolved = await FileStorageEngine.Instance.ResolveFileObject(fileEntity, user!, 'Read', provider);
67
+ if (!resolved) {
65
68
  throw new Error(`No FileStorageAccount found for ProviderID ${fileEntity.ProviderID}. Cannot generate download URL.`);
66
69
  }
67
-
68
- const driver = await FileStorageEngine.Instance.GetDriver(matchingAccounts[0].ID, user!);
69
- return driver.CreatePreAuthDownloadUrl(fileEntity.ProviderKey ?? fileEntity.Name);
70
+ return resolved.Driver.CreatePreAuthDownloadUrl(resolved.ObjectKey);
70
71
  }
71
72
  }
@@ -7,7 +7,7 @@ import type { AppContext } from '../types.js';
7
7
  import { GetReadOnlyProvider } from '../util.js';
8
8
 
9
9
  /** The run columns the tail reads. */
10
- type TailRunRow = Pick<MJAIAgentRunEntity, 'ID' | 'Status' | 'Result'>;
10
+ type TailRunRow = Pick<MJAIAgentRunEntity, 'ID' | 'Status' | 'FinalPayload' | 'Result'>;
11
11
 
12
12
  /** The step columns an event carries. The large data columns are never read. */
13
13
  type TailStepRow = Pick<
@@ -15,7 +15,7 @@ type TailStepRow = Pick<
15
15
  'StepNumber' | 'StepType' | 'StepName' | 'Status' | 'Success' | 'ErrorMessage' | 'StartedAt' | 'CompletedAt'
16
16
  >;
17
17
 
18
- const RUN_FIELDS: (keyof TailRunRow)[] = ['ID', 'Status', 'Result'];
18
+ const RUN_FIELDS: (keyof TailRunRow)[] = ['ID', 'Status', 'FinalPayload', 'Result'];
19
19
  const STEP_FIELDS: (keyof TailStepRow)[] = [
20
20
  'StepNumber', 'StepType', 'StepName', 'Status', 'Success', 'ErrorMessage', 'StartedAt', 'CompletedAt',
21
21
  ];
@@ -94,10 +94,14 @@ export class ConversationRunEventsOutput {
94
94
  /**
95
95
  * The run's answer, returned only once the run is terminal.
96
96
  *
97
- * Sourced from `AIAgentRun.Result`, falling back to the conversation detail's message because
98
- * `Result` is agent-dependent and null on many successful runs. Still nullable: a caller must
99
- * decide a message is finished from {@link IsInFlight} / {@link DetailStatus}, never from the
100
- * presence of this field, or it will wait forever on an agent that writes neither.
97
+ * Sourced from `AIAgentRun.FinalPayload` (`Result` on older rows), then the conversation detail's
98
+ * message, because the run payload is agent-dependent and null on many successful runs. Still
99
+ * nullable: a caller must decide a message is finished from {@link IsInFlight} /
100
+ * {@link DetailStatus}, never from the presence of this field, or it will wait forever on an
101
+ * agent that writes neither.
102
+ *
103
+ * A run that ended without completing (for example `Cancelled`) returns whatever `FinalPayload`
104
+ * it had saved, which can be partial, so a caller should check {@link RunStatus} first.
101
105
  */
102
106
  @Field(() => String, { nullable: true })
103
107
  FinalPayload?: string;
@@ -206,11 +210,11 @@ export class ConversationRunEventsResolver {
206
210
  // Only meaningful once terminal; sending it mid-run would hand the client a
207
211
  // partial payload it could mistake for the final answer.
208
212
  //
209
- // `AIAgentRun.Result` is agent-dependent — it is null on a large share of
210
- // genuinely successful runs — so the conversation detail's own message is the
211
- // fallback. That is the text the chat renders, so it is the answer a caller
212
- // waiting on this field actually wants.
213
- FinalPayload: isInFlight ? undefined : (run.Result ?? detail.Message ?? undefined),
213
+ // `AIAgentRun.FinalPayload` (`Result` on older rows) is agent-dependent — it is
214
+ // null on a large share of genuinely successful runs — so the conversation detail's
215
+ // own message is the fallback. That is the text the chat renders, so it is the
216
+ // answer a caller waiting on this field actually wants.
217
+ FinalPayload: isInFlight ? undefined : (run.FinalPayload ?? run.Result ?? detail.Message ?? undefined),
214
218
  DetailStatus: detail.Status ?? undefined,
215
219
  };
216
220
  } catch (e) {
@@ -1,6 +1,6 @@
1
1
  import { Arg, Ctx, Field, InputType, Int, ObjectType, Query, Resolver } from 'type-graphql';
2
- import { AppContext } from '../types.js';
3
- import { LogError, Metadata } from '@memberjunction/core';
2
+ import { AppContext, UserPayload } from '../types.js';
3
+ import { DatabaseProviderBase, DatasetItemFilterType, DatasetItemResultType, LogError, Metadata, UserInfo } from '@memberjunction/core';
4
4
  import { GetReadOnlyProvider } from '../util.js';
5
5
  import { ResolverBase } from '../generic/ResolverBase.js';
6
6
 
@@ -34,9 +34,64 @@ export class DatasetItemFilterTypeGQL {
34
34
  Filter: string;
35
35
  }
36
36
 
37
+ /**
38
+ * Request handling shared by the dataset queries. Every read runs as the session's user, so the
39
+ * provider applies that user's entity read permission and row-level security per item, and every
40
+ * client ItemFilter passes the same AST screen as a RunView ExtraFilter before a provider sees it.
41
+ */
42
+ export abstract class DatasetResolverBase extends ResolverBase {
43
+ /** The session's user. A dataset read never runs without one: the provider applies no read checks then. */
44
+ protected RequireDatasetUser(userPayload: UserPayload): UserInfo {
45
+ const user = this.GetUserFromPayload(userPayload);
46
+ if (!user) {
47
+ throw new Error('Dataset request refused: the session resolves to no user');
48
+ }
49
+ return user;
50
+ }
51
+
52
+ /**
53
+ * Screens every client ItemFilter: one statement, read-only, and subqueries only over entity base
54
+ * views the user can read. Throws on the first refusal. Returns the filters without null entries.
55
+ */
56
+ protected ScreenItemFilters(
57
+ itemFilters: DatasetItemFilterTypeGQL[] | undefined,
58
+ provider: DatabaseProviderBase,
59
+ user: UserInfo,
60
+ ): DatasetItemFilterType[] | undefined {
61
+ if (!itemFilters) return undefined;
62
+ itemFilters.forEach((itemFilter, index) => {
63
+ if (itemFilter) {
64
+ this.assertClientClauseUsesEntityBaseViews(itemFilter.Filter, `ItemFilters[${index}].Filter`, provider, user);
65
+ }
66
+ });
67
+ return itemFilters.filter((itemFilter) => itemFilter != null);
68
+ }
69
+
70
+ /**
71
+ * Applies the API encryption policy to each item's rows, as every other GraphQL read path does:
72
+ * an encrypted field the entity does not allow decrypted over the API leaves as ciphertext or the
73
+ * sentinel. Works on copies, because the rows can be the server cache's own frozen objects.
74
+ */
75
+ protected async ProtectEncryptedFields(
76
+ items: DatasetItemResultType[],
77
+ provider: DatabaseProviderBase,
78
+ user: UserInfo,
79
+ ): Promise<DatasetItemResultType[]> {
80
+ return Promise.all(
81
+ items.map(async (item) => {
82
+ const entity = provider.EntityByName(item.EntityName);
83
+ if (!item.Results?.length || !entity?.EncryptedFields.some((field) => !field.AllowDecryptInAPI)) {
84
+ return item;
85
+ }
86
+ const rows = item.Results.map((row: Record<string, unknown>) => ({ ...row }));
87
+ return { ...item, Results: await this.ArrayFilterEncryptedFieldsForAPI(item.EntityName, rows, user, provider, 'Name') };
88
+ }),
89
+ );
90
+ }
91
+ }
37
92
 
38
93
  @Resolver(DatasetResultType)
39
- export class DatasetResolverExtended extends ResolverBase {
94
+ export class DatasetResolverExtended extends DatasetResolverBase {
40
95
  @Query(() => DatasetResultType)
41
96
  async GetDatasetByName(
42
97
  @Arg('DatasetName', () => String) DatasetName: string,
@@ -45,10 +100,12 @@ export class DatasetResolverExtended extends ResolverBase {
45
100
  ) {
46
101
  // Check API key scope authorization for dataset read
47
102
  await this.CheckAPIKeyScopeAuthorization('dataset:read', DatasetName, userPayload);
103
+ const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
104
+ const user = this.RequireDatasetUser(userPayload);
105
+ const itemFilters = this.ScreenItemFilters(ItemFilters, md, user);
48
106
 
49
107
  try {
50
- const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
51
- const result = await md.GetDatasetByName(DatasetName, ItemFilters);
108
+ const result = await md.GetDatasetByName(DatasetName, itemFilters, user);
52
109
  if (result) {
53
110
  return {
54
111
  DatasetID: result.DatasetID,
@@ -56,7 +113,7 @@ export class DatasetResolverExtended extends ResolverBase {
56
113
  Success: result.Success,
57
114
  Status: result.Status,
58
115
  LatestUpdateDate: result.LatestUpdateDate,
59
- Results: JSON.stringify(result.Results),
116
+ Results: JSON.stringify(await this.ProtectEncryptedFields(result.Results, md, user)),
60
117
  };
61
118
  } else {
62
119
  throw new Error('Error retrieving Dataset: ' + DatasetName);
@@ -90,7 +147,7 @@ export class DatasetStatusResultType {
90
147
  }
91
148
 
92
149
  @Resolver(DatasetStatusResultType)
93
- export class DatasetStatusResolver extends ResolverBase {
150
+ export class DatasetStatusResolver extends DatasetResolverBase {
94
151
  @Query(() => DatasetStatusResultType)
95
152
  async GetDatasetStatusByName(
96
153
  @Arg('DatasetName', () => String) DatasetName: string,
@@ -99,10 +156,12 @@ export class DatasetStatusResolver extends ResolverBase {
99
156
  ) {
100
157
  // Check API key scope authorization for dataset read
101
158
  await this.CheckAPIKeyScopeAuthorization('dataset:read', DatasetName, userPayload);
159
+ const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
160
+ const user = this.RequireDatasetUser(userPayload);
161
+ const itemFilters = this.ScreenItemFilters(ItemFilters, md, user);
102
162
 
103
163
  try {
104
- const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
105
- const result = await md.GetDatasetStatusByName(DatasetName, ItemFilters);
164
+ const result = await md.GetDatasetStatusByName(DatasetName, itemFilters, user);
106
165
  if (result) {
107
166
  return {
108
167
  DatasetID: result.DatasetID,
@@ -131,12 +190,13 @@ export class DatasetStatusResolver extends ResolverBase {
131
190
  @Ctx() { providers, userPayload }: AppContext,
132
191
  ): Promise<DatasetStatusResultType[]> {
133
192
  const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
193
+ const user = this.RequireDatasetUser(userPayload);
134
194
  const results: DatasetStatusResultType[] = [];
135
195
 
136
196
  // Execute all status checks in parallel
137
197
  const statusPromises = DatasetNames.map(async (name) => {
138
198
  await this.CheckAPIKeyScopeAuthorization('dataset:read', name, userPayload);
139
- return md.GetDatasetStatusByName(name);
199
+ return md.GetDatasetStatusByName(name, undefined, user);
140
200
  });
141
201
 
142
202
  const statuses = await Promise.all(statusPromises);
@@ -38,7 +38,8 @@ export class EntityRecordNameResult {
38
38
  *
39
39
  * Entity-level read permission is checked here ({@link ResolverBase.CheckUserReadPermissions}).
40
40
  * Field- and row-level security are applied by the provider's lookup, for the acting user passed
41
- * to it: when any field the name is built from is withheld, it answers with no name.
41
+ * to it: when any field the name is built from is withheld, it answers with no name, and it reads
42
+ * only rows the user's read row filter allows. Without an acting user there is no lookup.
42
43
  *
43
44
  * A withheld name answers `Success: false` with the same status as a record that does not exist.
44
45
  * Callers already treat "no name" as "show the primary key", and the identical answer keeps this
@@ -85,7 +86,7 @@ export class EntityRecordNameResolver extends ResolverBase {
85
86
  if (e) {
86
87
  const contextUser = userPayload ? this.GetUserFromPayload(userPayload) : undefined;
87
88
  // The acting user is what field- and row-level security are applied for.
88
- const recordName = await md.GetEntityRecordName(e.Name, pk, contextUser);
89
+ const recordName = contextUser ? await md.GetEntityRecordName(e.Name, pk, contextUser) : '';
89
90
  if (recordName) return { Success: true, Status: 'OK', CompositeKey: pk, RecordName: recordName, EntityName };
90
91
  else
91
92
  return {