@memberjunction/server 6.2.0-edge.3 → 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 (543) hide show
  1. package/README.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
  4. package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
  5. package/dist/agentSessions/BridgeHostInstance.js +30 -0
  6. package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
  7. package/dist/agentSessions/HostInstance.d.ts +75 -11
  8. package/dist/agentSessions/HostInstance.d.ts.map +1 -1
  9. package/dist/agentSessions/HostInstance.js +99 -28
  10. package/dist/agentSessions/HostInstance.js.map +1 -1
  11. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
  12. package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
  13. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
  14. package/dist/agentSessions/SessionJanitor.d.ts +236 -27
  15. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  16. package/dist/agentSessions/SessionJanitor.js +682 -54
  17. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  18. package/dist/agentSessions/SessionManager.d.ts +48 -3
  19. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  20. package/dist/agentSessions/SessionManager.js +112 -9
  21. package/dist/agentSessions/SessionManager.js.map +1 -1
  22. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  23. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  24. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  25. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  26. package/dist/agentSessions/index.d.ts +2 -0
  27. package/dist/agentSessions/index.d.ts.map +1 -1
  28. package/dist/agentSessions/index.js +10 -3
  29. package/dist/agentSessions/index.js.map +1 -1
  30. package/dist/apiRoot.d.ts +32 -0
  31. package/dist/apiRoot.d.ts.map +1 -0
  32. package/dist/apiRoot.js +67 -0
  33. package/dist/apiRoot.js.map +1 -0
  34. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  35. package/dist/auth/AuthProviderCatalogRouter.js +25 -0
  36. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  37. package/dist/auth/LoginScreenEngine.d.ts +22 -0
  38. package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
  39. package/dist/auth/LoginScreenEngine.js +57 -0
  40. package/dist/auth/LoginScreenEngine.js.map +1 -0
  41. package/dist/auth/actionAuthorization.d.ts +40 -0
  42. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  43. package/dist/auth/actionAuthorization.js +94 -0
  44. package/dist/auth/actionAuthorization.js.map +1 -0
  45. package/dist/auth/index.d.ts +1 -0
  46. package/dist/auth/index.d.ts.map +1 -1
  47. package/dist/auth/index.js +4 -10
  48. package/dist/auth/index.js.map +1 -1
  49. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  50. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  51. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  52. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  53. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  54. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  55. package/dist/auth/newUsers.d.ts +10 -0
  56. package/dist/auth/newUsers.d.ts.map +1 -1
  57. package/dist/auth/newUsers.js +80 -11
  58. package/dist/auth/newUsers.js.map +1 -1
  59. package/dist/config.d.ts +2514 -22
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +166 -4
  62. package/dist/config.js.map +1 -1
  63. package/dist/context.d.ts +8 -0
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +14 -11
  66. package/dist/context.js.map +1 -1
  67. package/dist/coreRoutes.d.ts +29 -0
  68. package/dist/coreRoutes.d.ts.map +1 -0
  69. package/dist/coreRoutes.js +142 -0
  70. package/dist/coreRoutes.js.map +1 -0
  71. package/dist/generated/graphql-schemas/__mj.d.ts +504 -3
  72. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  73. package/dist/generated/graphql-schemas/__mj.js +2456 -7
  74. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  75. package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
  76. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  77. package/dist/generic/CacheInvalidationResolver.js +59 -19
  78. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  79. package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
  80. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  81. package/dist/generic/PushStatusResolver.js +1 -1
  82. package/dist/generic/PushStatusResolver.js.map +1 -1
  83. package/dist/generic/ResolverBase.d.ts +123 -48
  84. package/dist/generic/ResolverBase.d.ts.map +1 -1
  85. package/dist/generic/ResolverBase.js +293 -305
  86. package/dist/generic/ResolverBase.js.map +1 -1
  87. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  88. package/dist/generic/RunViewResolver.js +16 -0
  89. package/dist/generic/RunViewResolver.js.map +1 -1
  90. package/dist/globalCors.d.ts +33 -0
  91. package/dist/globalCors.d.ts.map +1 -0
  92. package/dist/globalCors.js +58 -0
  93. package/dist/globalCors.js.map +1 -0
  94. package/dist/index.d.ts +17 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +219 -278
  97. package/dist/index.js.map +1 -1
  98. package/dist/isolatedProvider.d.ts +20 -0
  99. package/dist/isolatedProvider.d.ts.map +1 -0
  100. package/dist/isolatedProvider.js +22 -0
  101. package/dist/isolatedProvider.js.map +1 -0
  102. package/dist/multiTenancy/index.d.ts.map +1 -1
  103. package/dist/multiTenancy/index.js +22 -2
  104. package/dist/multiTenancy/index.js.map +1 -1
  105. package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
  106. package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
  107. package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
  108. package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
  109. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
  110. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
  111. package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
  112. package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
  113. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
  114. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
  115. package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
  116. package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
  117. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
  118. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
  119. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +139 -0
  120. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
  121. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
  122. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
  123. package/dist/realtimeSessions/RealtimeVerifyRouter.js +99 -0
  124. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
  125. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  126. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  127. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  128. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  129. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
  130. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
  131. package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
  132. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
  133. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
  134. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
  135. package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
  136. package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
  137. package/dist/realtimeSessions/index.d.ts +19 -0
  138. package/dist/realtimeSessions/index.d.ts.map +1 -0
  139. package/dist/realtimeSessions/index.js +19 -0
  140. package/dist/realtimeSessions/index.js.map +1 -0
  141. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
  142. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  143. package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
  144. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  145. package/dist/realtimeSessions/sessionAccess.d.ts +92 -0
  146. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
  147. package/dist/realtimeSessions/sessionAccess.js +94 -0
  148. package/dist/realtimeSessions/sessionAccess.js.map +1 -0
  149. package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
  150. package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
  151. package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
  152. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
  153. package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
  154. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
  155. package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
  156. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
  157. package/dist/realtimeSessions/verificationCore.d.ts +243 -0
  158. package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
  159. package/dist/realtimeSessions/verificationCore.js +356 -0
  160. package/dist/realtimeSessions/verificationCore.js.map +1 -0
  161. package/dist/realtimeSessions/verificationPages.d.ts +66 -0
  162. package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
  163. package/dist/realtimeSessions/verificationPages.js +110 -0
  164. package/dist/realtimeSessions/verificationPages.js.map +1 -0
  165. package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
  166. package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
  167. package/dist/realtimeSessions/verificationPolicy.js +249 -0
  168. package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
  169. package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
  170. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
  171. package/dist/realtimeSessions/verificationWorkflow.js +435 -0
  172. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
  173. package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
  174. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  175. package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
  176. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  177. package/dist/resolvers/ActionResolver.d.ts +31 -5
  178. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  179. package/dist/resolvers/ActionResolver.js +75 -14
  180. package/dist/resolvers/ActionResolver.js.map +1 -1
  181. package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
  182. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  183. package/dist/resolvers/AdhocQueryResolver.js +21 -5
  184. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  185. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  186. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  187. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  188. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  189. package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
  190. package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
  191. package/dist/resolvers/CacheStatsResolver.js +113 -2
  192. package/dist/resolvers/CacheStatsResolver.js.map +1 -1
  193. package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
  194. package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
  195. package/dist/resolvers/ClientToolRequestResolver.js +28 -10
  196. package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
  197. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  198. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  199. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  200. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  201. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  202. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  203. package/dist/resolvers/DatasetResolver.js +57 -8
  204. package/dist/resolvers/DatasetResolver.js.map +1 -1
  205. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  206. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  207. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  208. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  209. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +27 -1
  210. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  211. package/dist/resolvers/FetchEntityVectorsResolver.js +148 -2
  212. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  213. package/dist/resolvers/FileResolver.d.ts +82 -11
  214. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  215. package/dist/resolvers/FileResolver.js +279 -101
  216. package/dist/resolvers/FileResolver.js.map +1 -1
  217. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  218. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  219. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  220. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  221. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  222. package/dist/resolvers/GetDataResolver.js +6 -3
  223. package/dist/resolvers/GetDataResolver.js.map +1 -1
  224. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
  225. package/dist/resolvers/HumanHandoffResolver.js +7 -1
  226. package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
  227. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  228. package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
  229. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  230. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  231. package/dist/resolvers/MCPResolver.js +9 -4
  232. package/dist/resolvers/MCPResolver.js.map +1 -1
  233. package/dist/resolvers/QueryResolver.d.ts +2 -0
  234. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  235. package/dist/resolvers/QueryResolver.js +13 -0
  236. package/dist/resolvers/QueryResolver.js.map +1 -1
  237. package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
  238. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  239. package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
  240. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  241. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +157 -33
  242. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  243. package/dist/resolvers/RealtimeClientSessionResolver.js +404 -116
  244. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  245. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
  246. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
  247. package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
  248. package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
  249. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
  250. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
  251. package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
  252. package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
  253. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  254. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  255. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  256. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  257. package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
  258. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  259. package/dist/resolvers/RunAIAgentResolver.js +100 -14
  260. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  261. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  262. package/dist/resolvers/RunTemplateResolver.js +12 -11
  263. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  264. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  265. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  266. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  267. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  268. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  269. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  270. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  271. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  272. package/dist/resolvers/SyncDataResolver.js +6 -2
  273. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  274. package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
  275. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  276. package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
  277. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  278. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  279. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  280. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  281. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  282. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  283. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  284. package/dist/resolvers/TestQuerySQLResolver.js +65 -13
  285. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  286. package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
  287. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  288. package/dist/resolvers/TransactionGroupResolver.js +102 -24
  289. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  290. package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
  291. package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
  292. package/dist/resolvers/UserAvatarResolver.js +124 -0
  293. package/dist/resolvers/UserAvatarResolver.js.map +1 -0
  294. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  295. package/dist/resolvers/UserResolver.js +2 -17
  296. package/dist/resolvers/UserResolver.js.map +1 -1
  297. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  298. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  299. package/dist/resolvers/agent-run-data-guard.js +72 -0
  300. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  301. package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
  302. package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
  303. package/dist/resolvers/agentVisionConsentAudit.js +63 -0
  304. package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
  305. package/dist/resolvers/avatarInputValidation.d.ts +40 -0
  306. package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
  307. package/dist/resolvers/avatarInputValidation.js +97 -0
  308. package/dist/resolvers/avatarInputValidation.js.map +1 -0
  309. package/dist/resolvers/currentUserRoles.d.ts +29 -0
  310. package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
  311. package/dist/resolvers/currentUserRoles.js +30 -0
  312. package/dist/resolvers/currentUserRoles.js.map +1 -1
  313. package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
  314. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
  315. package/dist/resolvers/meetings/meetingMutations.js +21 -0
  316. package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
  317. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  318. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  319. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  320. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  321. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  322. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  323. package/dist/rest/ViewOperationsHandler.js +21 -12
  324. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  325. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  326. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  327. package/dist/rest/setupRESTEndpoints.js +6 -0
  328. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  329. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  330. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  331. package/dist/serverExtensionReservedRoots.js +14 -17
  332. package/dist/serverExtensionReservedRoots.js.map +1 -1
  333. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  334. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  335. package/dist/services/MJStorageBlobStore.js +25 -19
  336. package/dist/services/MJStorageBlobStore.js.map +1 -1
  337. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  338. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  339. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  340. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  341. package/dist/services/TaskGraphActionRunner.js +27 -5
  342. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  343. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  344. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  345. package/dist/services/TaskGraphAgentRunner.js +157 -9
  346. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  347. package/dist/services/TaskGraphContinuationDeliverer.d.ts +57 -4
  348. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  349. package/dist/services/TaskGraphContinuationDeliverer.js +607 -18
  350. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  351. package/dist/sharedCache.d.ts +105 -0
  352. package/dist/sharedCache.d.ts.map +1 -0
  353. package/dist/sharedCache.js +279 -0
  354. package/dist/sharedCache.js.map +1 -0
  355. package/dist/types.d.ts +6 -0
  356. package/dist/types.d.ts.map +1 -1
  357. package/dist/types.js.map +1 -1
  358. package/package.json +99 -96
  359. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  360. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  361. package/src/__tests__/BridgeHostInstance.test.ts +232 -0
  362. package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
  363. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  364. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  365. package/src/__tests__/FetchEntityVectorsResolver.test.ts +433 -0
  366. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  367. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  368. package/src/__tests__/HostInstance.test.ts +84 -0
  369. package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
  370. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  371. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  372. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  373. package/src/__tests__/MeetingResolver.test.ts +75 -1
  374. package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
  375. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  376. package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
  377. package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
  378. package/src/__tests__/RealtimeRelay.test.ts +876 -0
  379. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  380. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  381. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  382. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  383. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  384. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  385. package/src/__tests__/ResolverBase.clientOldValues.test.ts +212 -6
  386. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  387. package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
  388. package/src/__tests__/RunAIAgentResolver.branch.test.ts +170 -0
  389. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +6 -4
  390. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
  391. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
  392. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  393. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
  394. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  395. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  396. package/src/__tests__/SessionJanitor.test.ts +1203 -21
  397. package/src/__tests__/SessionManager.test.ts +240 -17
  398. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  399. package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
  400. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  401. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  402. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +403 -22
  403. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  404. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  405. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  406. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  407. package/src/__tests__/UserAvatarResolver.test.ts +255 -0
  408. package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
  409. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  410. package/src/__tests__/actionAuthorization.test.ts +130 -0
  411. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  412. package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
  413. package/src/__tests__/apiRootBoot.test.ts +202 -0
  414. package/src/__tests__/avatarInputValidation.test.ts +160 -0
  415. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  416. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  417. package/src/__tests__/globalCors.test.ts +217 -0
  418. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  419. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  420. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  421. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  422. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  423. package/src/__tests__/multiTenancy.test.ts +1 -1
  424. package/src/__tests__/newUsers.template.test.ts +151 -0
  425. package/src/__tests__/newUsers.test.ts +74 -5
  426. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
  427. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  428. package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
  429. package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
  430. package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
  431. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
  432. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
  433. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
  434. package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
  435. package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
  436. package/src/__tests__/realtimeSessions/helpers.ts +4 -0
  437. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
  438. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
  439. package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
  440. package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
  441. package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
  442. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
  443. package/src/__tests__/resolverBase.fls.test.ts +87 -4
  444. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  445. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -96
  446. package/src/__tests__/sharedCache.test.ts +312 -0
  447. package/src/__tests__/widgetGuestElevation.test.ts +44 -0
  448. package/src/agentSessions/BridgeHostInstance.ts +32 -0
  449. package/src/agentSessions/HostInstance.ts +124 -28
  450. package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
  451. package/src/agentSessions/SessionJanitor.ts +821 -59
  452. package/src/agentSessions/SessionManager.ts +138 -26
  453. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  454. package/src/agentSessions/index.ts +10 -3
  455. package/src/apiRoot.ts +78 -0
  456. package/src/auth/AuthProviderCatalogRouter.ts +26 -0
  457. package/src/auth/LoginScreenEngine.ts +50 -0
  458. package/src/auth/actionAuthorization.ts +116 -0
  459. package/src/auth/index.ts +4 -8
  460. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  461. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  462. package/src/auth/newUsers.ts +99 -11
  463. package/src/config.ts +171 -5
  464. package/src/context.ts +15 -10
  465. package/src/coreRoutes.ts +166 -0
  466. package/src/generated/graphql-schemas/__mj.ts +1815 -57
  467. package/src/generic/CacheInvalidationResolver.ts +73 -18
  468. package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
  469. package/src/generic/PushStatusResolver.ts +1 -1
  470. package/src/generic/ResolverBase.ts +335 -326
  471. package/src/generic/RunViewResolver.ts +21 -0
  472. package/src/globalCors.ts +71 -0
  473. package/src/index.ts +231 -300
  474. package/src/isolatedProvider.ts +22 -0
  475. package/src/multiTenancy/index.ts +24 -2
  476. package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
  477. package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
  478. package/src/realtimeSessions/README.md +179 -0
  479. package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
  480. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +168 -0
  481. package/src/realtimeSessions/RealtimeVerifyRouter.ts +109 -0
  482. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  483. package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
  484. package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
  485. package/src/realtimeSessions/index.ts +18 -0
  486. package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
  487. package/src/realtimeSessions/sessionAccess.ts +145 -0
  488. package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
  489. package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
  490. package/src/realtimeSessions/verificationCore.ts +502 -0
  491. package/src/realtimeSessions/verificationPages.ts +149 -0
  492. package/src/realtimeSessions/verificationPolicy.ts +321 -0
  493. package/src/realtimeSessions/verificationWorkflow.ts +702 -0
  494. package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
  495. package/src/resolvers/ActionResolver.ts +84 -21
  496. package/src/resolvers/AdhocQueryResolver.ts +24 -6
  497. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  498. package/src/resolvers/CacheStatsResolver.ts +89 -2
  499. package/src/resolvers/ClientToolRequestResolver.ts +25 -7
  500. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  501. package/src/resolvers/DatasetResolver.ts +70 -10
  502. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  503. package/src/resolvers/FetchEntityVectorsResolver.ts +179 -2
  504. package/src/resolvers/FileResolver.ts +321 -106
  505. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  506. package/src/resolvers/GetDataResolver.ts +6 -3
  507. package/src/resolvers/HumanHandoffResolver.ts +8 -0
  508. package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
  509. package/src/resolvers/MCPResolver.ts +8 -5
  510. package/src/resolvers/QueryResolver.ts +11 -0
  511. package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
  512. package/src/resolvers/RealtimeClientSessionResolver.ts +511 -129
  513. package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
  514. package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
  515. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  516. package/src/resolvers/RunAIAgentResolver.ts +116 -14
  517. package/src/resolvers/RunTemplateResolver.ts +13 -12
  518. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  519. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  520. package/src/resolvers/SyncDataResolver.ts +6 -2
  521. package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
  522. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  523. package/src/resolvers/TestQuerySQLResolver.ts +68 -13
  524. package/src/resolvers/TransactionGroupResolver.ts +131 -27
  525. package/src/resolvers/UserAvatarResolver.ts +111 -0
  526. package/src/resolvers/UserResolver.ts +2 -16
  527. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  528. package/src/resolvers/agent-run-data-guard.ts +89 -0
  529. package/src/resolvers/agentVisionConsentAudit.ts +82 -0
  530. package/src/resolvers/avatarInputValidation.ts +123 -0
  531. package/src/resolvers/currentUserRoles.ts +32 -1
  532. package/src/resolvers/meetings/meetingMutations.ts +21 -0
  533. package/src/rest/RealtimeRecordingHandler.ts +464 -0
  534. package/src/rest/ViewOperationsHandler.ts +29 -13
  535. package/src/rest/setupRESTEndpoints.ts +6 -0
  536. package/src/serverExtensionReservedRoots.ts +14 -17
  537. package/src/services/MJStorageBlobStore.ts +31 -30
  538. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  539. package/src/services/TaskGraphActionRunner.ts +25 -5
  540. package/src/services/TaskGraphAgentRunner.ts +188 -11
  541. package/src/services/TaskGraphContinuationDeliverer.ts +713 -17
  542. package/src/sharedCache.ts +305 -0
  543. package/src/types.ts +7 -0
@@ -1,5 +1,5 @@
1
1
  import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, EntityPermissionType, KeyValuePair, LogDebug, LogError, LogStatus, Metadata, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
2
- import { UserCache } from '@memberjunction/generic-database-provider';
2
+ import { ClientClauseScreen, UserCache } from '@memberjunction/generic-database-provider';
3
3
  import { AuthorizationError } from 'type-graphql';
4
4
  import { GraphQLError } from 'graphql';
5
5
  import { RefusalExtensions } from './refusalExtensions.js';
@@ -7,13 +7,22 @@ import { GetAPIKeyEngine } from '@memberjunction/api-keys';
7
7
  import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
8
8
  import { DeleteOptionsInput } from './DeleteOptionsInput.js';
9
9
  import { MJEventType, MJGlobal, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
10
- import { SQLParser } from '@memberjunction/sql-parser';
11
- import { PostgreSQLDialect, SQLServerDialect } from '@memberjunction/sql-dialect';
12
- import { EncryptionEngine } from '@memberjunction/encryption';
10
+ import { SQLParser, IsBalancedSQLFragment, LexSQL } from '@memberjunction/sql-parser';
11
+ import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
13
12
  import { PublishStatusUpdate } from './PushStatusResolver.js';
14
13
  import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
15
14
  import { PubSubManager } from './PubSubManager.js';
16
15
  import { FieldMapper } from '@memberjunction/graphql-dataprovider';
16
+ /**
17
+ * Entities whose update is always hydrated from the stored row, never from the client's `OldValues___`, though they
18
+ * do not track record changes: their server-side write rules compare against the prior state, which a client could
19
+ * otherwise supply itself (lower-case names). See `ResolverBase.MustLoadTruthFromDatabase`.
20
+ */
21
+ const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW = new Set([
22
+ 'mj: record changes',
23
+ 'mj: conversation details',
24
+ 'mj: data context items',
25
+ ]);
17
26
  export class ResolverBase {
18
27
  static { this._emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null; }
19
28
  static { this._cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {}; }
@@ -51,126 +60,123 @@ export class ResolverBase {
51
60
  * @param contextUser - Optional user context for decryption (required for encrypted fields)
52
61
  * @returns A new object in transport shape, or null when there is nothing to map
53
62
  */
54
- async MapFieldNamesToCodeNames(entityName, dataObject, contextUser, provider, deniedReadFields, recordChangeProjector) {
55
- // Return null for empty objects (e.g. when no rows found due to RLS filtering)
63
+ /**
64
+ * Precomputed per-entity / per-request field-mapping plan shared across rows in
65
+ * {@link MapFieldNamesToCodeNames} and {@link ArrayMapFieldNamesToCodeNames}.
66
+ */
67
+ buildFieldMappingPlan(entityName, contextUser, provider, deniedReadFields, recordChangeProjector) {
68
+ const md = provider ?? new Metadata();
69
+ const entityInfo = md.EntityByName(entityName);
70
+ if (!entityInfo)
71
+ throw new Error(`Entity ${entityName} not found in metadata`);
72
+ const denied = deniedReadFields ?? (entityInfo.EnableFieldLevelSecurity && contextUser
73
+ ? entityInfo.GetDeniedReadFields(contextUser)
74
+ : null);
75
+ const mapper = new FieldMapper();
76
+ const fieldsToProcess = [];
77
+ for (const f of entityInfo.Fields) {
78
+ const mappedFieldName = mapper.MapFieldName(f.CodeName);
79
+ const isDenied = Boolean(denied?.has(f.Name.trim().toLowerCase()));
80
+ if (isDenied || mappedFieldName !== f.Name) {
81
+ fieldsToProcess.push({ name: f.Name, mappedFieldName, isDenied });
82
+ }
83
+ }
84
+ const readableFields = this.BuildReadableFieldsTransportValue(entityInfo, denied);
85
+ const projector = contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)
86
+ ? (recordChangeProjector ?? new RecordChangeFieldSecurityProjector(md, contextUser))
87
+ : undefined;
88
+ const encryptedFields = entityInfo.EncryptedFields ?? [];
89
+ const checkBinary = entityInfo.HasBinaryFields !== false;
90
+ return {
91
+ entityInfo,
92
+ fieldsToProcess,
93
+ readableFields,
94
+ projector,
95
+ encryptedFields,
96
+ checkBinary,
97
+ };
98
+ }
99
+ /**
100
+ * Applies synchronous field renaming, FLS stripping, readable-fields transport stamping, and
101
+ * Record Changes payload projection for a single row using a precomputed mapping plan.
102
+ */
103
+ mapRowSynchronously(dataObject, plan) {
56
104
  if (!dataObject || Object.keys(dataObject).length === 0) {
57
105
  return null;
58
106
  }
59
- // Shallow copy up front so every write below lands on our object, never the caller's.
60
- // Binary values are base64 strings everywhere above the providers; any byte array that still
61
- // reaches a resolver (custom code, an external driver) is converted here, because GraphQL's
62
- // String scalar cannot serialize a Buffer and would fail the whole response.
63
- dataObject = ReplaceByteArraysWithBase64({ ...dataObject });
64
- // for the given entity name provided, check to see if there are any fields
65
- // where the code name is different from the field name, and for just those
66
- // fields, iterate through the dataObject and REPLACE the property that has the field name
67
- // with the CodeName, because we can't transfer those via GraphQL as they are not
68
- // valid property names in GraphQL
69
- {
70
- const md = provider ?? new Metadata();
71
- const entityInfo = md.EntityByName(entityName);
72
- if (!entityInfo)
73
- throw new Error(`Entity ${entityName} not found in metadata`);
74
- // const fields = entityInfo.Fields.filter((f) => f.Name !== f.CodeName || f.Name.startsWith('__mj_'));
75
- // FIELD-LEVEL SECURITY — this is the authoritative read boundary for every GraphQL
76
- // return path (single-record resolvers, external-data-source loads, and anything else
77
- // routed through here), sitting exactly where encryption masking already lives.
78
- //
79
- // The denied set is normally computed ONCE per (entity, user) by the caller and passed
80
- // in: this method runs once per ROW, so resolving it here would cost fields x rows
81
- // aggregations. We fall back to computing it when a caller doesn't supply it (the
82
- // single-record resolvers, where "once per row" and "once per request" are the same
83
- // thing) rather than failing open — a missing argument must never mean missing security.
84
- const denied = deniedReadFields ?? (entityInfo.EnableFieldLevelSecurity && contextUser
85
- ? entityInfo.GetDeniedReadFields(contextUser)
86
- : null);
87
- const mapper = new FieldMapper();
88
- entityInfo.Fields.forEach((f) => {
89
- if (dataObject.hasOwnProperty(f.Name)) {
90
- // GraphQL doesn't allow us to pass back fields with __ so we are mapping our special field cases that start with __mj_ to _mj__ for transport - they are converted back on the other side automatically
91
- const mappedFieldName = mapper.MapFieldName(f.CodeName);
92
- if (denied?.has(f.Name.trim().toLowerCase())) {
93
- // Omit entirely rather than nulling — a null is indistinguishable from a real
94
- // null value, and the client should see the field as absent, not as empty.
95
- // Both key shapes are removed because callers reach this method with rows keyed
96
- // either way depending on whether mapping has already run.
97
- delete dataObject[f.Name];
98
- delete dataObject[mappedFieldName];
99
- return;
100
- }
101
- if (mappedFieldName !== f.Name) {
102
- dataObject[mappedFieldName] = dataObject[f.Name];
103
- delete dataObject[f.Name];
104
- }
107
+ let row = plan.checkBinary
108
+ ? ReplaceByteArraysWithBase64({ ...dataObject })
109
+ : { ...dataObject };
110
+ for (let i = 0; i < plan.fieldsToProcess.length; i++) {
111
+ const fp = plan.fieldsToProcess[i];
112
+ if (Object.prototype.hasOwnProperty.call(row, fp.name)) {
113
+ if (fp.isDenied) {
114
+ delete row[fp.name];
115
+ delete row[fp.mappedFieldName];
116
+ }
117
+ else {
118
+ row[fp.mappedFieldName] = row[fp.name];
119
+ delete row[fp.name];
105
120
  }
106
- });
107
- // Field security, second half: say IN-BAND which fields this caller may read. Deleting the
108
- // key above is not enough on its own — GraphQL emits every SELECTED field regardless, so a
109
- // denied field the client asked for arrives as an explicit null it cannot tell apart from a
110
- // genuine one. The client must not settle that from its own metadata: during the window
111
- // after a permission change its copy disagrees with ours, and once metadata filtering lands
112
- // (issue #3485) it may not hold the permission rows at all. Null for unrestricted callers,
113
- // which is nearly every request.
114
- const readableFields = this.BuildReadableFieldsTransportValue(entityInfo, denied);
115
- if (readableFields) {
116
- dataObject[ReadableFieldsTransportKey] = readableFields;
117
- }
118
- // Field security, third part — the audit trail. A 'MJ: Record Changes' row carries the old
119
- // and new values of ANOTHER entity's fields, and its own EnableFieldLevelSecurity flag is
120
- // off, so everything above is a no-op for it and the payload would flow out untouched. The
121
- // projector recomputes the denied set against the entity each row is about; see
122
- // RecordChangeFieldSecurityProjector for what each payload column gets.
123
- //
124
- // Same "a missing argument must never mean missing security" rule as the denied set above:
125
- // the projector is normally built ONCE per request by the array caller and passed in, and
126
- // we build one here when it wasn't — the single-record resolvers, where once-per-row and
127
- // once-per-request are the same thing.
128
- if (contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)) {
129
- const projector = recordChangeProjector ?? new RecordChangeFieldSecurityProjector(md, contextUser);
130
- dataObject = projector.ProjectRow(dataObject);
131
121
  }
132
- // Handle encrypted fields - data from raw SQL queries is still encrypted
133
- const encryptedFields = entityInfo.EncryptedFields;
134
- if (encryptedFields.length > 0) {
135
- for (const field of encryptedFields) {
136
- const fieldName = field.CodeName;
137
- const value = dataObject[fieldName];
138
- // Skip null/undefined values
139
- if (value === null || value === undefined)
140
- continue;
141
- // Check if value is encrypted (raw SQL returns encrypted values)
142
- const engine = EncryptionEngine.Instance;
143
- await engine.Config(false, contextUser);
144
- const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
145
- if (typeof value === 'string' && IsValueEncrypted(value, keyMarker)) {
146
- if (field.AllowDecryptInAPI) {
147
- // Decrypt the value for the client
148
- if (contextUser) {
149
- try {
150
- const decryptedValue = await engine.Decrypt(value, contextUser);
151
- dataObject[fieldName] = decryptedValue;
152
- }
153
- catch (err) {
154
- LogError(`Failed to decrypt field ${fieldName} for API response: ${err}`);
155
- // On decryption failure, use sentinel for safety
156
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
157
- }
122
+ }
123
+ if (plan.readableFields) {
124
+ row[ReadableFieldsTransportKey] = plan.readableFields;
125
+ }
126
+ if (plan.projector) {
127
+ row = plan.projector.ProjectRow(row);
128
+ }
129
+ return row;
130
+ }
131
+ /**
132
+ * Decrypts or masks encrypted fields on a mapped row when `plan.encryptedFields.length > 0`.
133
+ */
134
+ async decryptRowEncryptedFields(row, encryptedFields, contextUser) {
135
+ for (const field of encryptedFields) {
136
+ const fieldName = field.CodeName;
137
+ const value = row[fieldName];
138
+ if (value === null || value === undefined)
139
+ continue;
140
+ const engine = EncryptionEngine.Instance;
141
+ await engine.Config(false, contextUser);
142
+ const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
143
+ if (typeof value === 'string' && IsValueEncrypted(value, keyMarker)) {
144
+ if (field.AllowDecryptInAPI) {
145
+ if (contextUser) {
146
+ try {
147
+ const decryptedValue = await engine.Decrypt(value, contextUser);
148
+ row[fieldName] = decryptedValue;
149
+ }
150
+ catch (err) {
151
+ if (IsEncryptionKeyUnavailable(err)) {
152
+ ReportEncryptionKeyUnavailable(err);
158
153
  }
159
154
  else {
160
- // No context user, can't decrypt - use sentinel
161
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
155
+ LogError(`Failed to decrypt field ${fieldName} for API response: ${err}`);
162
156
  }
157
+ row[fieldName] = ENCRYPTED_SENTINEL;
163
158
  }
164
- else if (!field.SendEncryptedValue) {
165
- // AllowDecryptInAPI=false and SendEncryptedValue=false - use sentinel
166
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
167
- }
168
- // else: AllowDecryptInAPI=false and SendEncryptedValue=true - keep encrypted value as-is
159
+ }
160
+ else {
161
+ row[fieldName] = ENCRYPTED_SENTINEL;
169
162
  }
170
163
  }
164
+ else if (!field.SendEncryptedValue) {
165
+ row[fieldName] = ENCRYPTED_SENTINEL;
166
+ }
171
167
  }
172
168
  }
173
- return dataObject;
169
+ }
170
+ async MapFieldNamesToCodeNames(entityName, dataObject, contextUser, provider, deniedReadFields, recordChangeProjector) {
171
+ if (!dataObject || Object.keys(dataObject).length === 0) {
172
+ return null;
173
+ }
174
+ const plan = this.buildFieldMappingPlan(entityName, contextUser, provider, deniedReadFields, recordChangeProjector);
175
+ const mapped = this.mapRowSynchronously(dataObject, plan);
176
+ if (mapped && plan.encryptedFields.length > 0) {
177
+ await this.decryptRowEncryptedFields(mapped, plan.encryptedFields, contextUser);
178
+ }
179
+ return mapped;
174
180
  }
175
181
  /**
176
182
  * Reverse-maps GraphQL-safe field names back to entity CodeNames in a mutation input object.
@@ -227,26 +233,17 @@ export class ResolverBase {
227
233
  if (!dataObjectArray || dataObjectArray.length === 0) {
228
234
  return dataObjectArray;
229
235
  }
230
- // Resolve the field-security denied set ONCE for the whole array and pass it into the
231
- // per-row mapper. MapFieldNamesToCodeNames runs once per row, so computing this inside
232
- // the loop would be fields x rows aggregations — 40,000 for a 1,000-row x 40-column
233
- // result, each re-scanning the user's roles and allocating. Gated on the entity-level
234
- // flag so non-FLS entities (nearly all of them) don't even resolve the entity twice.
235
- const md = provider ?? new Metadata();
236
- const entityInfo = md.EntityByName(entityName);
237
- const deniedReadFields = entityInfo?.EnableFieldLevelSecurity && contextUser
238
- ? entityInfo.GetDeniedReadFields(contextUser)
239
- : undefined;
240
- // Record Changes payload security is resolved per DISTINCT target entity, and one array can
241
- // span many — so the projector, which owns that memo, must outlive the row loop. Built once
242
- // here for the same reason the denied set above is: GetDeniedReadFields walks every field on
243
- // an entity and must never be called per row.
244
- const recordChangeProjector = contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)
245
- ? new RecordChangeFieldSecurityProjector(md, contextUser)
246
- : undefined;
247
- const mapped = [];
248
- for (const element of dataObjectArray) {
249
- mapped.push(await this.MapFieldNamesToCodeNames(entityName, element, contextUser, provider, deniedReadFields, recordChangeProjector));
236
+ // Precompute the entity lookup, FieldMapper plan, denied-fields set, readable-fields list,
237
+ // and Record Changes projector ONCE for the whole array rather than per row.
238
+ const plan = this.buildFieldMappingPlan(entityName, contextUser, provider);
239
+ const hasEncryptedFields = plan.encryptedFields.length > 0;
240
+ const mapped = new Array(dataObjectArray.length);
241
+ for (let i = 0; i < dataObjectArray.length; i++) {
242
+ const row = this.mapRowSynchronously(dataObjectArray[i], plan);
243
+ if (row && hasEncryptedFields) {
244
+ await this.decryptRowEncryptedFields(row, plan.encryptedFields, contextUser);
245
+ }
246
+ mapped[i] = row;
250
247
  }
251
248
  return mapped;
252
249
  }
@@ -302,9 +299,11 @@ export class ResolverBase {
302
299
  * @param dataObject - The data object containing field values
303
300
  * @param encryptionEngine - Optional encryption engine for re-encryption (lazy loaded if needed)
304
301
  * @param contextUser - User context for encryption operations
302
+ * @param fieldKey - What the object's keys are: field CodeNames (GraphQL transport rows, the
303
+ * default) or field Names (raw provider rows, such as dataset item results)
305
304
  * @returns The filtered data object
306
305
  */
307
- async FilterEncryptedFieldsForAPI(entityName, dataObject, contextUser, provider) {
306
+ async FilterEncryptedFieldsForAPI(entityName, dataObject, contextUser, provider, fieldKey = 'CodeName') {
308
307
  if (!dataObject)
309
308
  return dataObject;
310
309
  const md = provider ?? new Metadata();
@@ -317,7 +316,7 @@ export class ResolverBase {
317
316
  return dataObject;
318
317
  // Process each encrypted field
319
318
  for (const field of encryptedFields) {
320
- const fieldName = field.CodeName;
319
+ const fieldName = fieldKey === 'Name' ? field.Name : field.CodeName;
321
320
  const value = dataObject[fieldName];
322
321
  // Skip null/undefined values
323
322
  if (value === null || value === undefined)
@@ -353,9 +352,10 @@ export class ResolverBase {
353
352
  return dataObject;
354
353
  }
355
354
  /**
356
- * Filters encrypted fields for an array of data objects
355
+ * Filters encrypted fields for an array of data objects. `fieldKey` is as for
356
+ * {@link FilterEncryptedFieldsForAPI}.
357
357
  */
358
- async ArrayFilterEncryptedFieldsForAPI(entityName, dataObjectArray, contextUser, provider) {
358
+ async ArrayFilterEncryptedFieldsForAPI(entityName, dataObjectArray, contextUser, provider, fieldKey = 'CodeName') {
359
359
  if (!dataObjectArray || dataObjectArray.length === 0)
360
360
  return dataObjectArray;
361
361
  // Check if entity has any encrypted fields first to avoid unnecessary processing
@@ -368,7 +368,7 @@ export class ResolverBase {
368
368
  return dataObjectArray;
369
369
  // Process each element
370
370
  for (const element of dataObjectArray) {
371
- await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser, provider);
371
+ await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser, provider, fieldKey);
372
372
  }
373
373
  return dataObjectArray;
374
374
  }
@@ -727,44 +727,59 @@ export class ResolverBase {
727
727
  * This screen uses `@memberjunction/sql-parser` (same wrap as EDS `assertReadOnlyClause`):
728
728
  * wrap the fragment as a single SELECT, fail closed if it does not parse as a read, then
729
729
  * allow a FROM only when it is an entity **BaseView**. Base tables (`Meeting`, `__mj.User`)
730
- * and catalogs are rejected. Server-internal RunView callers never hit this.
730
+ * and catalogs are rejected. Server-internal RunView callers get only the provider's
731
+ * statement-separator check, which cannot refuse a legitimate filter.
732
+ *
733
+ * The screen is `ClientClauseScreen` in `@memberjunction/generic-database-provider`, shared
734
+ * with the REST view routes, ad-hoc list sources, and the provider's saved-view checks.
735
+ *
736
+ * Before that parse, the fragment must be balanced on its own (`IsBalancedSQLFragment`). The
737
+ * provider and the tenant hook splice it inside parentheses and AND their own filters after
738
+ * it, so a fragment that closes those parentheses (`1=1) OR (1=1`) would leave row-level
739
+ * security, API-key row filters and tenant scope on one side of an OR the caller controls.
740
+ * The parse wraps the fragment in parentheses too, which is why it cannot catch this alone.
741
+ * A backslash inside a quoted string or identifier is refused as well: the parser reads `\'`
742
+ * inside a plain literal as an escaped quote, but SQL Server and PostgreSQL end the literal
743
+ * there, so the text after it could be code that the parse-based checks never saw.
731
744
  *
732
745
  * RLS is applied by RunView as an outer WHERE around the entity being queried, not compiled
733
- * into the view. Subqueries against another entity's BaseView therefore do not inherit that
734
- * entity's RLS; they are still restricted to the view (not the table).
746
+ * into the view, and RunView removes denied fields from its own result only. A subquery reads a
747
+ * base view directly with neither, so the screen refuses one over any entity the user reads under
748
+ * a row filter or with denied fields (`ClientClauseScreen.AssertUnscopedRead`), the queried
749
+ * entity included.
735
750
  */
736
- assertClientClauseUsesEntityBaseViews(clause, label, provider, user) {
751
+ assertClientClauseUsesEntityBaseViews(clause, label, provider, user, options) {
737
752
  if (!clause?.trim())
738
753
  return;
754
+ // The balance and backslash checks run before the shared screen: it parses the fragment
755
+ // inside its own parentheses, so it cannot see a fragment that closes the parentheses the
756
+ // provider and the tenant hook splice it into.
739
757
  const dialect = this.dialectForProvider(provider);
740
758
  if (SQLParser.HasStackedStatements(clause, dialect)) {
741
759
  throw new Error(`Invalid ${label}: multiple statements are not permitted in client-supplied filters`);
742
760
  }
743
- const wrapped = label === 'OrderBy'
744
- ? `SELECT 1 FROM __mj_clause_screen ORDER BY ${clause}`
745
- : `SELECT 1 FROM __mj_clause_screen WHERE (${clause})`;
746
- const parser = new SQLParser(wrapped, dialect);
747
- if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
748
- throw new Error(`Invalid ${label}: not a safe read-only filter fragment — refusing under uncertainty`);
761
+ const balance = IsBalancedSQLFragment(clause, dialect);
762
+ if (!balance.IsBalanced) {
763
+ throw new Error(`Invalid ${label}: ${balance.Reason}`);
749
764
  }
750
- if (this.astContainsWriteNode(parser.AST)) {
751
- throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
752
- }
753
- const allowed = this.entityBaseViewAllowList(provider);
754
- const tables = SQLParser.ExtractTableRefs(wrapped, dialect);
755
- for (const t of tables) {
756
- const table = this.stripSqlIdent(t.TableName);
757
- const schema = this.stripSqlIdent(t.SchemaName);
758
- if (table.toLowerCase() === '__mj_clause_screen')
759
- continue;
760
- this.assertTableRefReadable(allowed, schema, table, label, user);
765
+ if (this.hasBackslashInQuotes(clause, dialect)) {
766
+ throw new Error(`Invalid ${label}: a backslash inside a quoted string or identifier is not permitted in client-supplied filters`);
761
767
  }
768
+ ClientClauseScreen.AssertClauseUsesEntityBaseViews(clause, label, provider?.Entities ?? [], dialect, user, options);
762
769
  }
763
770
  /**
764
771
  * SECURITY — screen for a FULL client-supplied SQL statement (ad-hoc query path). In addition
765
772
  * to the SELECT-only validation the caller performs, every table reference must resolve to an
766
- * entity BaseView the acting user can read UNSCOPED. CTE names defined by the statement itself
767
- * are excluded from the check. Fails closed: no user, or an unresolvable reference, refuses.
773
+ * entity BaseView the acting user can read UNSCOPED. A reference is excluded only when it reads a
774
+ * CTE that is in scope there (`StatementRelation.IsCTEReference`): unqualified, declared by its
775
+ * own SELECT or an enclosing one, and matched by the dialect's identifier rules. A name reserved
776
+ * for the SQL parser is refused before parsing (`ClientClauseScreen.AssertNoIdentifierAliasNames`).
777
+ * Fails closed: no user, or an unresolvable reference, refuses.
778
+ *
779
+ * The references come from a walk of the whole parse tree
780
+ * (`ClientClauseScreen.CollectStatementReads`). The statement is refused when that walk does not
781
+ * account for every SELECT in the text, or when a FROM item names no table (a table function or
782
+ * a row list).
768
783
  *
769
784
  * Entity CanRead is not unscoped base-view read authority: RunView narrows a granted read with
770
785
  * row-level security and denied-field projection, and raw SQL applies neither. Rather than try
@@ -777,6 +792,7 @@ export class ResolverBase {
777
792
  throw new Error(`Invalid ${label}: no acting user resolved for ad-hoc SQL — refusing`);
778
793
  }
779
794
  const dialect = this.dialectForProvider(provider);
795
+ ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
780
796
  const parser = new SQLParser(sqlText, dialect);
781
797
  if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
782
798
  throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
@@ -784,51 +800,20 @@ export class ResolverBase {
784
800
  if (this.astContainsWriteNode(parser.AST)) {
785
801
  throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
786
802
  }
787
- const cteNames = new Set();
788
- this.collectCTENames(parser.AST, cteNames);
803
+ const reads = ClientClauseScreen.CollectStatementReads(parser);
804
+ ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
789
805
  const allowed = this.entityBaseViewAllowList(provider);
790
- const tables = SQLParser.ExtractTableRefs(sqlText, dialect);
791
- for (const t of tables) {
792
- const table = this.stripSqlIdent(t.TableName);
793
- const schema = this.stripSqlIdent(t.SchemaName);
794
- if (this.isUnqualifiedSchema(schema) && cteNames.has(table.toLowerCase()))
806
+ for (const relation of reads.Relations) {
807
+ if (relation.IsCTEReference)
795
808
  continue;
796
- const entity = this.assertTableRefReadable(allowed, schema, table, label, user);
797
- this.assertUnscopedRead(entity, user, label);
809
+ const entity = ClientClauseScreen.AssertRelationReadable(allowed, relation, label, user);
810
+ if (entity)
811
+ this.assertUnscopedRead(entity, user, label);
798
812
  }
799
813
  }
800
- /** Refuses an entity whose read is narrowed for this user by row-level security or field-level denials. */
814
+ /** Refuses an entity whose read is narrowed for this user; see `ClientClauseScreen.AssertUnscopedRead`. */
801
815
  assertUnscopedRead(entity, user, label) {
802
- if (entity.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Read, '').length > 0) {
803
- throw new Error(`Invalid ${label}: entity '${entity.Name}' is row-level-security filtered for you — ad-hoc SQL cannot apply that filter; use RunView`);
804
- }
805
- if (entity.GetDeniedReadFields(user).size > 0) {
806
- throw new Error(`Invalid ${label}: entity '${entity.Name}' has fields you are not permitted to read — ad-hoc SQL cannot project them away; use RunView`);
807
- }
808
- }
809
- /** Walks a parsed AST collecting the names of CTEs the statement itself defines. */
810
- collectCTENames(node, names) {
811
- if (!node || typeof node !== 'object')
812
- return;
813
- if (Array.isArray(node)) {
814
- for (const n of node)
815
- this.collectCTENames(n, names);
816
- return;
817
- }
818
- const obj = node;
819
- const withClause = obj.with;
820
- if (Array.isArray(withClause)) {
821
- for (const cte of withClause) {
822
- const name = cte?.name;
823
- if (typeof name === 'string')
824
- names.add(name.toLowerCase());
825
- else if (name && typeof name === 'object' && typeof name.value === 'string') {
826
- names.add(name.value.toLowerCase());
827
- }
828
- }
829
- }
830
- for (const v of Object.values(obj))
831
- this.collectCTENames(v, names);
816
+ ClientClauseScreen.AssertUnscopedRead(entity, user, label);
832
817
  }
833
818
  /**
834
819
  * Applies {@link assertClientClauseUsesEntityBaseViews} to every client-supplied clause
@@ -851,96 +836,37 @@ export class ResolverBase {
851
836
  * The single place that is not true is a field carrying `UserSearchParamFormatAPI`, whose
852
837
  * admin-authored format may splice the term in unquoted; `createViewUserSearchSQL` re-applies
853
838
  * `ValidateUserProvidedSQLClause` for exactly those entities.
839
+ *
840
+ * Each `Aggregates[].expression` gets the same screen, except that text the parser cannot read
841
+ * goes on to the provider, which refuses it per aggregate. The provider never runs the caller's
842
+ * aggregate text: it runs the SQL that `CheckAggregateExpression` rebuilds from a single
843
+ * aggregate call over the entity's columns, and refuses everything else per aggregate.
854
844
  */
855
845
  screenClientViewClauses(clauses, provider, user) {
856
846
  this.assertClientClauseUsesEntityBaseViews(clauses.extraFilter, 'ExtraFilter', provider, user);
857
847
  this.assertClientClauseUsesEntityBaseViews(clauses.orderBy, 'OrderBy', provider, user);
858
848
  this.assertClientClauseUsesEntityBaseViews(clauses.overrideExcludeFilter, 'OverrideExcludeFilter', provider, user);
849
+ for (const aggregate of clauses.aggregates ?? []) {
850
+ this.assertClientClauseUsesEntityBaseViews(aggregate?.expression, 'Aggregate', provider, user, { LeaveUnparseableToProvider: true });
851
+ }
852
+ }
853
+ /**
854
+ * True when a string literal or quoted identifier in `clause`, read the way the database reads
855
+ * it, contains a backslash (see {@link assertClientClauseUsesEntityBaseViews}).
856
+ */
857
+ hasBackslashInQuotes(clause, dialect) {
858
+ return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
859
859
  }
860
- /** Same write-node walk as EDS `sqlReadOnlyScreen.astContainsWriteNode`. */
860
+ /** Write/DDL node walk shared with the clause screen; see `ClientClauseScreen.AstContainsWriteNode`. */
861
861
  astContainsWriteNode(node) {
862
- if (!node || typeof node !== 'object')
863
- return false;
864
- if (Array.isArray(node))
865
- return node.some((n) => this.astContainsWriteNode(n));
866
- const obj = node;
867
- const type = obj.type;
868
- if (typeof type === 'string' && ResolverBase.WRITE_NODE_TYPES.has(type.toLowerCase())) {
869
- return true;
870
- }
871
- return Object.values(obj).some((v) => this.astContainsWriteNode(v));
862
+ return ClientClauseScreen.AstContainsWriteNode(node);
872
863
  }
873
- static { this.WRITE_NODE_TYPES = new Set([
874
- 'insert', 'update', 'delete', 'merge', 'replace', 'drop', 'create', 'alter', 'truncate',
875
- 'rename', 'call', 'exec', 'execute', 'grant', 'revoke', 'use', 'load', 'copy', 'do',
876
- ]); }
877
864
  dialectForProvider(provider) {
878
- const name = provider?.constructor?.name ?? '';
879
- if (/postgres/i.test(name))
880
- return new PostgreSQLDialect();
881
- return new SQLServerDialect();
882
- }
883
- /** True for a reference with no schema — which `SQLParser.ExtractTableRefs` reports as `dbo`. */
884
- isUnqualifiedSchema(schema) {
885
- return !schema || schema.toLowerCase() === 'dbo';
865
+ return ClientClauseScreen.DialectFor(provider);
886
866
  }
887
- stripSqlIdent(name) {
888
- if (!name)
889
- return '';
890
- return name.replace(/^\[|\]$/g, '').replace(/^"|"$/g, '').replace(/^`|`$/g, '');
891
- }
892
- /**
893
- * `bare` maps an unqualified view name to its entity, or to `null` when the name exists in
894
- * more than one schema — the database, not this screen, would decide which one runs.
895
- */
867
+ /** The entity base-view allow-list for a provider's entities. */
896
868
  entityBaseViewAllowList(provider) {
897
- const qualified = new Map();
898
- const bare = new Map();
899
- const entities = provider?.Entities ?? [];
900
- for (const e of entities) {
901
- const view = this.stripSqlIdent(e.BaseView);
902
- if (!view)
903
- continue;
904
- const schema = this.stripSqlIdent(e.SchemaName);
905
- const bareKey = view.toLowerCase();
906
- bare.set(bareKey, bare.has(bareKey) ? null : e);
907
- if (schema)
908
- qualified.set(`${schema}.${view}`.toLowerCase(), e);
909
- }
910
- return { qualified, bare };
911
- }
912
- /**
913
- * SECURITY — resolves a table reference against the entity BaseView allow-list and, when an
914
- * acting user is supplied, additionally requires that user to hold CanRead on the referenced
915
- * entity. Base views do not embed RLS and entity permissions are otherwise checked only on
916
- * the TOP entity of a request, so without this check a subquery (or ad-hoc query) could read
917
- * entities the caller has no read grant on.
918
- *
919
- * A schema-qualified reference must match that exact schema — `secret.vwFoo` never resolves
920
- * through another schema's `vwFoo`. An unqualified reference resolves only when the view name
921
- * is unique across schemas; an ambiguous one must be qualified, because the database's
922
- * default-schema resolution, not this screen, would pick which view actually runs.
923
- *
924
- * `SQLParser.ExtractTableRefs` reports an unqualified reference as schema `dbo`, so `dbo` is
925
- * indistinguishable from "no schema" here and both take the unqualified path (an exact
926
- * `dbo.<view>` entity wins first).
927
- */
928
- assertTableRefReadable(allowed, schema, table, label, user) {
929
- const qualified = allowed.qualified.get(`${schema}.${table}`.toLowerCase());
930
- const entity = this.isUnqualifiedSchema(schema) ? qualified ?? allowed.bare.get(table.toLowerCase()) : qualified;
931
- if (entity === null) {
932
- throw new Error(`Invalid ${label}: '${table}' is a base view in more than one schema — qualify it with its schema`);
933
- }
934
- if (!entity) {
935
- throw new Error(`Invalid ${label}: subquery must use an entity base view, not '${schema ? schema + '.' : ''}${table}'`);
936
- }
937
- if (user) {
938
- const perms = entity.GetUserPermisions(user);
939
- if (!perms.CanRead) {
940
- throw new Error(`Invalid ${label}: you do not have read permission on entity '${entity.Name}' referenced by '${schema ? schema + '.' : ''}${table}'`);
941
- }
942
- }
943
- return entity;
869
+ return ClientClauseScreen.BuildEntityBaseViewAllowList(provider?.Entities ?? []);
944
870
  }
945
871
  /**
946
872
  * Optimized RunViewGenericInternal implementation with:
@@ -962,7 +888,7 @@ export class ResolverBase {
962
888
  // SECURITY: GraphQL ExtraFilter may contain IN (SELECT … FROM <base view>).
963
889
  // Screen at this boundary: parse, reject writes, allow only entity BaseViews the
964
890
  // acting user can read (subqueries do not inherit the top entity's permission check).
965
- this.screenClientViewClauses({ extraFilter, orderBy, userSearchString, overrideExcludeFilter }, provider, user);
891
+ this.screenClientViewClauses({ extraFilter, orderBy, userSearchString, overrideExcludeFilter, aggregates }, provider, user);
966
892
  // Check API key scope authorization for view operations
967
893
  await this.CheckAPIKeyScopeAuthorization('view:run', viewInfo.Entity, userPayload);
968
894
  const md = provider;
@@ -1102,6 +1028,7 @@ export class ResolverBase {
1102
1028
  orderBy: param.orderBy,
1103
1029
  userSearchString: param.userSearchString,
1104
1030
  overrideExcludeFilter: param.overrideExcludeFilter,
1031
+ aggregates: param.aggregates,
1105
1032
  }, md, contextUser ?? undefined);
1106
1033
  if (param.viewInfo) {
1107
1034
  // Validate entity only once per entity type
@@ -1481,9 +1408,10 @@ export class ResolverBase {
1481
1408
  // has TrackRecordChanges off, so without it a client sending OldValues takes the branch below
1482
1409
  // that hydrates from those values — never loading what the database actually holds.
1483
1410
  const hasNarrowedAuditPayload = this.StripRecordChangePayloadFromClientInput(entityInfo, userInfo, input, clientNewValues);
1484
- if (this.MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload)) {
1411
+ if (this.MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload, userInfo)) {
1485
1412
  // We get here because the entity tracks record changes, OR the client did not provide OldValues,
1486
- // OR field-level security is in play — in every case we need the true old values from the DB
1413
+ // OR field-level security or an Update row filter is in play — in every case we need the true
1414
+ // old values from the DB
1487
1415
  const cKey = new CompositeKey(entityInfo.PrimaryKeys.map((pk) => {
1488
1416
  return {
1489
1417
  FieldName: pk.Name,
@@ -1552,13 +1480,35 @@ export class ResolverBase {
1552
1480
  extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
1553
1481
  });
1554
1482
  }
1483
+ /**
1484
+ * Checks whether `oldValues` (`input.OldValues___`) contains every writable in-table field of
1485
+ * `entityInfo` (plus primary keys).
1486
+ *
1487
+ * With the delta-only `Save()` wire protocol, `GraphQLDataProvider.Save()` sends only primary keys
1488
+ * and dirty writable fields in `vars.input` and `OldValues___` on normal updates. Hydrating an
1489
+ * entity directly from a partial-delta `OldValues___` via `entityObject.LoadFromData(oldValues)`
1490
+ * (without first loading the authoritative row via `InnerLoad`) would leave every untouched column
1491
+ * `NotLoaded = true` (or `null`), causing `spUpdate` or custom `Validate()` hooks to see incomplete
1492
+ * state. Returning `false` for partial deltas ensures {@link MustLoadTruthFromDatabase} routes
1493
+ * delta updates through `InnerLoad(cKey)` + {@link TestAndSetClientOldValuesToDBValues}, even on
1494
+ * entities with `TrackRecordChanges === false`.
1495
+ */
1496
+ hasCompleteOldValues(entityInfo, oldValues) {
1497
+ if (!oldValues || oldValues.length === 0) {
1498
+ return false;
1499
+ }
1500
+ const providedKeys = new Set(oldValues.map((kv) => kv.Key));
1501
+ return entityInfo.Fields.every((f) => (!f.IsPrimaryKey && (f.ReadOnly || f.IsVirtual)) || providedKeys.has(f.Name) || providedKeys.has(f.CodeName));
1502
+ }
1555
1503
  /**
1556
1504
  * Whether `UpdateRecord` must hydrate the entity from the DATABASE rather than from the
1557
1505
  * client's `OldValues___`.
1558
1506
  *
1559
- * The `OldValues___` path exists as an optimization: when nothing needs the true prior state,
1560
- * the client already holds it and a round trip is wasted. Each condition below is a reason
1561
- * that assumption fails.
1507
+ * The `OldValues___` path exists as an optimization when a client supplies the complete pre-save
1508
+ * record (e.g. `IgnoreDirtyState: true`) and nothing else requires the database's authoritative prior
1509
+ * state. Under the delta-only `Save()` wire protocol, normal updates send only primary keys and dirty
1510
+ * writable fields in `OldValues___`, so {@link hasCompleteOldValues} returns `false` and forces the
1511
+ * database load (`InnerLoad`) so untouched columns retain their stored values.
1562
1512
  *
1563
1513
  * `EnableFieldLevelSecurity` is the security-critical one, and it is NOT redundant with
1564
1514
  * `hasDeniedReadFields`. That flag reports READ denials, and the canonical FLS configuration is
@@ -1576,21 +1526,41 @@ export class ResolverBase {
1576
1526
  * it never applies them to the entity. An update-denied field can then only become dirty by being
1577
1527
  * named in the mutation input itself, which is precisely the case that check does catch.
1578
1528
  *
1579
- * Ordered so the boolean flag is evaluated last: the extra load lands only on entities that have
1580
- * the feature switched on, which is almost none of them.
1529
+ * The flag comes after the cheap terms, so the extra load lands only on entities that have the
1530
+ * feature switched on, which is almost none of them.
1581
1531
  *
1582
1532
  * `MJ: Record Changes` always loads from the database: its server class allows an update only when
1583
1533
  * Comments is the one dirty field, and "dirty" compared against client-supplied OldValues lets a
1584
1534
  * caller pin forged audit columns as both old and new values. It doesn't track its own changes, so
1585
1535
  * nothing else forces the load.
1536
+ *
1537
+ * `MJ: Data Context Items` always loads from the database for the same reason: its server class
1538
+ * lets only an administrator save an item that holds SQL, and decides that from the item's `Type`
1539
+ * and `SQL`. Hydrated from OldValues, a client could leave both out, and the stored procedure would
1540
+ * keep the stored SQL item while the rule saw none.
1541
+ *
1542
+ * `MJ: Conversation Details` always loads for the same reason: its write gate decides from the stored
1543
+ * row (who wrote a person's message, its role, the owner's rating), and an OldValues-hydrated entity
1544
+ * would let a grantee claim to be a message's author while rewriting it. See
1545
+ * {@link ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW}.
1546
+ *
1547
+ * A caller under an Update row filter (a role's Update RLS filter or an API-key row filter) always
1548
+ * loads from the database too (#4919). The save's post-image check skips its query when no column
1549
+ * the filter reads is dirty, and dirtiness measured against client OldValues is the caller's to
1550
+ * choose: claiming the new value of a filter column as its old value made it clean, so the update
1551
+ * moved the row outside the caller's scope without the check ever running. Evaluated last: it
1552
+ * builds the filter clause, and only callers with such a filter pay for the load. A missing user
1553
+ * (a user-cache miss) also loads: with no user the filter can't be ruled out.
1586
1554
  */
1587
- MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload) {
1555
+ MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload, user) {
1588
1556
  return (entityInfo.TrackRecordChanges ||
1589
- entityInfo.Name.trim().toLowerCase() === 'mj: record changes' ||
1590
- !input.OldValues___ ||
1557
+ ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW.has(entityInfo.Name.trim().toLowerCase()) ||
1558
+ !this.hasCompleteOldValues(entityInfo, input.OldValues___) ||
1591
1559
  hasDeniedReadFields ||
1592
1560
  hasNarrowedAuditPayload ||
1593
- entityInfo.EnableFieldLevelSecurity);
1561
+ entityInfo.EnableFieldLevelSecurity ||
1562
+ !user ||
1563
+ entityInfo.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Update, '').length > 0);
1594
1564
  }
1595
1565
  /**
1596
1566
  * Field-level security guard for the update path.
@@ -1736,19 +1706,35 @@ export class ResolverBase {
1736
1706
  }
1737
1707
  }
1738
1708
  /**
1739
- * This routine compares the OldValues property in the input object to the values in the DB that we just loaded. If there are differences, we need to check to see if the client
1740
- * is trying to update any of those fields (e.g. overlap). If there is overlap, we throw an error. If there is no overlap, we can proceed with the update even if the DB Values
1741
- * and the ClientOldValues are not 100% the same, so long as there is no overlap in the specific FIELDS that are different.
1709
+ * Compares the client-supplied `OldValues___` array in `input` against the authoritative database
1710
+ * values just loaded onto `entityObject` via `InnerLoad`, checks for overlapping concurrent field
1711
+ * modifications, and then applies `clientNewValues` onto `entityObject` via `SetMany`.
1742
1712
  *
1743
- * ASSUMES: input object has an OldValues___ property that is an array of Key/Value pairs that represent the old values of the record that the client is trying to update.
1713
+ * Supports both full-record and delta-only `OldValues___` payloads: only keys present in
1714
+ * `input.OldValues___` (e.g. primary keys + dirty writable fields) are compared against `dbValues`,
1715
+ * and `entityObject.SetMany(clientNewValues)` mutates only the fields sent in `clientNewValues`
1716
+ * while preserving all untouched DB-loaded fields on `entityObject`.
1717
+ *
1718
+ * ASSUMES: `input` has an `OldValues___` property that is an array of Key/Value pairs representing
1719
+ * the pre-mutation values of the fields the client is updating.
1744
1720
  */
1721
+ /**
1722
+ * Resolves a field on `entityInfo` by `CodeName` or `Name` in $O(1)$ via `FieldByCodeName` /
1723
+ * `FieldByName` when available on `EntityInfo`, falling back to `.Fields.find()` for test stubs.
1724
+ */
1725
+ findEntityFieldByCodeNameOrName(entityInfo, key) {
1726
+ if (typeof entityInfo.FieldByCodeName === 'function') {
1727
+ return entityInfo.FieldByCodeName(key) ?? entityInfo.FieldByName(key);
1728
+ }
1729
+ return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
1730
+ }
1745
1731
  async TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject, contextUser) {
1746
1732
  // we have OldValues, so we need to compare them to the values we just loaded from the DB
1747
1733
  const clientOldValues = {};
1748
1734
  // for each item in the oldValues array, add it to the clientOldValues object
1749
1735
  input.OldValues___.forEach((item) => {
1750
1736
  // we need to do a quick transform on the values to make sure they match the TS Type for the given field because item.Value will always be a string
1751
- const field = entityObject.EntityInfo.Fields.find((f) => f.CodeName === item.Key);
1737
+ const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
1752
1738
  const val = this.ClientOldValueToFieldValue(field, item.Value);
1753
1739
  clientOldValues[item.Key] = val;
1754
1740
  });
@@ -1758,19 +1744,20 @@ export class ResolverBase {
1758
1744
  // now we need to compare clientOldValues and dbValues and have a new array that has entries for any differences and have FieldName, clientOldValue and dbValue as properties
1759
1745
  const dbDifferences = [];
1760
1746
  Object.keys(clientOldValues).forEach((key) => {
1761
- const f = entityObject.EntityInfo.Fields.find((f) => f.CodeName === key);
1747
+ const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
1748
+ const dbVal = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
1762
1749
  let different = false;
1763
1750
  switch (typeof clientOldValues[key]) {
1764
1751
  case 'number':
1765
- different = clientOldValues[key] !== dbValues[key];
1752
+ different = clientOldValues[key] !== dbVal;
1766
1753
  break;
1767
1754
  case 'boolean':
1768
- different = clientOldValues[key] !== dbValues[key];
1755
+ different = clientOldValues[key] !== dbVal;
1769
1756
  break;
1770
1757
  case 'object':
1771
1758
  if (clientOldValues[key] instanceof Date) {
1772
1759
  const clientDate = clientOldValues[key];
1773
- const dbDate = dbValues[key];
1760
+ const dbDate = dbVal;
1774
1761
  if (dbDate == null || !(dbDate instanceof Date)) {
1775
1762
  different = true;
1776
1763
  }
@@ -1789,11 +1776,11 @@ export class ResolverBase {
1789
1776
  }
1790
1777
  }
1791
1778
  else if (clientOldValues[key] === null) {
1792
- different = dbValues[key] !== null;
1779
+ different = dbVal !== null;
1793
1780
  }
1794
1781
  break;
1795
1782
  default:
1796
- different = clientOldValues[key] !== dbValues[key];
1783
+ different = clientOldValues[key] !== dbVal;
1797
1784
  break;
1798
1785
  }
1799
1786
  if (different && f && !f.ReadOnly) {
@@ -1801,7 +1788,7 @@ export class ResolverBase {
1801
1788
  dbDifferences.push({
1802
1789
  FieldName: key,
1803
1790
  ClientOldValue: clientOldValues[key],
1804
- DBValue: dbValues[key],
1791
+ DBValue: dbVal,
1805
1792
  });
1806
1793
  }
1807
1794
  });
@@ -1810,8 +1797,8 @@ export class ResolverBase {
1810
1797
  // first step is to get clientNewValues into an object that is like clientOldValues, get the diff and then compare that diff to the differences array that shows diff between DB and ClientOld
1811
1798
  const clientDifferences = [];
1812
1799
  Object.keys(clientOldValues).forEach((key) => {
1813
- const f = entityObject.EntityInfo.Fields.find((f) => f.CodeName === key);
1814
- if (clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
1800
+ const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
1801
+ if (key in clientNewValues && clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
1815
1802
  // only include updateable fields
1816
1803
  clientDifferences.push({
1817
1804
  FieldName: key,
@@ -1822,7 +1809,8 @@ export class ResolverBase {
1822
1809
  });
1823
1810
  // now we have clientDifferences which shows what the client thinks they are changing. And, we have the dbDifferences array that shows changes between the clientOldValues and the dbValues
1824
1811
  // if there is ANY overlap in the FIELDS that appear in both arrays, we need to log a warning but allow the save to continue
1825
- const overlap = clientDifferences.filter((cd) => dbDifferences.find((dd) => dd.FieldName === cd.FieldName));
1812
+ const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
1813
+ const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
1826
1814
  if (overlap.length > 0) {
1827
1815
  const msg = {
1828
1816
  Message: 'Inconsistency between old values provided for changed fields, and the values of one or more of those fields in the database. Save operation continued with warning.',