@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.
- package/README.md +20 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
- package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
- package/dist/agentSessions/BridgeHostInstance.js +30 -0
- package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
- package/dist/agentSessions/HostInstance.d.ts +75 -11
- package/dist/agentSessions/HostInstance.d.ts.map +1 -1
- package/dist/agentSessions/HostInstance.js +99 -28
- package/dist/agentSessions/HostInstance.js.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +236 -27
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +682 -54
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +48 -3
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +112 -9
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +2 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +10 -3
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/apiRoot.d.ts +32 -0
- package/dist/apiRoot.d.ts.map +1 -0
- package/dist/apiRoot.js +67 -0
- package/dist/apiRoot.js.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.js +25 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
- package/dist/auth/LoginScreenEngine.d.ts +22 -0
- package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
- package/dist/auth/LoginScreenEngine.js +57 -0
- package/dist/auth/LoginScreenEngine.js.map +1 -0
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +4 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +6 -3
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.d.ts +10 -0
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +80 -11
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +2514 -22
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +166 -4
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +14 -11
- package/dist/context.js.map +1 -1
- package/dist/coreRoutes.d.ts +29 -0
- package/dist/coreRoutes.d.ts.map +1 -0
- package/dist/coreRoutes.js +142 -0
- package/dist/coreRoutes.js.map +1 -0
- package/dist/generated/graphql-schemas/__mj.d.ts +504 -3
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +2456 -7
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +59 -19
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/PushStatusResolver.js +1 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +123 -48
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +293 -305
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +16 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/globalCors.d.ts +33 -0
- package/dist/globalCors.d.ts.map +1 -0
- package/dist/globalCors.js +58 -0
- package/dist/globalCors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +219 -278
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +20 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +22 -0
- package/dist/isolatedProvider.js.map +1 -0
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +22 -2
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
- package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
- package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js +139 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js +99 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
- package/dist/realtimeSessions/index.d.ts +19 -0
- package/dist/realtimeSessions/index.d.ts.map +1 -0
- package/dist/realtimeSessions/index.js +19 -0
- package/dist/realtimeSessions/index.js.map +1 -0
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
- package/dist/realtimeSessions/sessionAccess.d.ts +92 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionAccess.js +94 -0
- package/dist/realtimeSessions/sessionAccess.js.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
- package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
- package/dist/realtimeSessions/verificationCore.d.ts +243 -0
- package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationCore.js +356 -0
- package/dist/realtimeSessions/verificationCore.js.map +1 -0
- package/dist/realtimeSessions/verificationPages.d.ts +66 -0
- package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPages.js +110 -0
- package/dist/realtimeSessions/verificationPages.js.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.js +249 -0
- package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.js +435 -0
- package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +21 -5
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.js +9 -7
- package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
- package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
- package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
- package/dist/resolvers/CacheStatsResolver.js +113 -2
- package/dist/resolvers/CacheStatsResolver.js.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
- package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.js +28 -10
- package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +24 -3
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +57 -8
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +3 -2
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +27 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +148 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +82 -11
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +279 -101
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +117 -38
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +6 -3
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.js +7 -1
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +9 -4
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +2 -0
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +13 -0
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +157 -33
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +404 -116
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +100 -14
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
- package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +6 -2
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +65 -13
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +102 -24
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
- package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
- package/dist/resolvers/UserAvatarResolver.js +124 -0
- package/dist/resolvers/UserAvatarResolver.js.map +1 -0
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +2 -17
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
- package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
- package/dist/resolvers/agent-run-data-guard.js +72 -0
- package/dist/resolvers/agent-run-data-guard.js.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.js +63 -0
- package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
- package/dist/resolvers/avatarInputValidation.d.ts +40 -0
- package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
- package/dist/resolvers/avatarInputValidation.js +97 -0
- package/dist/resolvers/avatarInputValidation.js.map +1 -0
- package/dist/resolvers/currentUserRoles.d.ts +29 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
- package/dist/resolvers/currentUserRoles.js +30 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.js +21 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
- package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
- package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
- package/dist/rest/RealtimeRecordingHandler.js +383 -0
- package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts +3 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +21 -12
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +6 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +6 -0
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/serverExtensionReservedRoots.d.ts +12 -10
- package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
- package/dist/serverExtensionReservedRoots.js +14 -17
- package/dist/serverExtensionReservedRoots.js.map +1 -1
- package/dist/services/MJStorageBlobStore.d.ts +8 -1
- package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
- package/dist/services/MJStorageBlobStore.js +25 -19
- package/dist/services/MJStorageBlobStore.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +2 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +5 -3
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -5
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +157 -9
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +57 -4
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +607 -18
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/sharedCache.d.ts +105 -0
- package/dist/sharedCache.d.ts.map +1 -0
- package/dist/sharedCache.js +279 -0
- package/dist/sharedCache.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +99 -96
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/BridgeHostInstance.test.ts +232 -0
- package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +433 -0
- package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/HostInstance.test.ts +84 -0
- package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
- package/src/__tests__/LoginScreenEngine.test.ts +86 -0
- package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
- package/src/__tests__/MeetingResolver.test.ts +75 -1
- package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
- package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
- package/src/__tests__/RealtimeRelay.test.ts +876 -0
- package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +212 -6
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
- package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +170 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +6 -4
- package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
- package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
- package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
- package/src/__tests__/SessionJanitor.test.ts +1203 -21
- package/src/__tests__/SessionManager.test.ts +240 -17
- package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
- package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
- package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +403 -22
- package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
- package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
- package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/UserAvatarResolver.test.ts +255 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/agent-run-data-guard.test.ts +130 -0
- package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
- package/src/__tests__/apiRootBoot.test.ts +202 -0
- package/src/__tests__/avatarInputValidation.test.ts +160 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
- package/src/__tests__/conversationRunEvents.test.ts +56 -14
- package/src/__tests__/globalCors.test.ts +217 -0
- package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
- package/src/__tests__/magicLinkService.consume.test.ts +6 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/newUsers.template.test.ts +151 -0
- package/src/__tests__/newUsers.test.ts +74 -5
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
- package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
- package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
- package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
- package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
- package/src/__tests__/realtimeSessions/helpers.ts +4 -0
- package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
- package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
- package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
- package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
- package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
- package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
- package/src/__tests__/resolverBase.fls.test.ts +87 -4
- package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
- package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -96
- package/src/__tests__/sharedCache.test.ts +312 -0
- package/src/__tests__/widgetGuestElevation.test.ts +44 -0
- package/src/agentSessions/BridgeHostInstance.ts +32 -0
- package/src/agentSessions/HostInstance.ts +124 -28
- package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
- package/src/agentSessions/SessionJanitor.ts +821 -59
- package/src/agentSessions/SessionManager.ts +138 -26
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +10 -3
- package/src/apiRoot.ts +78 -0
- package/src/auth/AuthProviderCatalogRouter.ts +26 -0
- package/src/auth/LoginScreenEngine.ts +50 -0
- package/src/auth/actionAuthorization.ts +116 -0
- package/src/auth/index.ts +4 -8
- package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
- package/src/auth/magicLink/MagicLinkService.ts +6 -3
- package/src/auth/newUsers.ts +99 -11
- package/src/config.ts +171 -5
- package/src/context.ts +15 -10
- package/src/coreRoutes.ts +166 -0
- package/src/generated/graphql-schemas/__mj.ts +1815 -57
- package/src/generic/CacheInvalidationResolver.ts +73 -18
- package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
- package/src/generic/PushStatusResolver.ts +1 -1
- package/src/generic/ResolverBase.ts +335 -326
- package/src/generic/RunViewResolver.ts +21 -0
- package/src/globalCors.ts +71 -0
- package/src/index.ts +231 -300
- package/src/isolatedProvider.ts +22 -0
- package/src/multiTenancy/index.ts +24 -2
- package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
- package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
- package/src/realtimeSessions/README.md +179 -0
- package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
- package/src/realtimeSessions/RealtimeSessionVerificationService.ts +168 -0
- package/src/realtimeSessions/RealtimeVerifyRouter.ts +109 -0
- package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
- package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
- package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
- package/src/realtimeSessions/index.ts +18 -0
- package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
- package/src/realtimeSessions/sessionAccess.ts +145 -0
- package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
- package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
- package/src/realtimeSessions/verificationCore.ts +502 -0
- package/src/realtimeSessions/verificationPages.ts +149 -0
- package/src/realtimeSessions/verificationPolicy.ts +321 -0
- package/src/realtimeSessions/verificationWorkflow.ts +702 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
- package/src/resolvers/ActionResolver.ts +84 -21
- package/src/resolvers/AdhocQueryResolver.ts +24 -6
- package/src/resolvers/ArtifactFileResolver.ts +9 -8
- package/src/resolvers/CacheStatsResolver.ts +89 -2
- package/src/resolvers/ClientToolRequestResolver.ts +25 -7
- package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +3 -2
- package/src/resolvers/FetchEntityVectorsResolver.ts +179 -2
- package/src/resolvers/FileResolver.ts +321 -106
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +6 -3
- package/src/resolvers/HumanHandoffResolver.ts +8 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
- package/src/resolvers/MCPResolver.ts +8 -5
- package/src/resolvers/QueryResolver.ts +11 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
- package/src/resolvers/RealtimeClientSessionResolver.ts +511 -129
- package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
- package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
- package/src/resolvers/RunAIAgentResolver.ts +116 -14
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
- package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
- package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
- package/src/resolvers/TestQuerySQLResolver.ts +68 -13
- package/src/resolvers/TransactionGroupResolver.ts +131 -27
- package/src/resolvers/UserAvatarResolver.ts +111 -0
- package/src/resolvers/UserResolver.ts +2 -16
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/agent-run-data-guard.ts +89 -0
- package/src/resolvers/agentVisionConsentAudit.ts +82 -0
- package/src/resolvers/avatarInputValidation.ts +123 -0
- package/src/resolvers/currentUserRoles.ts +32 -1
- package/src/resolvers/meetings/meetingMutations.ts +21 -0
- package/src/rest/RealtimeRecordingHandler.ts +464 -0
- package/src/rest/ViewOperationsHandler.ts +29 -13
- package/src/rest/setupRESTEndpoints.ts +6 -0
- package/src/serverExtensionReservedRoots.ts +14 -17
- package/src/services/MJStorageBlobStore.ts +31 -30
- package/src/services/StartTaskGraphDispatcher.ts +2 -2
- package/src/services/TaskGraphActionRunner.ts +25 -5
- package/src/services/TaskGraphAgentRunner.ts +188 -11
- package/src/services/TaskGraphContinuationDeliverer.ts +713 -17
- package/src/sharedCache.ts +305 -0
- package/src/types.ts +7 -0
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from '@memberjunction/core';
|
|
27
27
|
import { MJAuditLogEntity, MJErrorLogEntity, MJUserViewEntityExtended } from '@memberjunction/core-entities';
|
|
28
28
|
import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
|
|
29
|
-
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
29
|
+
import { ClientClauseScreen, UserCache, type EntityBaseViewAllowList } from '@memberjunction/generic-database-provider';
|
|
30
30
|
import { PubSubEngine, AuthorizationError } from 'type-graphql';
|
|
31
31
|
import { GraphQLError } from 'graphql';
|
|
32
32
|
import { RefusalExtensions } from './refusalExtensions.js';
|
|
@@ -38,15 +38,26 @@ import { RunViewGenericParams, UserPayload } from '../types.js';
|
|
|
38
38
|
import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './RunViewResolver.js';
|
|
39
39
|
import { DeleteOptionsInput } from './DeleteOptionsInput.js';
|
|
40
40
|
import { MJEvent, MJEventType, MJGlobal, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
41
|
-
import { SQLParser } from '@memberjunction/sql-parser';
|
|
42
|
-
import {
|
|
43
|
-
import { EncryptionEngine } from '@memberjunction/encryption';
|
|
41
|
+
import { SQLParser, IsBalancedSQLFragment, LexSQL } from '@memberjunction/sql-parser';
|
|
42
|
+
import type { SQLParserDialect } from '@memberjunction/sql-dialect';
|
|
43
|
+
import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
|
|
44
44
|
import { PUSH_STATUS_UPDATES_TOPIC, PublishStatusUpdate } from './PushStatusResolver.js';
|
|
45
45
|
import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
|
|
46
46
|
import { PubSubManager } from './PubSubManager.js';
|
|
47
47
|
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
48
48
|
import { Subscription } from 'rxjs';
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Entities whose update is always hydrated from the stored row, never from the client's `OldValues___`, though they
|
|
52
|
+
* do not track record changes: their server-side write rules compare against the prior state, which a client could
|
|
53
|
+
* otherwise supply itself (lower-case names). See `ResolverBase.MustLoadTruthFromDatabase`.
|
|
54
|
+
*/
|
|
55
|
+
const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW: ReadonlySet<string> = new Set([
|
|
56
|
+
'mj: record changes',
|
|
57
|
+
'mj: conversation details',
|
|
58
|
+
'mj: data context items',
|
|
59
|
+
]);
|
|
60
|
+
|
|
50
61
|
export class ResolverBase {
|
|
51
62
|
private static _emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null;
|
|
52
63
|
private static _cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {};
|
|
@@ -86,137 +97,158 @@ export class ResolverBase {
|
|
|
86
97
|
* @param contextUser - Optional user context for decryption (required for encrypted fields)
|
|
87
98
|
* @returns A new object in transport shape, or null when there is nothing to map
|
|
88
99
|
*/
|
|
89
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Precomputed per-entity / per-request field-mapping plan shared across rows in
|
|
102
|
+
* {@link MapFieldNamesToCodeNames} and {@link ArrayMapFieldNamesToCodeNames}.
|
|
103
|
+
*/
|
|
104
|
+
private buildFieldMappingPlan(
|
|
90
105
|
entityName: string,
|
|
91
|
-
dataObject: any,
|
|
92
106
|
contextUser?: UserInfo,
|
|
93
107
|
provider?: IMetadataProvider,
|
|
94
108
|
deniedReadFields?: Set<string>,
|
|
95
109
|
recordChangeProjector?: RecordChangeFieldSecurityProjector
|
|
96
|
-
):
|
|
97
|
-
|
|
110
|
+
): {
|
|
111
|
+
entityInfo: EntityInfo;
|
|
112
|
+
fieldsToProcess: Array<{ name: string; mappedFieldName: string; isDenied: boolean }>;
|
|
113
|
+
readableFields: string[] | null;
|
|
114
|
+
projector: RecordChangeFieldSecurityProjector | undefined;
|
|
115
|
+
encryptedFields: EntityFieldInfo[];
|
|
116
|
+
checkBinary: boolean;
|
|
117
|
+
} {
|
|
118
|
+
const md = provider ?? new Metadata();
|
|
119
|
+
const entityInfo = md.EntityByName(entityName);
|
|
120
|
+
if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
|
|
121
|
+
|
|
122
|
+
const denied = deniedReadFields ?? (
|
|
123
|
+
entityInfo.EnableFieldLevelSecurity && contextUser
|
|
124
|
+
? entityInfo.GetDeniedReadFields(contextUser)
|
|
125
|
+
: null
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const mapper = new FieldMapper();
|
|
129
|
+
const fieldsToProcess: Array<{ name: string; mappedFieldName: string; isDenied: boolean }> = [];
|
|
130
|
+
for (const f of entityInfo.Fields) {
|
|
131
|
+
const mappedFieldName = mapper.MapFieldName(f.CodeName);
|
|
132
|
+
const isDenied = Boolean(denied?.has(f.Name.trim().toLowerCase()));
|
|
133
|
+
if (isDenied || mappedFieldName !== f.Name) {
|
|
134
|
+
fieldsToProcess.push({ name: f.Name, mappedFieldName, isDenied });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const readableFields = this.BuildReadableFieldsTransportValue(entityInfo, denied);
|
|
139
|
+
const projector = contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)
|
|
140
|
+
? (recordChangeProjector ?? new RecordChangeFieldSecurityProjector(md, contextUser))
|
|
141
|
+
: undefined;
|
|
142
|
+
const encryptedFields = entityInfo.EncryptedFields ?? [];
|
|
143
|
+
const checkBinary = entityInfo.HasBinaryFields !== false;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
entityInfo,
|
|
147
|
+
fieldsToProcess,
|
|
148
|
+
readableFields,
|
|
149
|
+
projector,
|
|
150
|
+
encryptedFields,
|
|
151
|
+
checkBinary,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Applies synchronous field renaming, FLS stripping, readable-fields transport stamping, and
|
|
157
|
+
* Record Changes payload projection for a single row using a precomputed mapping plan.
|
|
158
|
+
*/
|
|
159
|
+
private mapRowSynchronously(
|
|
160
|
+
dataObject: Record<string, unknown> | null | undefined,
|
|
161
|
+
plan: ReturnType<ResolverBase['buildFieldMappingPlan']>
|
|
162
|
+
): Record<string, unknown> | null {
|
|
98
163
|
if (!dataObject || Object.keys(dataObject).length === 0) {
|
|
99
164
|
return null;
|
|
100
165
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// String scalar cannot serialize a Buffer and would fail the whole response.
|
|
105
|
-
dataObject = ReplaceByteArraysWithBase64({ ...dataObject });
|
|
106
|
-
|
|
107
|
-
// for the given entity name provided, check to see if there are any fields
|
|
108
|
-
// where the code name is different from the field name, and for just those
|
|
109
|
-
// fields, iterate through the dataObject and REPLACE the property that has the field name
|
|
110
|
-
// with the CodeName, because we can't transfer those via GraphQL as they are not
|
|
111
|
-
// valid property names in GraphQL
|
|
112
|
-
{
|
|
113
|
-
const md = provider ?? new Metadata();
|
|
114
|
-
const entityInfo = md.EntityByName(entityName);
|
|
115
|
-
if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
|
|
116
|
-
// const fields = entityInfo.Fields.filter((f) => f.Name !== f.CodeName || f.Name.startsWith('__mj_'));
|
|
117
|
-
// FIELD-LEVEL SECURITY — this is the authoritative read boundary for every GraphQL
|
|
118
|
-
// return path (single-record resolvers, external-data-source loads, and anything else
|
|
119
|
-
// routed through here), sitting exactly where encryption masking already lives.
|
|
120
|
-
//
|
|
121
|
-
// The denied set is normally computed ONCE per (entity, user) by the caller and passed
|
|
122
|
-
// in: this method runs once per ROW, so resolving it here would cost fields x rows
|
|
123
|
-
// aggregations. We fall back to computing it when a caller doesn't supply it (the
|
|
124
|
-
// single-record resolvers, where "once per row" and "once per request" are the same
|
|
125
|
-
// thing) rather than failing open — a missing argument must never mean missing security.
|
|
126
|
-
const denied = deniedReadFields ?? (
|
|
127
|
-
entityInfo.EnableFieldLevelSecurity && contextUser
|
|
128
|
-
? entityInfo.GetDeniedReadFields(contextUser)
|
|
129
|
-
: null
|
|
130
|
-
);
|
|
166
|
+
let row: Record<string, unknown> = plan.checkBinary
|
|
167
|
+
? ReplaceByteArraysWithBase64({ ...dataObject })
|
|
168
|
+
: { ...dataObject };
|
|
131
169
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// either way depending on whether mapping has already run.
|
|
142
|
-
delete dataObject[f.Name];
|
|
143
|
-
delete dataObject[mappedFieldName];
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (mappedFieldName !== f.Name) {
|
|
147
|
-
dataObject[mappedFieldName] = dataObject[f.Name];
|
|
148
|
-
delete dataObject[f.Name];
|
|
149
|
-
}
|
|
170
|
+
for (let i = 0; i < plan.fieldsToProcess.length; i++) {
|
|
171
|
+
const fp = plan.fieldsToProcess[i];
|
|
172
|
+
if (Object.prototype.hasOwnProperty.call(row, fp.name)) {
|
|
173
|
+
if (fp.isDenied) {
|
|
174
|
+
delete row[fp.name];
|
|
175
|
+
delete row[fp.mappedFieldName];
|
|
176
|
+
} else {
|
|
177
|
+
row[fp.mappedFieldName] = row[fp.name];
|
|
178
|
+
delete row[fp.name];
|
|
150
179
|
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// Field security, second half: say IN-BAND which fields this caller may read. Deleting the
|
|
154
|
-
// key above is not enough on its own — GraphQL emits every SELECTED field regardless, so a
|
|
155
|
-
// denied field the client asked for arrives as an explicit null it cannot tell apart from a
|
|
156
|
-
// genuine one. The client must not settle that from its own metadata: during the window
|
|
157
|
-
// after a permission change its copy disagrees with ours, and once metadata filtering lands
|
|
158
|
-
// (issue #3485) it may not hold the permission rows at all. Null for unrestricted callers,
|
|
159
|
-
// which is nearly every request.
|
|
160
|
-
const readableFields = this.BuildReadableFieldsTransportValue(entityInfo, denied);
|
|
161
|
-
if (readableFields) {
|
|
162
|
-
dataObject[ReadableFieldsTransportKey] = readableFields;
|
|
163
180
|
}
|
|
181
|
+
}
|
|
164
182
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// the projector is normally built ONCE per request by the array caller and passed in, and
|
|
173
|
-
// we build one here when it wasn't — the single-record resolvers, where once-per-row and
|
|
174
|
-
// once-per-request are the same thing.
|
|
175
|
-
if (contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)) {
|
|
176
|
-
const projector = recordChangeProjector ?? new RecordChangeFieldSecurityProjector(md, contextUser);
|
|
177
|
-
dataObject = projector.ProjectRow(dataObject);
|
|
178
|
-
}
|
|
183
|
+
if (plan.readableFields) {
|
|
184
|
+
row[ReadableFieldsTransportKey] = plan.readableFields;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (plan.projector) {
|
|
188
|
+
row = plan.projector.ProjectRow(row);
|
|
189
|
+
}
|
|
179
190
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
191
|
+
return row;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Decrypts or masks encrypted fields on a mapped row when `plan.encryptedFields.length > 0`.
|
|
196
|
+
*/
|
|
197
|
+
private async decryptRowEncryptedFields(
|
|
198
|
+
row: Record<string, unknown>,
|
|
199
|
+
encryptedFields: EntityFieldInfo[],
|
|
200
|
+
contextUser?: UserInfo
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
for (const field of encryptedFields) {
|
|
203
|
+
const fieldName = field.CodeName;
|
|
204
|
+
const value = row[fieldName];
|
|
205
|
+
|
|
206
|
+
if (value === null || value === undefined) continue;
|
|
207
|
+
|
|
208
|
+
const engine = EncryptionEngine.Instance;
|
|
209
|
+
await engine.Config(false, contextUser);
|
|
210
|
+
const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
|
|
211
|
+
if (typeof value === 'string' && IsValueEncrypted(value, keyMarker)) {
|
|
212
|
+
if (field.AllowDecryptInAPI) {
|
|
213
|
+
if (contextUser) {
|
|
214
|
+
try {
|
|
215
|
+
const decryptedValue = await engine.Decrypt(value, contextUser);
|
|
216
|
+
row[fieldName] = decryptedValue;
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (IsEncryptionKeyUnavailable(err)) {
|
|
219
|
+
ReportEncryptionKeyUnavailable(err);
|
|
206
220
|
} else {
|
|
207
|
-
|
|
208
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
221
|
+
LogError(`Failed to decrypt field ${fieldName} for API response: ${err}`);
|
|
209
222
|
}
|
|
210
|
-
|
|
211
|
-
// AllowDecryptInAPI=false and SendEncryptedValue=false - use sentinel
|
|
212
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
223
|
+
row[fieldName] = ENCRYPTED_SENTINEL;
|
|
213
224
|
}
|
|
214
|
-
|
|
225
|
+
} else {
|
|
226
|
+
row[fieldName] = ENCRYPTED_SENTINEL;
|
|
215
227
|
}
|
|
228
|
+
} else if (!field.SendEncryptedValue) {
|
|
229
|
+
row[fieldName] = ENCRYPTED_SENTINEL;
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
}
|
|
219
|
-
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
protected async MapFieldNamesToCodeNames(
|
|
236
|
+
entityName: string,
|
|
237
|
+
dataObject: any,
|
|
238
|
+
contextUser?: UserInfo,
|
|
239
|
+
provider?: IMetadataProvider,
|
|
240
|
+
deniedReadFields?: Set<string>,
|
|
241
|
+
recordChangeProjector?: RecordChangeFieldSecurityProjector
|
|
242
|
+
): Promise<any> {
|
|
243
|
+
if (!dataObject || Object.keys(dataObject).length === 0) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const plan = this.buildFieldMappingPlan(entityName, contextUser, provider, deniedReadFields, recordChangeProjector);
|
|
247
|
+
const mapped = this.mapRowSynchronously(dataObject as Record<string, unknown>, plan);
|
|
248
|
+
if (mapped && plan.encryptedFields.length > 0) {
|
|
249
|
+
await this.decryptRowEncryptedFields(mapped, plan.encryptedFields, contextUser);
|
|
250
|
+
}
|
|
251
|
+
return mapped;
|
|
220
252
|
}
|
|
221
253
|
|
|
222
254
|
/**
|
|
@@ -276,26 +308,17 @@ export class ResolverBase {
|
|
|
276
308
|
if (!dataObjectArray || dataObjectArray.length === 0) {
|
|
277
309
|
return dataObjectArray;
|
|
278
310
|
}
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// span many — so the projector, which owns that memo, must outlive the row loop. Built once
|
|
291
|
-
// here for the same reason the denied set above is: GetDeniedReadFields walks every field on
|
|
292
|
-
// an entity and must never be called per row.
|
|
293
|
-
const recordChangeProjector = contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)
|
|
294
|
-
? new RecordChangeFieldSecurityProjector(md, contextUser)
|
|
295
|
-
: undefined;
|
|
296
|
-
const mapped: any[] = [];
|
|
297
|
-
for (const element of dataObjectArray) {
|
|
298
|
-
mapped.push(await this.MapFieldNamesToCodeNames(entityName, element, contextUser, provider, deniedReadFields, recordChangeProjector));
|
|
311
|
+
// Precompute the entity lookup, FieldMapper plan, denied-fields set, readable-fields list,
|
|
312
|
+
// and Record Changes projector ONCE for the whole array rather than per row.
|
|
313
|
+
const plan = this.buildFieldMappingPlan(entityName, contextUser, provider);
|
|
314
|
+
const hasEncryptedFields = plan.encryptedFields.length > 0;
|
|
315
|
+
const mapped: Array<Record<string, unknown> | null> = new Array(dataObjectArray.length);
|
|
316
|
+
for (let i = 0; i < dataObjectArray.length; i++) {
|
|
317
|
+
const row = this.mapRowSynchronously(dataObjectArray[i] as Record<string, unknown>, plan);
|
|
318
|
+
if (row && hasEncryptedFields) {
|
|
319
|
+
await this.decryptRowEncryptedFields(row, plan.encryptedFields, contextUser);
|
|
320
|
+
}
|
|
321
|
+
mapped[i] = row;
|
|
299
322
|
}
|
|
300
323
|
return mapped;
|
|
301
324
|
}
|
|
@@ -362,13 +385,16 @@ export class ResolverBase {
|
|
|
362
385
|
* @param dataObject - The data object containing field values
|
|
363
386
|
* @param encryptionEngine - Optional encryption engine for re-encryption (lazy loaded if needed)
|
|
364
387
|
* @param contextUser - User context for encryption operations
|
|
388
|
+
* @param fieldKey - What the object's keys are: field CodeNames (GraphQL transport rows, the
|
|
389
|
+
* default) or field Names (raw provider rows, such as dataset item results)
|
|
365
390
|
* @returns The filtered data object
|
|
366
391
|
*/
|
|
367
392
|
protected async FilterEncryptedFieldsForAPI(
|
|
368
393
|
entityName: string,
|
|
369
394
|
dataObject: Record<string, unknown>,
|
|
370
395
|
contextUser: UserInfo,
|
|
371
|
-
provider?: IMetadataProvider
|
|
396
|
+
provider?: IMetadataProvider,
|
|
397
|
+
fieldKey: 'CodeName' | 'Name' = 'CodeName'
|
|
372
398
|
): Promise<Record<string, unknown>> {
|
|
373
399
|
if (!dataObject) return dataObject;
|
|
374
400
|
|
|
@@ -382,7 +408,7 @@ export class ResolverBase {
|
|
|
382
408
|
|
|
383
409
|
// Process each encrypted field
|
|
384
410
|
for (const field of encryptedFields) {
|
|
385
|
-
const fieldName = field.CodeName;
|
|
411
|
+
const fieldName = fieldKey === 'Name' ? field.Name : field.CodeName;
|
|
386
412
|
const value = dataObject[fieldName];
|
|
387
413
|
|
|
388
414
|
// Skip null/undefined values
|
|
@@ -423,13 +449,15 @@ export class ResolverBase {
|
|
|
423
449
|
}
|
|
424
450
|
|
|
425
451
|
/**
|
|
426
|
-
* Filters encrypted fields for an array of data objects
|
|
452
|
+
* Filters encrypted fields for an array of data objects. `fieldKey` is as for
|
|
453
|
+
* {@link FilterEncryptedFieldsForAPI}.
|
|
427
454
|
*/
|
|
428
455
|
protected async ArrayFilterEncryptedFieldsForAPI(
|
|
429
456
|
entityName: string,
|
|
430
457
|
dataObjectArray: Record<string, unknown>[],
|
|
431
458
|
contextUser: UserInfo,
|
|
432
|
-
provider?: IMetadataProvider
|
|
459
|
+
provider?: IMetadataProvider,
|
|
460
|
+
fieldKey: 'CodeName' | 'Name' = 'CodeName'
|
|
433
461
|
): Promise<Record<string, unknown>[]> {
|
|
434
462
|
if (!dataObjectArray || dataObjectArray.length === 0) return dataObjectArray;
|
|
435
463
|
|
|
@@ -443,7 +471,7 @@ export class ResolverBase {
|
|
|
443
471
|
|
|
444
472
|
// Process each element
|
|
445
473
|
for (const element of dataObjectArray) {
|
|
446
|
-
await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser, provider);
|
|
474
|
+
await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser, provider, fieldKey);
|
|
447
475
|
}
|
|
448
476
|
|
|
449
477
|
return dataObjectArray;
|
|
@@ -921,54 +949,67 @@ export class ResolverBase {
|
|
|
921
949
|
* This screen uses `@memberjunction/sql-parser` (same wrap as EDS `assertReadOnlyClause`):
|
|
922
950
|
* wrap the fragment as a single SELECT, fail closed if it does not parse as a read, then
|
|
923
951
|
* allow a FROM only when it is an entity **BaseView**. Base tables (`Meeting`, `__mj.User`)
|
|
924
|
-
* and catalogs are rejected. Server-internal RunView callers
|
|
952
|
+
* and catalogs are rejected. Server-internal RunView callers get only the provider's
|
|
953
|
+
* statement-separator check, which cannot refuse a legitimate filter.
|
|
954
|
+
*
|
|
955
|
+
* The screen is `ClientClauseScreen` in `@memberjunction/generic-database-provider`, shared
|
|
956
|
+
* with the REST view routes, ad-hoc list sources, and the provider's saved-view checks.
|
|
957
|
+
*
|
|
958
|
+
* Before that parse, the fragment must be balanced on its own (`IsBalancedSQLFragment`). The
|
|
959
|
+
* provider and the tenant hook splice it inside parentheses and AND their own filters after
|
|
960
|
+
* it, so a fragment that closes those parentheses (`1=1) OR (1=1`) would leave row-level
|
|
961
|
+
* security, API-key row filters and tenant scope on one side of an OR the caller controls.
|
|
962
|
+
* The parse wraps the fragment in parentheses too, which is why it cannot catch this alone.
|
|
963
|
+
* A backslash inside a quoted string or identifier is refused as well: the parser reads `\'`
|
|
964
|
+
* inside a plain literal as an escaped quote, but SQL Server and PostgreSQL end the literal
|
|
965
|
+
* there, so the text after it could be code that the parse-based checks never saw.
|
|
925
966
|
*
|
|
926
967
|
* RLS is applied by RunView as an outer WHERE around the entity being queried, not compiled
|
|
927
|
-
* into the view
|
|
928
|
-
*
|
|
968
|
+
* into the view, and RunView removes denied fields from its own result only. A subquery reads a
|
|
969
|
+
* base view directly with neither, so the screen refuses one over any entity the user reads under
|
|
970
|
+
* a row filter or with denied fields (`ClientClauseScreen.AssertUnscopedRead`), the queried
|
|
971
|
+
* entity included.
|
|
929
972
|
*/
|
|
930
973
|
protected assertClientClauseUsesEntityBaseViews(
|
|
931
974
|
clause: string | undefined | null,
|
|
932
975
|
label: string,
|
|
933
976
|
provider?: IMetadataProvider,
|
|
934
977
|
user?: UserInfo,
|
|
978
|
+
options?: { LeaveUnparseableToProvider?: boolean },
|
|
935
979
|
): void {
|
|
936
980
|
if (!clause?.trim()) return;
|
|
937
981
|
|
|
982
|
+
// The balance and backslash checks run before the shared screen: it parses the fragment
|
|
983
|
+
// inside its own parentheses, so it cannot see a fragment that closes the parentheses the
|
|
984
|
+
// provider and the tenant hook splice it into.
|
|
938
985
|
const dialect = this.dialectForProvider(provider);
|
|
939
986
|
if (SQLParser.HasStackedStatements(clause, dialect)) {
|
|
940
987
|
throw new Error(`Invalid ${label}: multiple statements are not permitted in client-supplied filters`);
|
|
941
988
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
label
|
|
945
|
-
? `SELECT 1 FROM __mj_clause_screen ORDER BY ${clause}`
|
|
946
|
-
: `SELECT 1 FROM __mj_clause_screen WHERE (${clause})`;
|
|
947
|
-
const parser = new SQLParser(wrapped, dialect);
|
|
948
|
-
if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
|
|
949
|
-
throw new Error(
|
|
950
|
-
`Invalid ${label}: not a safe read-only filter fragment — refusing under uncertainty`,
|
|
951
|
-
);
|
|
989
|
+
const balance = IsBalancedSQLFragment(clause, dialect);
|
|
990
|
+
if (!balance.IsBalanced) {
|
|
991
|
+
throw new Error(`Invalid ${label}: ${balance.Reason}`);
|
|
952
992
|
}
|
|
953
|
-
if (this.
|
|
954
|
-
throw new Error(`Invalid ${label}:
|
|
993
|
+
if (this.hasBackslashInQuotes(clause, dialect)) {
|
|
994
|
+
throw new Error(`Invalid ${label}: a backslash inside a quoted string or identifier is not permitted in client-supplied filters`);
|
|
955
995
|
}
|
|
956
996
|
|
|
957
|
-
|
|
958
|
-
const tables = SQLParser.ExtractTableRefs(wrapped, dialect);
|
|
959
|
-
for (const t of tables) {
|
|
960
|
-
const table = this.stripSqlIdent(t.TableName);
|
|
961
|
-
const schema = this.stripSqlIdent(t.SchemaName);
|
|
962
|
-
if (table.toLowerCase() === '__mj_clause_screen') continue;
|
|
963
|
-
this.assertTableRefReadable(allowed, schema, table, label, user);
|
|
964
|
-
}
|
|
997
|
+
ClientClauseScreen.AssertClauseUsesEntityBaseViews(clause, label, provider?.Entities ?? [], dialect, user, options);
|
|
965
998
|
}
|
|
966
999
|
|
|
967
1000
|
/**
|
|
968
1001
|
* SECURITY — screen for a FULL client-supplied SQL statement (ad-hoc query path). In addition
|
|
969
1002
|
* to the SELECT-only validation the caller performs, every table reference must resolve to an
|
|
970
|
-
* entity BaseView the acting user can read UNSCOPED.
|
|
971
|
-
*
|
|
1003
|
+
* entity BaseView the acting user can read UNSCOPED. A reference is excluded only when it reads a
|
|
1004
|
+
* CTE that is in scope there (`StatementRelation.IsCTEReference`): unqualified, declared by its
|
|
1005
|
+
* own SELECT or an enclosing one, and matched by the dialect's identifier rules. A name reserved
|
|
1006
|
+
* for the SQL parser is refused before parsing (`ClientClauseScreen.AssertNoIdentifierAliasNames`).
|
|
1007
|
+
* Fails closed: no user, or an unresolvable reference, refuses.
|
|
1008
|
+
*
|
|
1009
|
+
* The references come from a walk of the whole parse tree
|
|
1010
|
+
* (`ClientClauseScreen.CollectStatementReads`). The statement is refused when that walk does not
|
|
1011
|
+
* account for every SELECT in the text, or when a FROM item names no table (a table function or
|
|
1012
|
+
* a row list).
|
|
972
1013
|
*
|
|
973
1014
|
* Entity CanRead is not unscoped base-view read authority: RunView narrows a granted read with
|
|
974
1015
|
* row-level security and denied-field projection, and raw SQL applies neither. Rather than try
|
|
@@ -986,6 +1027,7 @@ export class ResolverBase {
|
|
|
986
1027
|
throw new Error(`Invalid ${label}: no acting user resolved for ad-hoc SQL — refusing`);
|
|
987
1028
|
}
|
|
988
1029
|
const dialect = this.dialectForProvider(provider);
|
|
1030
|
+
ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
|
|
989
1031
|
const parser = new SQLParser(sqlText, dialect);
|
|
990
1032
|
if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
|
|
991
1033
|
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
@@ -993,52 +1035,19 @@ export class ResolverBase {
|
|
|
993
1035
|
if (this.astContainsWriteNode(parser.AST)) {
|
|
994
1036
|
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
995
1037
|
}
|
|
996
|
-
const
|
|
997
|
-
|
|
1038
|
+
const reads = ClientClauseScreen.CollectStatementReads(parser);
|
|
1039
|
+
ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
|
|
998
1040
|
const allowed = this.entityBaseViewAllowList(provider);
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
if (this.isUnqualifiedSchema(schema) && cteNames.has(table.toLowerCase())) continue;
|
|
1004
|
-
const entity = this.assertTableRefReadable(allowed, schema, table, label, user);
|
|
1005
|
-
this.assertUnscopedRead(entity, user, label);
|
|
1041
|
+
for (const relation of reads.Relations) {
|
|
1042
|
+
if (relation.IsCTEReference) continue;
|
|
1043
|
+
const entity = ClientClauseScreen.AssertRelationReadable(allowed, relation, label, user);
|
|
1044
|
+
if (entity) this.assertUnscopedRead(entity, user, label);
|
|
1006
1045
|
}
|
|
1007
1046
|
}
|
|
1008
1047
|
|
|
1009
|
-
/** Refuses an entity whose read is narrowed for this user
|
|
1048
|
+
/** Refuses an entity whose read is narrowed for this user; see `ClientClauseScreen.AssertUnscopedRead`. */
|
|
1010
1049
|
private assertUnscopedRead(entity: EntityInfo, user: UserInfo, label: string): void {
|
|
1011
|
-
|
|
1012
|
-
throw new Error(
|
|
1013
|
-
`Invalid ${label}: entity '${entity.Name}' is row-level-security filtered for you — ad-hoc SQL cannot apply that filter; use RunView`,
|
|
1014
|
-
);
|
|
1015
|
-
}
|
|
1016
|
-
if (entity.GetDeniedReadFields(user).size > 0) {
|
|
1017
|
-
throw new Error(
|
|
1018
|
-
`Invalid ${label}: entity '${entity.Name}' has fields you are not permitted to read — ad-hoc SQL cannot project them away; use RunView`,
|
|
1019
|
-
);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/** Walks a parsed AST collecting the names of CTEs the statement itself defines. */
|
|
1024
|
-
private collectCTENames(node: unknown, names: Set<string>): void {
|
|
1025
|
-
if (!node || typeof node !== 'object') return;
|
|
1026
|
-
if (Array.isArray(node)) {
|
|
1027
|
-
for (const n of node) this.collectCTENames(n, names);
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
const obj = node as Record<string, unknown>;
|
|
1031
|
-
const withClause = obj.with;
|
|
1032
|
-
if (Array.isArray(withClause)) {
|
|
1033
|
-
for (const cte of withClause) {
|
|
1034
|
-
const name = (cte as Record<string, unknown>)?.name;
|
|
1035
|
-
if (typeof name === 'string') names.add(name.toLowerCase());
|
|
1036
|
-
else if (name && typeof name === 'object' && typeof (name as Record<string, unknown>).value === 'string') {
|
|
1037
|
-
names.add(((name as Record<string, unknown>).value as string).toLowerCase());
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
for (const v of Object.values(obj)) this.collectCTENames(v, names);
|
|
1050
|
+
ClientClauseScreen.AssertUnscopedRead(entity, user, label);
|
|
1042
1051
|
}
|
|
1043
1052
|
|
|
1044
1053
|
/**
|
|
@@ -1062,6 +1071,11 @@ export class ResolverBase {
|
|
|
1062
1071
|
* The single place that is not true is a field carrying `UserSearchParamFormatAPI`, whose
|
|
1063
1072
|
* admin-authored format may splice the term in unquoted; `createViewUserSearchSQL` re-applies
|
|
1064
1073
|
* `ValidateUserProvidedSQLClause` for exactly those entities.
|
|
1074
|
+
*
|
|
1075
|
+
* Each `Aggregates[].expression` gets the same screen, except that text the parser cannot read
|
|
1076
|
+
* goes on to the provider, which refuses it per aggregate. The provider never runs the caller's
|
|
1077
|
+
* aggregate text: it runs the SQL that `CheckAggregateExpression` rebuilds from a single
|
|
1078
|
+
* aggregate call over the entity's columns, and refuses everything else per aggregate.
|
|
1065
1079
|
*/
|
|
1066
1080
|
protected screenClientViewClauses(
|
|
1067
1081
|
clauses: {
|
|
@@ -1069,6 +1083,7 @@ export class ResolverBase {
|
|
|
1069
1083
|
orderBy?: string | null;
|
|
1070
1084
|
userSearchString?: string | null;
|
|
1071
1085
|
overrideExcludeFilter?: string | null;
|
|
1086
|
+
aggregates?: AggregateExpression[] | null;
|
|
1072
1087
|
},
|
|
1073
1088
|
provider?: IMetadataProvider,
|
|
1074
1089
|
user?: UserInfo,
|
|
@@ -1076,107 +1091,31 @@ export class ResolverBase {
|
|
|
1076
1091
|
this.assertClientClauseUsesEntityBaseViews(clauses.extraFilter, 'ExtraFilter', provider, user);
|
|
1077
1092
|
this.assertClientClauseUsesEntityBaseViews(clauses.orderBy, 'OrderBy', provider, user);
|
|
1078
1093
|
this.assertClientClauseUsesEntityBaseViews(clauses.overrideExcludeFilter, 'OverrideExcludeFilter', provider, user);
|
|
1094
|
+
for (const aggregate of clauses.aggregates ?? []) {
|
|
1095
|
+
this.assertClientClauseUsesEntityBaseViews(aggregate?.expression, 'Aggregate', provider, user, { LeaveUnparseableToProvider: true });
|
|
1096
|
+
}
|
|
1079
1097
|
}
|
|
1080
1098
|
|
|
1081
|
-
/**
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (typeof type === 'string' && ResolverBase.WRITE_NODE_TYPES.has(type.toLowerCase())) {
|
|
1088
|
-
return true;
|
|
1089
|
-
}
|
|
1090
|
-
return Object.values(obj).some((v) => this.astContainsWriteNode(v));
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
private static readonly WRITE_NODE_TYPES = new Set<string>([
|
|
1094
|
-
'insert', 'update', 'delete', 'merge', 'replace', 'drop', 'create', 'alter', 'truncate',
|
|
1095
|
-
'rename', 'call', 'exec', 'execute', 'grant', 'revoke', 'use', 'load', 'copy', 'do',
|
|
1096
|
-
]);
|
|
1097
|
-
|
|
1098
|
-
private dialectForProvider(provider?: IMetadataProvider): SQLParserDialect {
|
|
1099
|
-
const name = provider?.constructor?.name ?? '';
|
|
1100
|
-
if (/postgres/i.test(name)) return new PostgreSQLDialect();
|
|
1101
|
-
return new SQLServerDialect();
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
/** True for a reference with no schema — which `SQLParser.ExtractTableRefs` reports as `dbo`. */
|
|
1105
|
-
private isUnqualifiedSchema(schema: string): boolean {
|
|
1106
|
-
return !schema || schema.toLowerCase() === 'dbo';
|
|
1099
|
+
/**
|
|
1100
|
+
* True when a string literal or quoted identifier in `clause`, read the way the database reads
|
|
1101
|
+
* it, contains a backslash (see {@link assertClientClauseUsesEntityBaseViews}).
|
|
1102
|
+
*/
|
|
1103
|
+
private hasBackslashInQuotes(clause: string, dialect: SQLParserDialect): boolean {
|
|
1104
|
+
return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
|
|
1107
1105
|
}
|
|
1108
1106
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
return
|
|
1107
|
+
/** Write/DDL node walk shared with the clause screen; see `ClientClauseScreen.AstContainsWriteNode`. */
|
|
1108
|
+
private astContainsWriteNode(node: unknown): boolean {
|
|
1109
|
+
return ClientClauseScreen.AstContainsWriteNode(node);
|
|
1112
1110
|
}
|
|
1113
1111
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
* more than one schema — the database, not this screen, would decide which one runs.
|
|
1117
|
-
*/
|
|
1118
|
-
private entityBaseViewAllowList(provider?: IMetadataProvider): {
|
|
1119
|
-
qualified: Map<string, EntityInfo>;
|
|
1120
|
-
bare: Map<string, EntityInfo | null>;
|
|
1121
|
-
} {
|
|
1122
|
-
const qualified = new Map<string, EntityInfo>();
|
|
1123
|
-
const bare = new Map<string, EntityInfo | null>();
|
|
1124
|
-
const entities = provider?.Entities ?? [];
|
|
1125
|
-
for (const e of entities) {
|
|
1126
|
-
const view = this.stripSqlIdent(e.BaseView);
|
|
1127
|
-
if (!view) continue;
|
|
1128
|
-
const schema = this.stripSqlIdent(e.SchemaName);
|
|
1129
|
-
const bareKey = view.toLowerCase();
|
|
1130
|
-
bare.set(bareKey, bare.has(bareKey) ? null : e);
|
|
1131
|
-
if (schema) qualified.set(`${schema}.${view}`.toLowerCase(), e);
|
|
1132
|
-
}
|
|
1133
|
-
return { qualified, bare };
|
|
1112
|
+
private dialectForProvider(provider?: IMetadataProvider): SQLParserDialect {
|
|
1113
|
+
return ClientClauseScreen.DialectFor(provider);
|
|
1134
1114
|
}
|
|
1135
1115
|
|
|
1136
|
-
/**
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
* entity. Base views do not embed RLS and entity permissions are otherwise checked only on
|
|
1140
|
-
* the TOP entity of a request, so without this check a subquery (or ad-hoc query) could read
|
|
1141
|
-
* entities the caller has no read grant on.
|
|
1142
|
-
*
|
|
1143
|
-
* A schema-qualified reference must match that exact schema — `secret.vwFoo` never resolves
|
|
1144
|
-
* through another schema's `vwFoo`. An unqualified reference resolves only when the view name
|
|
1145
|
-
* is unique across schemas; an ambiguous one must be qualified, because the database's
|
|
1146
|
-
* default-schema resolution, not this screen, would pick which view actually runs.
|
|
1147
|
-
*
|
|
1148
|
-
* `SQLParser.ExtractTableRefs` reports an unqualified reference as schema `dbo`, so `dbo` is
|
|
1149
|
-
* indistinguishable from "no schema" here and both take the unqualified path (an exact
|
|
1150
|
-
* `dbo.<view>` entity wins first).
|
|
1151
|
-
*/
|
|
1152
|
-
private assertTableRefReadable(
|
|
1153
|
-
allowed: { qualified: Map<string, EntityInfo>; bare: Map<string, EntityInfo | null> },
|
|
1154
|
-
schema: string,
|
|
1155
|
-
table: string,
|
|
1156
|
-
label: string,
|
|
1157
|
-
user?: UserInfo,
|
|
1158
|
-
): EntityInfo {
|
|
1159
|
-
const qualified = allowed.qualified.get(`${schema}.${table}`.toLowerCase());
|
|
1160
|
-
const entity = this.isUnqualifiedSchema(schema) ? qualified ?? allowed.bare.get(table.toLowerCase()) : qualified;
|
|
1161
|
-
if (entity === null) {
|
|
1162
|
-
throw new Error(
|
|
1163
|
-
`Invalid ${label}: '${table}' is a base view in more than one schema — qualify it with its schema`,
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
if (!entity) {
|
|
1167
|
-
throw new Error(
|
|
1168
|
-
`Invalid ${label}: subquery must use an entity base view, not '${schema ? schema + '.' : ''}${table}'`,
|
|
1169
|
-
);
|
|
1170
|
-
}
|
|
1171
|
-
if (user) {
|
|
1172
|
-
const perms = entity.GetUserPermisions(user);
|
|
1173
|
-
if (!perms.CanRead) {
|
|
1174
|
-
throw new Error(
|
|
1175
|
-
`Invalid ${label}: you do not have read permission on entity '${entity.Name}' referenced by '${schema ? schema + '.' : ''}${table}'`,
|
|
1176
|
-
);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
return entity;
|
|
1116
|
+
/** The entity base-view allow-list for a provider's entities. */
|
|
1117
|
+
private entityBaseViewAllowList(provider?: IMetadataProvider): EntityBaseViewAllowList {
|
|
1118
|
+
return ClientClauseScreen.BuildEntityBaseViewAllowList(provider?.Entities ?? []);
|
|
1180
1119
|
}
|
|
1181
1120
|
|
|
1182
1121
|
/**
|
|
@@ -1223,7 +1162,7 @@ export class ResolverBase {
|
|
|
1223
1162
|
// Screen at this boundary: parse, reject writes, allow only entity BaseViews the
|
|
1224
1163
|
// acting user can read (subqueries do not inherit the top entity's permission check).
|
|
1225
1164
|
this.screenClientViewClauses(
|
|
1226
|
-
{ extraFilter, orderBy, userSearchString, overrideExcludeFilter },
|
|
1165
|
+
{ extraFilter, orderBy, userSearchString, overrideExcludeFilter, aggregates },
|
|
1227
1166
|
provider as unknown as IMetadataProvider,
|
|
1228
1167
|
user,
|
|
1229
1168
|
);
|
|
@@ -1390,6 +1329,7 @@ export class ResolverBase {
|
|
|
1390
1329
|
orderBy: param.orderBy,
|
|
1391
1330
|
userSearchString: param.userSearchString,
|
|
1392
1331
|
overrideExcludeFilter: param.overrideExcludeFilter,
|
|
1332
|
+
aggregates: param.aggregates,
|
|
1393
1333
|
},
|
|
1394
1334
|
md,
|
|
1395
1335
|
contextUser ?? undefined,
|
|
@@ -1818,9 +1758,10 @@ export class ResolverBase {
|
|
|
1818
1758
|
// that hydrates from those values — never loading what the database actually holds.
|
|
1819
1759
|
const hasNarrowedAuditPayload = this.StripRecordChangePayloadFromClientInput(entityInfo, userInfo, input, clientNewValues);
|
|
1820
1760
|
|
|
1821
|
-
if (this.MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload)) {
|
|
1761
|
+
if (this.MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload, userInfo)) {
|
|
1822
1762
|
// We get here because the entity tracks record changes, OR the client did not provide OldValues,
|
|
1823
|
-
// OR field-level security is in play — in every case we need the true
|
|
1763
|
+
// OR field-level security or an Update row filter is in play — in every case we need the true
|
|
1764
|
+
// old values from the DB
|
|
1824
1765
|
const cKey = new CompositeKey(
|
|
1825
1766
|
entityInfo.PrimaryKeys.map((pk) => {
|
|
1826
1767
|
return {
|
|
@@ -1894,13 +1835,41 @@ export class ResolverBase {
|
|
|
1894
1835
|
});
|
|
1895
1836
|
}
|
|
1896
1837
|
|
|
1838
|
+
/**
|
|
1839
|
+
* Checks whether `oldValues` (`input.OldValues___`) contains every writable in-table field of
|
|
1840
|
+
* `entityInfo` (plus primary keys).
|
|
1841
|
+
*
|
|
1842
|
+
* With the delta-only `Save()` wire protocol, `GraphQLDataProvider.Save()` sends only primary keys
|
|
1843
|
+
* and dirty writable fields in `vars.input` and `OldValues___` on normal updates. Hydrating an
|
|
1844
|
+
* entity directly from a partial-delta `OldValues___` via `entityObject.LoadFromData(oldValues)`
|
|
1845
|
+
* (without first loading the authoritative row via `InnerLoad`) would leave every untouched column
|
|
1846
|
+
* `NotLoaded = true` (or `null`), causing `spUpdate` or custom `Validate()` hooks to see incomplete
|
|
1847
|
+
* state. Returning `false` for partial deltas ensures {@link MustLoadTruthFromDatabase} routes
|
|
1848
|
+
* delta updates through `InnerLoad(cKey)` + {@link TestAndSetClientOldValuesToDBValues}, even on
|
|
1849
|
+
* entities with `TrackRecordChanges === false`.
|
|
1850
|
+
*/
|
|
1851
|
+
protected hasCompleteOldValues(
|
|
1852
|
+
entityInfo: EntityInfo,
|
|
1853
|
+
oldValues?: Array<{ Key: string; Value: unknown }> | null
|
|
1854
|
+
): boolean {
|
|
1855
|
+
if (!oldValues || oldValues.length === 0) {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
const providedKeys = new Set(oldValues.map((kv) => kv.Key));
|
|
1859
|
+
return entityInfo.Fields.every(
|
|
1860
|
+
(f) => (!f.IsPrimaryKey && (f.ReadOnly || f.IsVirtual)) || providedKeys.has(f.Name) || providedKeys.has(f.CodeName)
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1897
1864
|
/**
|
|
1898
1865
|
* Whether `UpdateRecord` must hydrate the entity from the DATABASE rather than from the
|
|
1899
1866
|
* client's `OldValues___`.
|
|
1900
1867
|
*
|
|
1901
|
-
* The `OldValues___` path exists as an optimization
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1868
|
+
* The `OldValues___` path exists as an optimization when a client supplies the complete pre-save
|
|
1869
|
+
* record (e.g. `IgnoreDirtyState: true`) and nothing else requires the database's authoritative prior
|
|
1870
|
+
* state. Under the delta-only `Save()` wire protocol, normal updates send only primary keys and dirty
|
|
1871
|
+
* writable fields in `OldValues___`, so {@link hasCompleteOldValues} returns `false` and forces the
|
|
1872
|
+
* database load (`InnerLoad`) so untouched columns retain their stored values.
|
|
1904
1873
|
*
|
|
1905
1874
|
* `EnableFieldLevelSecurity` is the security-critical one, and it is NOT redundant with
|
|
1906
1875
|
* `hasDeniedReadFields`. That flag reports READ denials, and the canonical FLS configuration is
|
|
@@ -1918,27 +1887,48 @@ export class ResolverBase {
|
|
|
1918
1887
|
* it never applies them to the entity. An update-denied field can then only become dirty by being
|
|
1919
1888
|
* named in the mutation input itself, which is precisely the case that check does catch.
|
|
1920
1889
|
*
|
|
1921
|
-
*
|
|
1922
|
-
*
|
|
1890
|
+
* The flag comes after the cheap terms, so the extra load lands only on entities that have the
|
|
1891
|
+
* feature switched on, which is almost none of them.
|
|
1923
1892
|
*
|
|
1924
1893
|
* `MJ: Record Changes` always loads from the database: its server class allows an update only when
|
|
1925
1894
|
* Comments is the one dirty field, and "dirty" compared against client-supplied OldValues lets a
|
|
1926
1895
|
* caller pin forged audit columns as both old and new values. It doesn't track its own changes, so
|
|
1927
1896
|
* nothing else forces the load.
|
|
1897
|
+
*
|
|
1898
|
+
* `MJ: Data Context Items` always loads from the database for the same reason: its server class
|
|
1899
|
+
* lets only an administrator save an item that holds SQL, and decides that from the item's `Type`
|
|
1900
|
+
* and `SQL`. Hydrated from OldValues, a client could leave both out, and the stored procedure would
|
|
1901
|
+
* keep the stored SQL item while the rule saw none.
|
|
1902
|
+
*
|
|
1903
|
+
* `MJ: Conversation Details` always loads for the same reason: its write gate decides from the stored
|
|
1904
|
+
* row (who wrote a person's message, its role, the owner's rating), and an OldValues-hydrated entity
|
|
1905
|
+
* would let a grantee claim to be a message's author while rewriting it. See
|
|
1906
|
+
* {@link ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW}.
|
|
1907
|
+
*
|
|
1908
|
+
* A caller under an Update row filter (a role's Update RLS filter or an API-key row filter) always
|
|
1909
|
+
* loads from the database too (#4919). The save's post-image check skips its query when no column
|
|
1910
|
+
* the filter reads is dirty, and dirtiness measured against client OldValues is the caller's to
|
|
1911
|
+
* choose: claiming the new value of a filter column as its old value made it clean, so the update
|
|
1912
|
+
* moved the row outside the caller's scope without the check ever running. Evaluated last: it
|
|
1913
|
+
* builds the filter clause, and only callers with such a filter pay for the load. A missing user
|
|
1914
|
+
* (a user-cache miss) also loads: with no user the filter can't be ruled out.
|
|
1928
1915
|
*/
|
|
1929
1916
|
protected MustLoadTruthFromDatabase(
|
|
1930
1917
|
entityInfo: EntityInfo,
|
|
1931
1918
|
input: { OldValues___?: Array<{ Key: string; Value: unknown }> },
|
|
1932
1919
|
hasDeniedReadFields: boolean,
|
|
1933
|
-
hasNarrowedAuditPayload: boolean
|
|
1920
|
+
hasNarrowedAuditPayload: boolean,
|
|
1921
|
+
user: UserInfo | undefined
|
|
1934
1922
|
): boolean {
|
|
1935
1923
|
return (
|
|
1936
1924
|
entityInfo.TrackRecordChanges ||
|
|
1937
|
-
entityInfo.Name.trim().toLowerCase()
|
|
1938
|
-
!input.OldValues___ ||
|
|
1925
|
+
ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW.has(entityInfo.Name.trim().toLowerCase()) ||
|
|
1926
|
+
!this.hasCompleteOldValues(entityInfo, input.OldValues___) ||
|
|
1939
1927
|
hasDeniedReadFields ||
|
|
1940
1928
|
hasNarrowedAuditPayload ||
|
|
1941
|
-
entityInfo.EnableFieldLevelSecurity
|
|
1929
|
+
entityInfo.EnableFieldLevelSecurity ||
|
|
1930
|
+
!user ||
|
|
1931
|
+
entityInfo.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Update, '').length > 0
|
|
1942
1932
|
);
|
|
1943
1933
|
}
|
|
1944
1934
|
|
|
@@ -2107,19 +2097,36 @@ export class ResolverBase {
|
|
|
2107
2097
|
}
|
|
2108
2098
|
|
|
2109
2099
|
/**
|
|
2110
|
-
*
|
|
2111
|
-
*
|
|
2112
|
-
*
|
|
2100
|
+
* Compares the client-supplied `OldValues___` array in `input` against the authoritative database
|
|
2101
|
+
* values just loaded onto `entityObject` via `InnerLoad`, checks for overlapping concurrent field
|
|
2102
|
+
* modifications, and then applies `clientNewValues` onto `entityObject` via `SetMany`.
|
|
2113
2103
|
*
|
|
2114
|
-
*
|
|
2104
|
+
* Supports both full-record and delta-only `OldValues___` payloads: only keys present in
|
|
2105
|
+
* `input.OldValues___` (e.g. primary keys + dirty writable fields) are compared against `dbValues`,
|
|
2106
|
+
* and `entityObject.SetMany(clientNewValues)` mutates only the fields sent in `clientNewValues`
|
|
2107
|
+
* while preserving all untouched DB-loaded fields on `entityObject`.
|
|
2108
|
+
*
|
|
2109
|
+
* ASSUMES: `input` has an `OldValues___` property that is an array of Key/Value pairs representing
|
|
2110
|
+
* the pre-mutation values of the fields the client is updating.
|
|
2115
2111
|
*/
|
|
2112
|
+
/**
|
|
2113
|
+
* Resolves a field on `entityInfo` by `CodeName` or `Name` in $O(1)$ via `FieldByCodeName` /
|
|
2114
|
+
* `FieldByName` when available on `EntityInfo`, falling back to `.Fields.find()` for test stubs.
|
|
2115
|
+
*/
|
|
2116
|
+
private findEntityFieldByCodeNameOrName(entityInfo: EntityInfo, key: string): EntityFieldInfo | undefined {
|
|
2117
|
+
if (typeof entityInfo.FieldByCodeName === 'function') {
|
|
2118
|
+
return entityInfo.FieldByCodeName(key) ?? entityInfo.FieldByName(key);
|
|
2119
|
+
}
|
|
2120
|
+
return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2116
2123
|
protected async TestAndSetClientOldValuesToDBValues(input: any, clientNewValues: any, entityObject: BaseEntity, contextUser: UserInfo) {
|
|
2117
2124
|
// we have OldValues, so we need to compare them to the values we just loaded from the DB
|
|
2118
2125
|
const clientOldValues = {};
|
|
2119
2126
|
// for each item in the oldValues array, add it to the clientOldValues object
|
|
2120
2127
|
input.OldValues___.forEach((item) => {
|
|
2121
2128
|
// 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
|
|
2122
|
-
const field = entityObject.EntityInfo
|
|
2129
|
+
const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
|
|
2123
2130
|
const val = this.ClientOldValueToFieldValue(field, item.Value);
|
|
2124
2131
|
clientOldValues[item.Key] = val;
|
|
2125
2132
|
});
|
|
@@ -2131,19 +2138,20 @@ export class ResolverBase {
|
|
|
2131
2138
|
// 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
|
|
2132
2139
|
const dbDifferences = [];
|
|
2133
2140
|
Object.keys(clientOldValues).forEach((key) => {
|
|
2134
|
-
const f = entityObject.EntityInfo
|
|
2141
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
2142
|
+
const dbVal = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
|
|
2135
2143
|
let different = false;
|
|
2136
2144
|
switch (typeof clientOldValues[key]) {
|
|
2137
2145
|
case 'number':
|
|
2138
|
-
different = clientOldValues[key] !==
|
|
2146
|
+
different = clientOldValues[key] !== dbVal;
|
|
2139
2147
|
break;
|
|
2140
2148
|
case 'boolean':
|
|
2141
|
-
different = clientOldValues[key] !==
|
|
2149
|
+
different = clientOldValues[key] !== dbVal;
|
|
2142
2150
|
break;
|
|
2143
2151
|
case 'object':
|
|
2144
2152
|
if (clientOldValues[key] instanceof Date) {
|
|
2145
2153
|
const clientDate = clientOldValues[key] as Date;
|
|
2146
|
-
const dbDate =
|
|
2154
|
+
const dbDate = dbVal;
|
|
2147
2155
|
if (dbDate == null || !(dbDate instanceof Date)) {
|
|
2148
2156
|
different = true;
|
|
2149
2157
|
} else if (clientDate.getTime() !== dbDate.getTime()) {
|
|
@@ -2161,11 +2169,11 @@ export class ResolverBase {
|
|
|
2161
2169
|
}
|
|
2162
2170
|
}
|
|
2163
2171
|
} else if (clientOldValues[key] === null) {
|
|
2164
|
-
different =
|
|
2172
|
+
different = dbVal !== null;
|
|
2165
2173
|
}
|
|
2166
2174
|
break;
|
|
2167
2175
|
default:
|
|
2168
|
-
different = clientOldValues[key] !==
|
|
2176
|
+
different = clientOldValues[key] !== dbVal;
|
|
2169
2177
|
break;
|
|
2170
2178
|
}
|
|
2171
2179
|
if (different && f && !f.ReadOnly) {
|
|
@@ -2173,7 +2181,7 @@ export class ResolverBase {
|
|
|
2173
2181
|
dbDifferences.push({
|
|
2174
2182
|
FieldName: key,
|
|
2175
2183
|
ClientOldValue: clientOldValues[key],
|
|
2176
|
-
DBValue:
|
|
2184
|
+
DBValue: dbVal,
|
|
2177
2185
|
});
|
|
2178
2186
|
}
|
|
2179
2187
|
});
|
|
@@ -2183,8 +2191,8 @@ export class ResolverBase {
|
|
|
2183
2191
|
// 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
|
|
2184
2192
|
const clientDifferences = [];
|
|
2185
2193
|
Object.keys(clientOldValues).forEach((key) => {
|
|
2186
|
-
const f = entityObject.EntityInfo
|
|
2187
|
-
if (clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
|
|
2194
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
2195
|
+
if (key in clientNewValues && clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
|
|
2188
2196
|
// only include updateable fields
|
|
2189
2197
|
clientDifferences.push({
|
|
2190
2198
|
FieldName: key,
|
|
@@ -2196,7 +2204,8 @@ export class ResolverBase {
|
|
|
2196
2204
|
|
|
2197
2205
|
// 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
|
|
2198
2206
|
// 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
|
|
2199
|
-
const
|
|
2207
|
+
const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
|
|
2208
|
+
const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
|
|
2200
2209
|
if (overlap.length > 0) {
|
|
2201
2210
|
const msg = {
|
|
2202
2211
|
Message:
|