@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,152 @@
1
+ // ResolverBase transitively pulls in type-graphql decorators, which need the
2
+ // Reflect.metadata polyfill at import time.
3
+ import 'reflect-metadata';
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
+ import { CompositeKey, DatabaseProviderBase, EntityInfo, EntityPermissionType, Metadata, RowLevelSecurityFilterInfo, UserInfo } from '@memberjunction/core';
6
+ import type { DeleteSQLResult, IMetadataProvider, QueryExecutionSpec, RunQueryResult, SaveSQLResult } from '@memberjunction/core';
7
+ import { EntityRecordNameResolver } from '../resolvers/EntityRecordNameResolver.js';
8
+ import type { AppContext, UserPayload } from '../types.js';
9
+
10
+ /**
11
+ * The record-name queries against a real provider whose database only records what it is sent.
12
+ * The lookup the resolver causes carries the acting user's read row filter, binds the key value,
13
+ * refuses a key that is not the entity's primary key, and never runs without an acting user.
14
+ */
15
+
16
+ const CLIENTS_ID = 'E0000000-0000-0000-0000-0000000000C1';
17
+ const ACCOUNT_MANAGER_ROLE_ID = 'B0000000-0000-0000-0000-0000000000C1';
18
+ const OWN_CLIENTS_FILTER_ID = 'F0000000-0000-0000-0000-0000000000C1';
19
+ const MANAGER_ID = 'C0000000-0000-0000-0000-0000000000C1';
20
+ const CLIENT_ID_VALUE = '33333333-3333-3333-3333-333333333333';
21
+
22
+ const ROW_FILTERS: RowLevelSecurityFilterInfo[] = [
23
+ new RowLevelSecurityFilterInfo({ ID: OWN_CLIENTS_FILTER_ID, Name: 'Own clients', FilterText: "AccountManagerID = '{{UserID}}'" }),
24
+ ];
25
+
26
+ /** `Clients`: account managers read only the clients they manage. */
27
+ function clientsEntity(): EntityInfo {
28
+ return new EntityInfo({
29
+ ID: CLIENTS_ID, Name: 'Clients', SchemaName: 'dbo', BaseTable: 'Client', BaseView: 'vwClients',
30
+ Permissions: [
31
+ { ID: 'perm-manager', EntityID: CLIENTS_ID, RoleID: ACCOUNT_MANAGER_ROLE_ID, CanRead: true, ReadRLSFilterID: OWN_CLIENTS_FILTER_ID, Type: 'Allow' },
32
+ ],
33
+ Fields: [
34
+ { ID: 'cl-id', EntityID: CLIENTS_ID, Sequence: 1, Name: 'ID', Type: 'uniqueidentifier', IsPrimaryKey: true, NeedsQuotes: true },
35
+ { ID: 'cl-name', EntityID: CLIENTS_ID, Sequence: 2, Name: 'Name', Type: 'nvarchar', IsNameField: true, NeedsQuotes: true },
36
+ { ID: 'cl-tier', EntityID: CLIENTS_ID, Sequence: 3, Name: 'Tier', Type: 'int', NeedsQuotes: false },
37
+ { ID: 'cl-manager', EntityID: CLIENTS_ID, Sequence: 4, Name: 'AccountManagerID', Type: 'uniqueidentifier', NeedsQuotes: true },
38
+ ],
39
+ });
40
+ }
41
+
42
+ interface SentQuery {
43
+ SQL: string;
44
+ Parameters: unknown[] | undefined;
45
+ }
46
+
47
+ /** A SQL Server-shaped provider over `Clients`, whose database answers every query with one row. */
48
+ class RecordingProvider extends DatabaseProviderBase {
49
+ public Sent: SentQuery[] = [];
50
+ private readonly entities = [clientsEntity()];
51
+
52
+ public override get Entities(): EntityInfo[] {
53
+ return this.entities;
54
+ }
55
+
56
+ public override get RowLevelSecurityFilters(): RowLevelSecurityFilterInfo[] {
57
+ return ROW_FILTERS;
58
+ }
59
+
60
+ async ExecuteSQL<T>(sql: string, parameters?: unknown[]): Promise<Array<T>> {
61
+ this.Sent.push({ SQL: sql, Parameters: parameters });
62
+ return [{ Name: 'Acme Manufacturing' }] as T[];
63
+ }
64
+
65
+ public override BuildParameterPlaceholder(index: number): string { return `@p${index}`; }
66
+ protected get UUIDFunctionPattern(): RegExp { return /^newid\(\)$/i; }
67
+ protected get DBDefaultFunctionPattern(): RegExp { return /^getdate\(\)$/i; }
68
+ public QuoteIdentifier(name: string): string { return `[${name}]`; }
69
+ public QuoteSchemaAndView(schema: string, obj: string): string { return `[${schema}].[${obj}]`; }
70
+ protected BuildChildDiscoverySQL(): string { return ''; }
71
+ protected BuildHardLinkDependencySQL(): string { return ''; }
72
+ protected BuildSoftLinkDependencySQL(): string { return ''; }
73
+ protected async GenerateSaveSQL(): Promise<SaveSQLResult> { return { fullSQL: '' }; }
74
+ protected GenerateDeleteSQL(): DeleteSQLResult { return { fullSQL: '' }; }
75
+ protected BuildRecordChangeSQL(): { sql: string; parameters?: unknown[] } | null { return null; }
76
+ protected BuildSiblingRecordChangeSQL(): string { return ''; }
77
+ async BeginTransaction(): Promise<void> {}
78
+ async CommitTransaction(): Promise<void> {}
79
+ async RollbackTransaction(): Promise<void> {}
80
+ protected async InternalExecuteQueryFromSpec(_spec: QueryExecutionSpec): Promise<RunQueryResult> {
81
+ throw new Error('Not supported.');
82
+ }
83
+ }
84
+
85
+ const MANAGER = new UserInfo(null, {
86
+ ID: MANAGER_ID, Name: 'Morgan', Email: 'morgan@example.com', IsActive: true,
87
+ UserRoles: [{ UserID: MANAGER_ID, RoleID: ACCOUNT_MANAGER_ROLE_ID, Role: 'Account Managers' }],
88
+ });
89
+
90
+ function key(fieldName: string, value: string): CompositeKey {
91
+ return new CompositeKey([{ FieldName: fieldName, Value: value }]);
92
+ }
93
+
94
+ describe('EntityRecordNameResolver lookups under row-level security', () => {
95
+ let provider: RecordingProvider;
96
+ let savedProvider: IMetadataProvider;
97
+ let context: AppContext;
98
+ const resolver = new EntityRecordNameResolver();
99
+
100
+ beforeEach(() => {
101
+ provider = new RecordingProvider();
102
+ // Row filters resolve through the global provider.
103
+ savedProvider = Metadata.Provider;
104
+ Metadata.Provider = provider;
105
+ const userPayload: UserPayload = { email: MANAGER.Email, userRecord: MANAGER, sessionId: 'session-1' };
106
+ context = { dataSource: undefined, dataSources: [], providers: [{ provider, type: 'Read-Only' }], userPayload };
107
+ });
108
+
109
+ afterEach(() => {
110
+ Metadata.Provider = savedProvider;
111
+ });
112
+
113
+ function managerRowFilter(): string {
114
+ return provider.EntityByName('Clients').GetEffectiveRowFilterWhereClause(MANAGER, EntityPermissionType.Read, '');
115
+ }
116
+
117
+ it("looks the name up under the acting user's read row filter, with the key value bound", async () => {
118
+ const rowFilter = managerRowFilter();
119
+ expect(rowFilter).toContain(MANAGER_ID);
120
+
121
+ const result = await resolver.GetEntityRecordName('Clients', key('ID', CLIENT_ID_VALUE), context);
122
+
123
+ expect(result.Success).toBe(true);
124
+ expect(provider.Sent).toEqual([
125
+ { SQL: `SELECT [Name] FROM [dbo].[vwClients] WHERE [ID]=@p0 AND (${rowFilter})`, Parameters: [CLIENT_ID_VALUE] },
126
+ ]);
127
+ });
128
+
129
+ it('applies the row filter to each item of a batch', async () => {
130
+ const rowFilter = managerRowFilter();
131
+
132
+ const results = await resolver.GetEntityRecordNames([{ EntityName: 'Clients', CompositeKey: key('ID', CLIENT_ID_VALUE) }], context);
133
+
134
+ expect(results.map(r => r.Success)).toEqual([true]);
135
+ expect(provider.Sent.map(q => q.SQL)).toEqual([`SELECT [Name] FROM [dbo].[vwClients] WHERE [ID]=@p0 AND (${rowFilter})`]);
136
+ });
137
+
138
+ it('refuses a key field that is not a primary key, without querying', async () => {
139
+ const result = await resolver.GetEntityRecordName('Clients', key('Tier', '1'), context);
140
+
141
+ expect(result.Success).toBe(false);
142
+ expect(result.RecordName).toBeUndefined();
143
+ expect(provider.Sent).toHaveLength(0);
144
+ });
145
+
146
+ it('looks no name up when there is no acting user', async () => {
147
+ const result = await resolver.InnerGetEntityRecordName(provider, 'Clients', key('ID', CLIENT_ID_VALUE));
148
+
149
+ expect(result.Success).toBe(false);
150
+ expect(provider.Sent).toHaveLength(0);
151
+ });
152
+ });
@@ -22,6 +22,8 @@ vi.mock('type-graphql', () => {
22
22
  });
23
23
 
24
24
  const runViewCalls: Array<{ params: RunViewParams; contextUser: UserInfo | undefined }> = [];
25
+ /** Each RunViews call, as the list of entity names it batched. */
26
+ const runViewsBatches: string[][] = [];
25
27
  let runViewHandler: RunViewHandler = () => ({ Success: true, Results: [] });
26
28
 
27
29
  vi.mock('@memberjunction/core', async (importOriginal) => {
@@ -31,6 +33,10 @@ vi.mock('@memberjunction/core', async (importOriginal) => {
31
33
  runViewCalls.push({ params, contextUser });
32
34
  return runViewHandler(params, contextUser);
33
35
  }
36
+ async RunViews(params: RunViewParams[], contextUser?: UserInfo): Promise<ViewResult[]> {
37
+ runViewsBatches.push(params.map((p) => p.EntityName ?? ''));
38
+ return Promise.all(params.map((p) => this.RunView(p, contextUser)));
39
+ }
34
40
  }
35
41
  return { ...actual, RunView: MockRunView, LogStatus: () => undefined, LogError: () => undefined };
36
42
  });
@@ -123,6 +129,25 @@ const memberRows = [
123
129
  { ID: 'ccc', FirstName: 'Alan', LastName: null },
124
130
  ];
125
131
 
132
+ /**
133
+ * A fake `MJ: Entity Record Documents` view over `rows`: returns only the requested `Fields`, and
134
+ * only the rows named by an `ID IN ('a','b')` filter when one is given.
135
+ */
136
+ function erdView(rows: Array<Record<string, string>>) {
137
+ return (params: RunViewParams): ViewResult => {
138
+ const idList = /^ID IN \((.*)\)$/.exec(params.ExtraFilter ?? '');
139
+ const ids = idList ? new Set(idList[1].split(',').map((v) => v.trim().replace(/^'|'$/g, ''))) : undefined;
140
+ const picked = ids ? rows.filter((r) => ids.has(r.ID)) : rows;
141
+ const fields = params.Fields ?? Object.keys(rows[0] ?? {});
142
+ return { Success: true, Results: picked.map((r) => Object.fromEntries(fields.map((f) => [f, r[f]]))) };
143
+ };
144
+ }
145
+
146
+ /** The VectorJSON fetches: Entity Record Documents views filtered by ID. */
147
+ function vectorFetches() {
148
+ return callsFor('MJ: Entity Record Documents').filter((c) => c.params.ExtraFilter?.startsWith('ID IN ('));
149
+ }
150
+
126
151
  function callsFor(entityName: string) {
127
152
  return runViewCalls.filter((c) => c.params.EntityName === entityName);
128
153
  }
@@ -134,6 +159,7 @@ describe('FetchEntityVectorsResolver', () => {
134
159
 
135
160
  beforeEach(() => {
136
161
  runViewCalls.length = 0;
162
+ runViewsBatches.length = 0;
137
163
  });
138
164
 
139
165
  afterEach(() => {
@@ -150,7 +176,7 @@ describe('FetchEntityVectorsResolver', () => {
150
176
  it('reads VectorJSON for the document with the context user instead of calling QueryIndex', async () => {
151
177
  const { queryIndex } = useVectorDB(true);
152
178
  routeViews('Members', {
153
- 'MJ: Entity Record Documents': () => ({ Success: true, Results: storedRows }),
179
+ 'MJ: Entity Record Documents': erdView(storedRows),
154
180
  Members: () => ({ Success: true, Results: memberRows }),
155
181
  });
156
182
 
@@ -163,18 +189,38 @@ describe('FetchEntityVectorsResolver', () => {
163
189
  expect(result.Results.map((r) => r.ID)).toEqual(['ERD-1', 'ERD-2', 'ERD-3']);
164
190
  expect(result.Results.map((r) => r.Values)).toEqual([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]);
165
191
 
166
- const [erdCall] = callsFor('MJ: Entity Record Documents');
167
- expect(erdCall.contextUser).toBe(contextUser);
168
- expect(erdCall.params.ExtraFilter).toBe(`EntityDocumentID='${DOC_ID}' AND VectorJSON IS NOT NULL`);
169
- expect(erdCall.params.Fields).toEqual(['ID', 'RecordID', 'VectorJSON']);
170
- expect(erdCall.params.ResultType).toBe('simple');
171
- expect(erdCall.params.IgnoreMaxRows).toBe(true);
192
+ const [listCall, vectorCall] = callsFor('MJ: Entity Record Documents');
193
+ expect(listCall.contextUser).toBe(contextUser);
194
+ expect(listCall.params.ExtraFilter).toBe(`EntityDocumentID='${DOC_ID}' AND VectorJSON IS NOT NULL`);
195
+ expect(listCall.params.Fields).toEqual(['ID', 'RecordID']);
196
+ expect(listCall.params.ResultType).toBe('simple');
197
+ expect(listCall.params.IgnoreMaxRows).toBe(true);
198
+ expect(vectorCall.contextUser).toBe(contextUser);
199
+ expect(vectorCall.params.ExtraFilter).toBe("ID IN ('ERD-1','ERD-2','ERD-3')");
200
+ expect(vectorCall.params.Fields).toEqual(['ID', 'VectorJSON']);
201
+ expect(vectorCall.params.ResultType).toBe('simple');
202
+ expect(vectorCall.params.IgnoreMaxRows).toBe(true);
203
+ });
204
+
205
+ it('bypasses the server RunView cache for both Entity Record Documents views, so Fields is honoured', async () => {
206
+ useVectorDB(true);
207
+ routeViews('Members', {
208
+ 'MJ: Entity Record Documents': erdView(storedRows),
209
+ Members: () => ({ Success: true, Results: memberRows }),
210
+ });
211
+
212
+ await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, undefined, undefined, appContext(membersEntity));
213
+
214
+ const erdCalls = callsFor('MJ: Entity Record Documents');
215
+ expect(erdCalls).toHaveLength(2);
216
+ expect(erdCalls.map((c) => c.params.BypassCache)).toEqual([true, true]);
217
+ expect(callsFor('Members')[0].params.BypassCache).toBeUndefined();
172
218
  });
173
219
 
174
220
  it('labels each item with Entity, RecordID and the record name fields (RecordID matched case-insensitively)', async () => {
175
221
  useVectorDB(true);
176
222
  routeViews('Members', {
177
- 'MJ: Entity Record Documents': () => ({ Success: true, Results: storedRows }),
223
+ 'MJ: Entity Record Documents': erdView(storedRows),
178
224
  Members: () => ({ Success: true, Results: memberRows }),
179
225
  });
180
226
 
@@ -196,7 +242,7 @@ describe('FetchEntityVectorsResolver', () => {
196
242
  useVectorDB(true);
197
243
  const filter = "LastName = 'Hopper'";
198
244
  routeViews('Members', {
199
- 'MJ: Entity Record Documents': () => ({ Success: true, Results: storedRows }),
245
+ 'MJ: Entity Record Documents': erdView(storedRows),
200
246
  Members: (params) => ({
201
247
  Success: true,
202
248
  Results: params.ExtraFilter === filter ? memberRows.filter((r) => r.LastName === 'Hopper') : memberRows,
@@ -215,7 +261,7 @@ describe('FetchEntityVectorsResolver', () => {
215
261
  it('stops at maxRecords', async () => {
216
262
  useVectorDB(true);
217
263
  routeViews('Members', {
218
- 'MJ: Entity Record Documents': () => ({ Success: true, Results: storedRows }),
264
+ 'MJ: Entity Record Documents': erdView(storedRows),
219
265
  Members: () => ({ Success: true, Results: memberRows }),
220
266
  });
221
267
 
@@ -223,15 +269,74 @@ describe('FetchEntityVectorsResolver', () => {
223
269
 
224
270
  expect(result.Results.map((r) => r.ID)).toEqual(['ERD-1', 'ERD-2']);
225
271
  expect(result.TotalCount).toBe(2);
272
+ expect(vectorFetches().map((c) => c.params.ExtraFilter)).toEqual(["ID IN ('ERD-1','ERD-2')"]);
273
+ });
274
+
275
+ it('fetches VectorJSON only for vectors whose record passed the filter', async () => {
276
+ useVectorDB(true);
277
+ const filter = "LastName = 'Hopper'";
278
+ routeViews('Members', {
279
+ 'MJ: Entity Record Documents': erdView(storedRows),
280
+ Members: () => ({ Success: true, Results: memberRows.filter((r) => r.LastName === 'Hopper') }),
281
+ });
282
+
283
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, undefined, filter, appContext(membersEntity));
284
+
285
+ expect(result.Results.map((r) => r.Values)).toEqual([[0.3, 0.4]]);
286
+ expect(vectorFetches().map((c) => c.params.ExtraFilter)).toEqual(["ID IN ('ERD-2')"]);
287
+ });
288
+
289
+ it('runs no VectorJSON fetch when no stored vector belongs to a readable record', async () => {
290
+ useVectorDB(true);
291
+ routeViews('Members', {
292
+ 'MJ: Entity Record Documents': erdView(storedRows),
293
+ Members: () => ({ Success: true, Results: [] }),
294
+ });
295
+
296
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, undefined, undefined, appContext(membersEntity));
297
+
298
+ expect(result.Success).toBe(true);
299
+ expect(result.Results).toEqual([]);
300
+ expect(vectorFetches()).toHaveLength(0);
301
+ });
302
+
303
+ it('splits a large VectorJSON fetch into ID lists of at most 1000, sent in one RunViews batch', async () => {
304
+ useVectorDB(true);
305
+ const ids = Array.from({ length: 1500 }, (_, i) => `id-${i}`);
306
+ routeViews('Members', {
307
+ 'MJ: Entity Record Documents': erdView(ids.map((id, i) => ({ ID: `ERD-${i}`, RecordID: id, VectorJSON: `[${i}]` }))),
308
+ Members: () => ({ Success: true, Results: ids.map((id) => ({ ID: id, FirstName: id, LastName: null })) }),
309
+ });
310
+
311
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 1500, undefined, appContext(membersEntity));
312
+
313
+ expect(result.TotalCount).toBe(1500);
314
+ expect(result.Results[1499]).toMatchObject({ ID: 'ERD-1499', Values: [1499] });
315
+ const fetchSizes = vectorFetches().map((c) => (c.params.ExtraFilter ?? '').split(',').length);
316
+ expect(fetchSizes).toEqual([1000, 500]);
317
+ expect(runViewsBatches[1]).toEqual(['MJ: Entity Record Documents', 'MJ: Entity Record Documents']);
318
+ });
319
+
320
+ it('returns an error result when the VectorJSON fetch fails', async () => {
321
+ useVectorDB(true);
322
+ const listing = erdView(storedRows);
323
+ routeViews('Members', {
324
+ 'MJ: Entity Record Documents': (params) =>
325
+ params.ExtraFilter?.startsWith('ID IN (') ? { Success: false, Results: [], ErrorMessage: 'deadlock' } : listing(params),
326
+ Members: () => ({ Success: true, Results: memberRows }),
327
+ });
328
+
329
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, undefined, undefined, appContext(membersEntity));
330
+
331
+ expect(result.Success).toBe(false);
332
+ expect(result.Results).toEqual([]);
333
+ expect(result.ErrorMessage).toBe('Failed to load vector values: deadlock');
226
334
  });
227
335
 
228
336
  it('matches composite-key records by the compact RecordID segment and falls back to NameField for labels', async () => {
229
337
  useVectorDB(true);
230
338
  routeViews('Order Lines', {
231
- 'MJ: Entity Record Documents': () => ({
232
- Success: true,
233
- Results: [{ ID: 'ERD-9', RecordID: 'OrderID|o-1||LineNo|2', VectorJSON: '[1,0]' }],
234
- }),
339
+ 'MJ: Entity Record Documents': erdView([{ ID: 'ERD-9', RecordID: 'OrderID|o-1||LineNo|2', VectorJSON: '[1,0]' }]),
235
340
  'Order Lines': () => ({ Success: true, Results: [{ OrderID: 'o-1', LineNo: 2, Description: 'Brie wheel' }] }),
236
341
  });
237
342
 
@@ -250,7 +355,7 @@ describe('FetchEntityVectorsResolver', () => {
250
355
  it('returns an error result when the entity records cannot be loaded (e.g. an invalid filter)', async () => {
251
356
  useVectorDB(true);
252
357
  routeViews('Members', {
253
- 'MJ: Entity Record Documents': () => ({ Success: true, Results: storedRows }),
358
+ 'MJ: Entity Record Documents': erdView(storedRows),
254
359
  Members: () => ({ Success: false, Results: [], ErrorMessage: 'bad filter' }),
255
360
  });
256
361
 
@@ -260,6 +365,49 @@ describe('FetchEntityVectorsResolver', () => {
260
365
  expect(result.Results).toEqual([]);
261
366
  expect(result.ErrorMessage).toBe('Failed to load "Members" records: bad filter');
262
367
  });
368
+
369
+ it('returns an error result when the stored vectors cannot be loaded', async () => {
370
+ useVectorDB(true);
371
+ routeViews('Members', {
372
+ 'MJ: Entity Record Documents': () => ({ Success: false, Results: [], ErrorMessage: 'timeout' }),
373
+ Members: () => ({ Success: true, Results: memberRows }),
374
+ });
375
+
376
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, undefined, undefined, appContext(membersEntity));
377
+
378
+ expect(result.Success).toBe(false);
379
+ expect(result.Results).toEqual([]);
380
+ expect(result.TotalCount).toBe(0);
381
+ expect(result.ErrorMessage).toBe('Failed to load stored vectors: timeout');
382
+ });
383
+
384
+ it('returns an error result, and runs neither view, when the document names an entity missing from metadata', async () => {
385
+ const { queryIndex } = useVectorDB(true);
386
+ routeViews('Retired Entity', {
387
+ 'MJ: Entity Record Documents': erdView(storedRows),
388
+ });
389
+
390
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, undefined, undefined, appContext(membersEntity));
391
+
392
+ expect(result.Success).toBe(false);
393
+ expect(result.Results).toEqual([]);
394
+ expect(result.ErrorMessage).toBe('Entity "Retired Entity" not found in metadata');
395
+ expect(queryIndex).not.toHaveBeenCalled();
396
+ expect(callsFor('MJ: Entity Record Documents')).toHaveLength(0);
397
+ expect(runViewsBatches).toHaveLength(0);
398
+ });
399
+
400
+ it('lists the entity records and the stored vectors in one RunViews batch, then fetches the vector values', async () => {
401
+ useVectorDB(true);
402
+ routeViews('Members', {
403
+ 'MJ: Entity Record Documents': erdView(storedRows),
404
+ Members: () => ({ Success: true, Results: memberRows }),
405
+ });
406
+
407
+ await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, undefined, undefined, appContext(membersEntity));
408
+
409
+ expect(runViewsBatches).toEqual([['Members', 'MJ: Entity Record Documents'], ['MJ: Entity Record Documents']]);
410
+ });
263
411
  });
264
412
 
265
413
  describe('index-keyed providers (Pinecone path, unchanged)', () => {