@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, EntityPermissionType, KeyValuePair, LogDebug, LogError, LogStatus, Metadata, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
|
|
2
|
-
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
2
|
+
import { ClientClauseScreen, UserCache } from '@memberjunction/generic-database-provider';
|
|
3
3
|
import { AuthorizationError } from 'type-graphql';
|
|
4
4
|
import { GraphQLError } from 'graphql';
|
|
5
5
|
import { RefusalExtensions } from './refusalExtensions.js';
|
|
@@ -7,13 +7,22 @@ import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
|
7
7
|
import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
|
|
8
8
|
import { DeleteOptionsInput } from './DeleteOptionsInput.js';
|
|
9
9
|
import { MJEventType, MJGlobal, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
10
|
-
import { SQLParser } from '@memberjunction/sql-parser';
|
|
11
|
-
import {
|
|
12
|
-
import { EncryptionEngine } from '@memberjunction/encryption';
|
|
10
|
+
import { SQLParser, IsBalancedSQLFragment, LexSQL } from '@memberjunction/sql-parser';
|
|
11
|
+
import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
|
|
13
12
|
import { PublishStatusUpdate } from './PushStatusResolver.js';
|
|
14
13
|
import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
|
|
15
14
|
import { PubSubManager } from './PubSubManager.js';
|
|
16
15
|
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
16
|
+
/**
|
|
17
|
+
* Entities whose update is always hydrated from the stored row, never from the client's `OldValues___`, though they
|
|
18
|
+
* do not track record changes: their server-side write rules compare against the prior state, which a client could
|
|
19
|
+
* otherwise supply itself (lower-case names). See `ResolverBase.MustLoadTruthFromDatabase`.
|
|
20
|
+
*/
|
|
21
|
+
const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW = new Set([
|
|
22
|
+
'mj: record changes',
|
|
23
|
+
'mj: conversation details',
|
|
24
|
+
'mj: data context items',
|
|
25
|
+
]);
|
|
17
26
|
export class ResolverBase {
|
|
18
27
|
static { this._emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null; }
|
|
19
28
|
static { this._cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {}; }
|
|
@@ -51,126 +60,123 @@ export class ResolverBase {
|
|
|
51
60
|
* @param contextUser - Optional user context for decryption (required for encrypted fields)
|
|
52
61
|
* @returns A new object in transport shape, or null when there is nothing to map
|
|
53
62
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Precomputed per-entity / per-request field-mapping plan shared across rows in
|
|
65
|
+
* {@link MapFieldNamesToCodeNames} and {@link ArrayMapFieldNamesToCodeNames}.
|
|
66
|
+
*/
|
|
67
|
+
buildFieldMappingPlan(entityName, contextUser, provider, deniedReadFields, recordChangeProjector) {
|
|
68
|
+
const md = provider ?? new Metadata();
|
|
69
|
+
const entityInfo = md.EntityByName(entityName);
|
|
70
|
+
if (!entityInfo)
|
|
71
|
+
throw new Error(`Entity ${entityName} not found in metadata`);
|
|
72
|
+
const denied = deniedReadFields ?? (entityInfo.EnableFieldLevelSecurity && contextUser
|
|
73
|
+
? entityInfo.GetDeniedReadFields(contextUser)
|
|
74
|
+
: null);
|
|
75
|
+
const mapper = new FieldMapper();
|
|
76
|
+
const fieldsToProcess = [];
|
|
77
|
+
for (const f of entityInfo.Fields) {
|
|
78
|
+
const mappedFieldName = mapper.MapFieldName(f.CodeName);
|
|
79
|
+
const isDenied = Boolean(denied?.has(f.Name.trim().toLowerCase()));
|
|
80
|
+
if (isDenied || mappedFieldName !== f.Name) {
|
|
81
|
+
fieldsToProcess.push({ name: f.Name, mappedFieldName, isDenied });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const readableFields = this.BuildReadableFieldsTransportValue(entityInfo, denied);
|
|
85
|
+
const projector = contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)
|
|
86
|
+
? (recordChangeProjector ?? new RecordChangeFieldSecurityProjector(md, contextUser))
|
|
87
|
+
: undefined;
|
|
88
|
+
const encryptedFields = entityInfo.EncryptedFields ?? [];
|
|
89
|
+
const checkBinary = entityInfo.HasBinaryFields !== false;
|
|
90
|
+
return {
|
|
91
|
+
entityInfo,
|
|
92
|
+
fieldsToProcess,
|
|
93
|
+
readableFields,
|
|
94
|
+
projector,
|
|
95
|
+
encryptedFields,
|
|
96
|
+
checkBinary,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Applies synchronous field renaming, FLS stripping, readable-fields transport stamping, and
|
|
101
|
+
* Record Changes payload projection for a single row using a precomputed mapping plan.
|
|
102
|
+
*/
|
|
103
|
+
mapRowSynchronously(dataObject, plan) {
|
|
56
104
|
if (!dataObject || Object.keys(dataObject).length === 0) {
|
|
57
105
|
return null;
|
|
58
106
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!entityInfo)
|
|
73
|
-
throw new Error(`Entity ${entityName} not found in metadata`);
|
|
74
|
-
// const fields = entityInfo.Fields.filter((f) => f.Name !== f.CodeName || f.Name.startsWith('__mj_'));
|
|
75
|
-
// FIELD-LEVEL SECURITY — this is the authoritative read boundary for every GraphQL
|
|
76
|
-
// return path (single-record resolvers, external-data-source loads, and anything else
|
|
77
|
-
// routed through here), sitting exactly where encryption masking already lives.
|
|
78
|
-
//
|
|
79
|
-
// The denied set is normally computed ONCE per (entity, user) by the caller and passed
|
|
80
|
-
// in: this method runs once per ROW, so resolving it here would cost fields x rows
|
|
81
|
-
// aggregations. We fall back to computing it when a caller doesn't supply it (the
|
|
82
|
-
// single-record resolvers, where "once per row" and "once per request" are the same
|
|
83
|
-
// thing) rather than failing open — a missing argument must never mean missing security.
|
|
84
|
-
const denied = deniedReadFields ?? (entityInfo.EnableFieldLevelSecurity && contextUser
|
|
85
|
-
? entityInfo.GetDeniedReadFields(contextUser)
|
|
86
|
-
: null);
|
|
87
|
-
const mapper = new FieldMapper();
|
|
88
|
-
entityInfo.Fields.forEach((f) => {
|
|
89
|
-
if (dataObject.hasOwnProperty(f.Name)) {
|
|
90
|
-
// GraphQL doesn't allow us to pass back fields with __ so we are mapping our special field cases that start with __mj_ to _mj__ for transport - they are converted back on the other side automatically
|
|
91
|
-
const mappedFieldName = mapper.MapFieldName(f.CodeName);
|
|
92
|
-
if (denied?.has(f.Name.trim().toLowerCase())) {
|
|
93
|
-
// Omit entirely rather than nulling — a null is indistinguishable from a real
|
|
94
|
-
// null value, and the client should see the field as absent, not as empty.
|
|
95
|
-
// Both key shapes are removed because callers reach this method with rows keyed
|
|
96
|
-
// either way depending on whether mapping has already run.
|
|
97
|
-
delete dataObject[f.Name];
|
|
98
|
-
delete dataObject[mappedFieldName];
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (mappedFieldName !== f.Name) {
|
|
102
|
-
dataObject[mappedFieldName] = dataObject[f.Name];
|
|
103
|
-
delete dataObject[f.Name];
|
|
104
|
-
}
|
|
107
|
+
let row = plan.checkBinary
|
|
108
|
+
? ReplaceByteArraysWithBase64({ ...dataObject })
|
|
109
|
+
: { ...dataObject };
|
|
110
|
+
for (let i = 0; i < plan.fieldsToProcess.length; i++) {
|
|
111
|
+
const fp = plan.fieldsToProcess[i];
|
|
112
|
+
if (Object.prototype.hasOwnProperty.call(row, fp.name)) {
|
|
113
|
+
if (fp.isDenied) {
|
|
114
|
+
delete row[fp.name];
|
|
115
|
+
delete row[fp.mappedFieldName];
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
row[fp.mappedFieldName] = row[fp.name];
|
|
119
|
+
delete row[fp.name];
|
|
105
120
|
}
|
|
106
|
-
});
|
|
107
|
-
// Field security, second half: say IN-BAND which fields this caller may read. Deleting the
|
|
108
|
-
// key above is not enough on its own — GraphQL emits every SELECTED field regardless, so a
|
|
109
|
-
// denied field the client asked for arrives as an explicit null it cannot tell apart from a
|
|
110
|
-
// genuine one. The client must not settle that from its own metadata: during the window
|
|
111
|
-
// after a permission change its copy disagrees with ours, and once metadata filtering lands
|
|
112
|
-
// (issue #3485) it may not hold the permission rows at all. Null for unrestricted callers,
|
|
113
|
-
// which is nearly every request.
|
|
114
|
-
const readableFields = this.BuildReadableFieldsTransportValue(entityInfo, denied);
|
|
115
|
-
if (readableFields) {
|
|
116
|
-
dataObject[ReadableFieldsTransportKey] = readableFields;
|
|
117
|
-
}
|
|
118
|
-
// Field security, third part — the audit trail. A 'MJ: Record Changes' row carries the old
|
|
119
|
-
// and new values of ANOTHER entity's fields, and its own EnableFieldLevelSecurity flag is
|
|
120
|
-
// off, so everything above is a no-op for it and the payload would flow out untouched. The
|
|
121
|
-
// projector recomputes the denied set against the entity each row is about; see
|
|
122
|
-
// RecordChangeFieldSecurityProjector for what each payload column gets.
|
|
123
|
-
//
|
|
124
|
-
// Same "a missing argument must never mean missing security" rule as the denied set above:
|
|
125
|
-
// the projector is normally built ONCE per request by the array caller and passed in, and
|
|
126
|
-
// we build one here when it wasn't — the single-record resolvers, where once-per-row and
|
|
127
|
-
// once-per-request are the same thing.
|
|
128
|
-
if (contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)) {
|
|
129
|
-
const projector = recordChangeProjector ?? new RecordChangeFieldSecurityProjector(md, contextUser);
|
|
130
|
-
dataObject = projector.ProjectRow(dataObject);
|
|
131
121
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
122
|
+
}
|
|
123
|
+
if (plan.readableFields) {
|
|
124
|
+
row[ReadableFieldsTransportKey] = plan.readableFields;
|
|
125
|
+
}
|
|
126
|
+
if (plan.projector) {
|
|
127
|
+
row = plan.projector.ProjectRow(row);
|
|
128
|
+
}
|
|
129
|
+
return row;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Decrypts or masks encrypted fields on a mapped row when `plan.encryptedFields.length > 0`.
|
|
133
|
+
*/
|
|
134
|
+
async decryptRowEncryptedFields(row, encryptedFields, contextUser) {
|
|
135
|
+
for (const field of encryptedFields) {
|
|
136
|
+
const fieldName = field.CodeName;
|
|
137
|
+
const value = row[fieldName];
|
|
138
|
+
if (value === null || value === undefined)
|
|
139
|
+
continue;
|
|
140
|
+
const engine = EncryptionEngine.Instance;
|
|
141
|
+
await engine.Config(false, contextUser);
|
|
142
|
+
const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
|
|
143
|
+
if (typeof value === 'string' && IsValueEncrypted(value, keyMarker)) {
|
|
144
|
+
if (field.AllowDecryptInAPI) {
|
|
145
|
+
if (contextUser) {
|
|
146
|
+
try {
|
|
147
|
+
const decryptedValue = await engine.Decrypt(value, contextUser);
|
|
148
|
+
row[fieldName] = decryptedValue;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (IsEncryptionKeyUnavailable(err)) {
|
|
152
|
+
ReportEncryptionKeyUnavailable(err);
|
|
158
153
|
}
|
|
159
154
|
else {
|
|
160
|
-
|
|
161
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
155
|
+
LogError(`Failed to decrypt field ${fieldName} for API response: ${err}`);
|
|
162
156
|
}
|
|
157
|
+
row[fieldName] = ENCRYPTED_SENTINEL;
|
|
163
158
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
// else: AllowDecryptInAPI=false and SendEncryptedValue=true - keep encrypted value as-is
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
row[fieldName] = ENCRYPTED_SENTINEL;
|
|
169
162
|
}
|
|
170
163
|
}
|
|
164
|
+
else if (!field.SendEncryptedValue) {
|
|
165
|
+
row[fieldName] = ENCRYPTED_SENTINEL;
|
|
166
|
+
}
|
|
171
167
|
}
|
|
172
168
|
}
|
|
173
|
-
|
|
169
|
+
}
|
|
170
|
+
async MapFieldNamesToCodeNames(entityName, dataObject, contextUser, provider, deniedReadFields, recordChangeProjector) {
|
|
171
|
+
if (!dataObject || Object.keys(dataObject).length === 0) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const plan = this.buildFieldMappingPlan(entityName, contextUser, provider, deniedReadFields, recordChangeProjector);
|
|
175
|
+
const mapped = this.mapRowSynchronously(dataObject, plan);
|
|
176
|
+
if (mapped && plan.encryptedFields.length > 0) {
|
|
177
|
+
await this.decryptRowEncryptedFields(mapped, plan.encryptedFields, contextUser);
|
|
178
|
+
}
|
|
179
|
+
return mapped;
|
|
174
180
|
}
|
|
175
181
|
/**
|
|
176
182
|
* Reverse-maps GraphQL-safe field names back to entity CodeNames in a mutation input object.
|
|
@@ -227,26 +233,17 @@ export class ResolverBase {
|
|
|
227
233
|
if (!dataObjectArray || dataObjectArray.length === 0) {
|
|
228
234
|
return dataObjectArray;
|
|
229
235
|
}
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// span many — so the projector, which owns that memo, must outlive the row loop. Built once
|
|
242
|
-
// here for the same reason the denied set above is: GetDeniedReadFields walks every field on
|
|
243
|
-
// an entity and must never be called per row.
|
|
244
|
-
const recordChangeProjector = contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)
|
|
245
|
-
? new RecordChangeFieldSecurityProjector(md, contextUser)
|
|
246
|
-
: undefined;
|
|
247
|
-
const mapped = [];
|
|
248
|
-
for (const element of dataObjectArray) {
|
|
249
|
-
mapped.push(await this.MapFieldNamesToCodeNames(entityName, element, contextUser, provider, deniedReadFields, recordChangeProjector));
|
|
236
|
+
// Precompute the entity lookup, FieldMapper plan, denied-fields set, readable-fields list,
|
|
237
|
+
// and Record Changes projector ONCE for the whole array rather than per row.
|
|
238
|
+
const plan = this.buildFieldMappingPlan(entityName, contextUser, provider);
|
|
239
|
+
const hasEncryptedFields = plan.encryptedFields.length > 0;
|
|
240
|
+
const mapped = new Array(dataObjectArray.length);
|
|
241
|
+
for (let i = 0; i < dataObjectArray.length; i++) {
|
|
242
|
+
const row = this.mapRowSynchronously(dataObjectArray[i], plan);
|
|
243
|
+
if (row && hasEncryptedFields) {
|
|
244
|
+
await this.decryptRowEncryptedFields(row, plan.encryptedFields, contextUser);
|
|
245
|
+
}
|
|
246
|
+
mapped[i] = row;
|
|
250
247
|
}
|
|
251
248
|
return mapped;
|
|
252
249
|
}
|
|
@@ -302,9 +299,11 @@ export class ResolverBase {
|
|
|
302
299
|
* @param dataObject - The data object containing field values
|
|
303
300
|
* @param encryptionEngine - Optional encryption engine for re-encryption (lazy loaded if needed)
|
|
304
301
|
* @param contextUser - User context for encryption operations
|
|
302
|
+
* @param fieldKey - What the object's keys are: field CodeNames (GraphQL transport rows, the
|
|
303
|
+
* default) or field Names (raw provider rows, such as dataset item results)
|
|
305
304
|
* @returns The filtered data object
|
|
306
305
|
*/
|
|
307
|
-
async FilterEncryptedFieldsForAPI(entityName, dataObject, contextUser, provider) {
|
|
306
|
+
async FilterEncryptedFieldsForAPI(entityName, dataObject, contextUser, provider, fieldKey = 'CodeName') {
|
|
308
307
|
if (!dataObject)
|
|
309
308
|
return dataObject;
|
|
310
309
|
const md = provider ?? new Metadata();
|
|
@@ -317,7 +316,7 @@ export class ResolverBase {
|
|
|
317
316
|
return dataObject;
|
|
318
317
|
// Process each encrypted field
|
|
319
318
|
for (const field of encryptedFields) {
|
|
320
|
-
const fieldName = field.CodeName;
|
|
319
|
+
const fieldName = fieldKey === 'Name' ? field.Name : field.CodeName;
|
|
321
320
|
const value = dataObject[fieldName];
|
|
322
321
|
// Skip null/undefined values
|
|
323
322
|
if (value === null || value === undefined)
|
|
@@ -353,9 +352,10 @@ export class ResolverBase {
|
|
|
353
352
|
return dataObject;
|
|
354
353
|
}
|
|
355
354
|
/**
|
|
356
|
-
* Filters encrypted fields for an array of data objects
|
|
355
|
+
* Filters encrypted fields for an array of data objects. `fieldKey` is as for
|
|
356
|
+
* {@link FilterEncryptedFieldsForAPI}.
|
|
357
357
|
*/
|
|
358
|
-
async ArrayFilterEncryptedFieldsForAPI(entityName, dataObjectArray, contextUser, provider) {
|
|
358
|
+
async ArrayFilterEncryptedFieldsForAPI(entityName, dataObjectArray, contextUser, provider, fieldKey = 'CodeName') {
|
|
359
359
|
if (!dataObjectArray || dataObjectArray.length === 0)
|
|
360
360
|
return dataObjectArray;
|
|
361
361
|
// Check if entity has any encrypted fields first to avoid unnecessary processing
|
|
@@ -368,7 +368,7 @@ export class ResolverBase {
|
|
|
368
368
|
return dataObjectArray;
|
|
369
369
|
// Process each element
|
|
370
370
|
for (const element of dataObjectArray) {
|
|
371
|
-
await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser, provider);
|
|
371
|
+
await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser, provider, fieldKey);
|
|
372
372
|
}
|
|
373
373
|
return dataObjectArray;
|
|
374
374
|
}
|
|
@@ -727,44 +727,59 @@ export class ResolverBase {
|
|
|
727
727
|
* This screen uses `@memberjunction/sql-parser` (same wrap as EDS `assertReadOnlyClause`):
|
|
728
728
|
* wrap the fragment as a single SELECT, fail closed if it does not parse as a read, then
|
|
729
729
|
* allow a FROM only when it is an entity **BaseView**. Base tables (`Meeting`, `__mj.User`)
|
|
730
|
-
* and catalogs are rejected. Server-internal RunView callers
|
|
730
|
+
* and catalogs are rejected. Server-internal RunView callers get only the provider's
|
|
731
|
+
* statement-separator check, which cannot refuse a legitimate filter.
|
|
732
|
+
*
|
|
733
|
+
* The screen is `ClientClauseScreen` in `@memberjunction/generic-database-provider`, shared
|
|
734
|
+
* with the REST view routes, ad-hoc list sources, and the provider's saved-view checks.
|
|
735
|
+
*
|
|
736
|
+
* Before that parse, the fragment must be balanced on its own (`IsBalancedSQLFragment`). The
|
|
737
|
+
* provider and the tenant hook splice it inside parentheses and AND their own filters after
|
|
738
|
+
* it, so a fragment that closes those parentheses (`1=1) OR (1=1`) would leave row-level
|
|
739
|
+
* security, API-key row filters and tenant scope on one side of an OR the caller controls.
|
|
740
|
+
* The parse wraps the fragment in parentheses too, which is why it cannot catch this alone.
|
|
741
|
+
* A backslash inside a quoted string or identifier is refused as well: the parser reads `\'`
|
|
742
|
+
* inside a plain literal as an escaped quote, but SQL Server and PostgreSQL end the literal
|
|
743
|
+
* there, so the text after it could be code that the parse-based checks never saw.
|
|
731
744
|
*
|
|
732
745
|
* RLS is applied by RunView as an outer WHERE around the entity being queried, not compiled
|
|
733
|
-
* into the view
|
|
734
|
-
*
|
|
746
|
+
* into the view, and RunView removes denied fields from its own result only. A subquery reads a
|
|
747
|
+
* base view directly with neither, so the screen refuses one over any entity the user reads under
|
|
748
|
+
* a row filter or with denied fields (`ClientClauseScreen.AssertUnscopedRead`), the queried
|
|
749
|
+
* entity included.
|
|
735
750
|
*/
|
|
736
|
-
assertClientClauseUsesEntityBaseViews(clause, label, provider, user) {
|
|
751
|
+
assertClientClauseUsesEntityBaseViews(clause, label, provider, user, options) {
|
|
737
752
|
if (!clause?.trim())
|
|
738
753
|
return;
|
|
754
|
+
// The balance and backslash checks run before the shared screen: it parses the fragment
|
|
755
|
+
// inside its own parentheses, so it cannot see a fragment that closes the parentheses the
|
|
756
|
+
// provider and the tenant hook splice it into.
|
|
739
757
|
const dialect = this.dialectForProvider(provider);
|
|
740
758
|
if (SQLParser.HasStackedStatements(clause, dialect)) {
|
|
741
759
|
throw new Error(`Invalid ${label}: multiple statements are not permitted in client-supplied filters`);
|
|
742
760
|
}
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const parser = new SQLParser(wrapped, dialect);
|
|
747
|
-
if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
|
|
748
|
-
throw new Error(`Invalid ${label}: not a safe read-only filter fragment — refusing under uncertainty`);
|
|
761
|
+
const balance = IsBalancedSQLFragment(clause, dialect);
|
|
762
|
+
if (!balance.IsBalanced) {
|
|
763
|
+
throw new Error(`Invalid ${label}: ${balance.Reason}`);
|
|
749
764
|
}
|
|
750
|
-
if (this.
|
|
751
|
-
throw new Error(`Invalid ${label}:
|
|
752
|
-
}
|
|
753
|
-
const allowed = this.entityBaseViewAllowList(provider);
|
|
754
|
-
const tables = SQLParser.ExtractTableRefs(wrapped, dialect);
|
|
755
|
-
for (const t of tables) {
|
|
756
|
-
const table = this.stripSqlIdent(t.TableName);
|
|
757
|
-
const schema = this.stripSqlIdent(t.SchemaName);
|
|
758
|
-
if (table.toLowerCase() === '__mj_clause_screen')
|
|
759
|
-
continue;
|
|
760
|
-
this.assertTableRefReadable(allowed, schema, table, label, user);
|
|
765
|
+
if (this.hasBackslashInQuotes(clause, dialect)) {
|
|
766
|
+
throw new Error(`Invalid ${label}: a backslash inside a quoted string or identifier is not permitted in client-supplied filters`);
|
|
761
767
|
}
|
|
768
|
+
ClientClauseScreen.AssertClauseUsesEntityBaseViews(clause, label, provider?.Entities ?? [], dialect, user, options);
|
|
762
769
|
}
|
|
763
770
|
/**
|
|
764
771
|
* SECURITY — screen for a FULL client-supplied SQL statement (ad-hoc query path). In addition
|
|
765
772
|
* to the SELECT-only validation the caller performs, every table reference must resolve to an
|
|
766
|
-
* entity BaseView the acting user can read UNSCOPED.
|
|
767
|
-
*
|
|
773
|
+
* entity BaseView the acting user can read UNSCOPED. A reference is excluded only when it reads a
|
|
774
|
+
* CTE that is in scope there (`StatementRelation.IsCTEReference`): unqualified, declared by its
|
|
775
|
+
* own SELECT or an enclosing one, and matched by the dialect's identifier rules. A name reserved
|
|
776
|
+
* for the SQL parser is refused before parsing (`ClientClauseScreen.AssertNoIdentifierAliasNames`).
|
|
777
|
+
* Fails closed: no user, or an unresolvable reference, refuses.
|
|
778
|
+
*
|
|
779
|
+
* The references come from a walk of the whole parse tree
|
|
780
|
+
* (`ClientClauseScreen.CollectStatementReads`). The statement is refused when that walk does not
|
|
781
|
+
* account for every SELECT in the text, or when a FROM item names no table (a table function or
|
|
782
|
+
* a row list).
|
|
768
783
|
*
|
|
769
784
|
* Entity CanRead is not unscoped base-view read authority: RunView narrows a granted read with
|
|
770
785
|
* row-level security and denied-field projection, and raw SQL applies neither. Rather than try
|
|
@@ -777,6 +792,7 @@ export class ResolverBase {
|
|
|
777
792
|
throw new Error(`Invalid ${label}: no acting user resolved for ad-hoc SQL — refusing`);
|
|
778
793
|
}
|
|
779
794
|
const dialect = this.dialectForProvider(provider);
|
|
795
|
+
ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
|
|
780
796
|
const parser = new SQLParser(sqlText, dialect);
|
|
781
797
|
if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
|
|
782
798
|
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
@@ -784,51 +800,20 @@ export class ResolverBase {
|
|
|
784
800
|
if (this.astContainsWriteNode(parser.AST)) {
|
|
785
801
|
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
786
802
|
}
|
|
787
|
-
const
|
|
788
|
-
|
|
803
|
+
const reads = ClientClauseScreen.CollectStatementReads(parser);
|
|
804
|
+
ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
|
|
789
805
|
const allowed = this.entityBaseViewAllowList(provider);
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
const table = this.stripSqlIdent(t.TableName);
|
|
793
|
-
const schema = this.stripSqlIdent(t.SchemaName);
|
|
794
|
-
if (this.isUnqualifiedSchema(schema) && cteNames.has(table.toLowerCase()))
|
|
806
|
+
for (const relation of reads.Relations) {
|
|
807
|
+
if (relation.IsCTEReference)
|
|
795
808
|
continue;
|
|
796
|
-
const entity =
|
|
797
|
-
|
|
809
|
+
const entity = ClientClauseScreen.AssertRelationReadable(allowed, relation, label, user);
|
|
810
|
+
if (entity)
|
|
811
|
+
this.assertUnscopedRead(entity, user, label);
|
|
798
812
|
}
|
|
799
813
|
}
|
|
800
|
-
/** Refuses an entity whose read is narrowed for this user
|
|
814
|
+
/** Refuses an entity whose read is narrowed for this user; see `ClientClauseScreen.AssertUnscopedRead`. */
|
|
801
815
|
assertUnscopedRead(entity, user, label) {
|
|
802
|
-
|
|
803
|
-
throw new Error(`Invalid ${label}: entity '${entity.Name}' is row-level-security filtered for you — ad-hoc SQL cannot apply that filter; use RunView`);
|
|
804
|
-
}
|
|
805
|
-
if (entity.GetDeniedReadFields(user).size > 0) {
|
|
806
|
-
throw new Error(`Invalid ${label}: entity '${entity.Name}' has fields you are not permitted to read — ad-hoc SQL cannot project them away; use RunView`);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
/** Walks a parsed AST collecting the names of CTEs the statement itself defines. */
|
|
810
|
-
collectCTENames(node, names) {
|
|
811
|
-
if (!node || typeof node !== 'object')
|
|
812
|
-
return;
|
|
813
|
-
if (Array.isArray(node)) {
|
|
814
|
-
for (const n of node)
|
|
815
|
-
this.collectCTENames(n, names);
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
const obj = node;
|
|
819
|
-
const withClause = obj.with;
|
|
820
|
-
if (Array.isArray(withClause)) {
|
|
821
|
-
for (const cte of withClause) {
|
|
822
|
-
const name = cte?.name;
|
|
823
|
-
if (typeof name === 'string')
|
|
824
|
-
names.add(name.toLowerCase());
|
|
825
|
-
else if (name && typeof name === 'object' && typeof name.value === 'string') {
|
|
826
|
-
names.add(name.value.toLowerCase());
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
for (const v of Object.values(obj))
|
|
831
|
-
this.collectCTENames(v, names);
|
|
816
|
+
ClientClauseScreen.AssertUnscopedRead(entity, user, label);
|
|
832
817
|
}
|
|
833
818
|
/**
|
|
834
819
|
* Applies {@link assertClientClauseUsesEntityBaseViews} to every client-supplied clause
|
|
@@ -851,96 +836,37 @@ export class ResolverBase {
|
|
|
851
836
|
* The single place that is not true is a field carrying `UserSearchParamFormatAPI`, whose
|
|
852
837
|
* admin-authored format may splice the term in unquoted; `createViewUserSearchSQL` re-applies
|
|
853
838
|
* `ValidateUserProvidedSQLClause` for exactly those entities.
|
|
839
|
+
*
|
|
840
|
+
* Each `Aggregates[].expression` gets the same screen, except that text the parser cannot read
|
|
841
|
+
* goes on to the provider, which refuses it per aggregate. The provider never runs the caller's
|
|
842
|
+
* aggregate text: it runs the SQL that `CheckAggregateExpression` rebuilds from a single
|
|
843
|
+
* aggregate call over the entity's columns, and refuses everything else per aggregate.
|
|
854
844
|
*/
|
|
855
845
|
screenClientViewClauses(clauses, provider, user) {
|
|
856
846
|
this.assertClientClauseUsesEntityBaseViews(clauses.extraFilter, 'ExtraFilter', provider, user);
|
|
857
847
|
this.assertClientClauseUsesEntityBaseViews(clauses.orderBy, 'OrderBy', provider, user);
|
|
858
848
|
this.assertClientClauseUsesEntityBaseViews(clauses.overrideExcludeFilter, 'OverrideExcludeFilter', provider, user);
|
|
849
|
+
for (const aggregate of clauses.aggregates ?? []) {
|
|
850
|
+
this.assertClientClauseUsesEntityBaseViews(aggregate?.expression, 'Aggregate', provider, user, { LeaveUnparseableToProvider: true });
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* True when a string literal or quoted identifier in `clause`, read the way the database reads
|
|
855
|
+
* it, contains a backslash (see {@link assertClientClauseUsesEntityBaseViews}).
|
|
856
|
+
*/
|
|
857
|
+
hasBackslashInQuotes(clause, dialect) {
|
|
858
|
+
return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
|
|
859
859
|
}
|
|
860
|
-
/**
|
|
860
|
+
/** Write/DDL node walk shared with the clause screen; see `ClientClauseScreen.AstContainsWriteNode`. */
|
|
861
861
|
astContainsWriteNode(node) {
|
|
862
|
-
|
|
863
|
-
return false;
|
|
864
|
-
if (Array.isArray(node))
|
|
865
|
-
return node.some((n) => this.astContainsWriteNode(n));
|
|
866
|
-
const obj = node;
|
|
867
|
-
const type = obj.type;
|
|
868
|
-
if (typeof type === 'string' && ResolverBase.WRITE_NODE_TYPES.has(type.toLowerCase())) {
|
|
869
|
-
return true;
|
|
870
|
-
}
|
|
871
|
-
return Object.values(obj).some((v) => this.astContainsWriteNode(v));
|
|
862
|
+
return ClientClauseScreen.AstContainsWriteNode(node);
|
|
872
863
|
}
|
|
873
|
-
static { this.WRITE_NODE_TYPES = new Set([
|
|
874
|
-
'insert', 'update', 'delete', 'merge', 'replace', 'drop', 'create', 'alter', 'truncate',
|
|
875
|
-
'rename', 'call', 'exec', 'execute', 'grant', 'revoke', 'use', 'load', 'copy', 'do',
|
|
876
|
-
]); }
|
|
877
864
|
dialectForProvider(provider) {
|
|
878
|
-
|
|
879
|
-
if (/postgres/i.test(name))
|
|
880
|
-
return new PostgreSQLDialect();
|
|
881
|
-
return new SQLServerDialect();
|
|
882
|
-
}
|
|
883
|
-
/** True for a reference with no schema — which `SQLParser.ExtractTableRefs` reports as `dbo`. */
|
|
884
|
-
isUnqualifiedSchema(schema) {
|
|
885
|
-
return !schema || schema.toLowerCase() === 'dbo';
|
|
865
|
+
return ClientClauseScreen.DialectFor(provider);
|
|
886
866
|
}
|
|
887
|
-
|
|
888
|
-
if (!name)
|
|
889
|
-
return '';
|
|
890
|
-
return name.replace(/^\[|\]$/g, '').replace(/^"|"$/g, '').replace(/^`|`$/g, '');
|
|
891
|
-
}
|
|
892
|
-
/**
|
|
893
|
-
* `bare` maps an unqualified view name to its entity, or to `null` when the name exists in
|
|
894
|
-
* more than one schema — the database, not this screen, would decide which one runs.
|
|
895
|
-
*/
|
|
867
|
+
/** The entity base-view allow-list for a provider's entities. */
|
|
896
868
|
entityBaseViewAllowList(provider) {
|
|
897
|
-
|
|
898
|
-
const bare = new Map();
|
|
899
|
-
const entities = provider?.Entities ?? [];
|
|
900
|
-
for (const e of entities) {
|
|
901
|
-
const view = this.stripSqlIdent(e.BaseView);
|
|
902
|
-
if (!view)
|
|
903
|
-
continue;
|
|
904
|
-
const schema = this.stripSqlIdent(e.SchemaName);
|
|
905
|
-
const bareKey = view.toLowerCase();
|
|
906
|
-
bare.set(bareKey, bare.has(bareKey) ? null : e);
|
|
907
|
-
if (schema)
|
|
908
|
-
qualified.set(`${schema}.${view}`.toLowerCase(), e);
|
|
909
|
-
}
|
|
910
|
-
return { qualified, bare };
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* SECURITY — resolves a table reference against the entity BaseView allow-list and, when an
|
|
914
|
-
* acting user is supplied, additionally requires that user to hold CanRead on the referenced
|
|
915
|
-
* entity. Base views do not embed RLS and entity permissions are otherwise checked only on
|
|
916
|
-
* the TOP entity of a request, so without this check a subquery (or ad-hoc query) could read
|
|
917
|
-
* entities the caller has no read grant on.
|
|
918
|
-
*
|
|
919
|
-
* A schema-qualified reference must match that exact schema — `secret.vwFoo` never resolves
|
|
920
|
-
* through another schema's `vwFoo`. An unqualified reference resolves only when the view name
|
|
921
|
-
* is unique across schemas; an ambiguous one must be qualified, because the database's
|
|
922
|
-
* default-schema resolution, not this screen, would pick which view actually runs.
|
|
923
|
-
*
|
|
924
|
-
* `SQLParser.ExtractTableRefs` reports an unqualified reference as schema `dbo`, so `dbo` is
|
|
925
|
-
* indistinguishable from "no schema" here and both take the unqualified path (an exact
|
|
926
|
-
* `dbo.<view>` entity wins first).
|
|
927
|
-
*/
|
|
928
|
-
assertTableRefReadable(allowed, schema, table, label, user) {
|
|
929
|
-
const qualified = allowed.qualified.get(`${schema}.${table}`.toLowerCase());
|
|
930
|
-
const entity = this.isUnqualifiedSchema(schema) ? qualified ?? allowed.bare.get(table.toLowerCase()) : qualified;
|
|
931
|
-
if (entity === null) {
|
|
932
|
-
throw new Error(`Invalid ${label}: '${table}' is a base view in more than one schema — qualify it with its schema`);
|
|
933
|
-
}
|
|
934
|
-
if (!entity) {
|
|
935
|
-
throw new Error(`Invalid ${label}: subquery must use an entity base view, not '${schema ? schema + '.' : ''}${table}'`);
|
|
936
|
-
}
|
|
937
|
-
if (user) {
|
|
938
|
-
const perms = entity.GetUserPermisions(user);
|
|
939
|
-
if (!perms.CanRead) {
|
|
940
|
-
throw new Error(`Invalid ${label}: you do not have read permission on entity '${entity.Name}' referenced by '${schema ? schema + '.' : ''}${table}'`);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
return entity;
|
|
869
|
+
return ClientClauseScreen.BuildEntityBaseViewAllowList(provider?.Entities ?? []);
|
|
944
870
|
}
|
|
945
871
|
/**
|
|
946
872
|
* Optimized RunViewGenericInternal implementation with:
|
|
@@ -962,7 +888,7 @@ export class ResolverBase {
|
|
|
962
888
|
// SECURITY: GraphQL ExtraFilter may contain IN (SELECT … FROM <base view>).
|
|
963
889
|
// Screen at this boundary: parse, reject writes, allow only entity BaseViews the
|
|
964
890
|
// acting user can read (subqueries do not inherit the top entity's permission check).
|
|
965
|
-
this.screenClientViewClauses({ extraFilter, orderBy, userSearchString, overrideExcludeFilter }, provider, user);
|
|
891
|
+
this.screenClientViewClauses({ extraFilter, orderBy, userSearchString, overrideExcludeFilter, aggregates }, provider, user);
|
|
966
892
|
// Check API key scope authorization for view operations
|
|
967
893
|
await this.CheckAPIKeyScopeAuthorization('view:run', viewInfo.Entity, userPayload);
|
|
968
894
|
const md = provider;
|
|
@@ -1102,6 +1028,7 @@ export class ResolverBase {
|
|
|
1102
1028
|
orderBy: param.orderBy,
|
|
1103
1029
|
userSearchString: param.userSearchString,
|
|
1104
1030
|
overrideExcludeFilter: param.overrideExcludeFilter,
|
|
1031
|
+
aggregates: param.aggregates,
|
|
1105
1032
|
}, md, contextUser ?? undefined);
|
|
1106
1033
|
if (param.viewInfo) {
|
|
1107
1034
|
// Validate entity only once per entity type
|
|
@@ -1481,9 +1408,10 @@ export class ResolverBase {
|
|
|
1481
1408
|
// has TrackRecordChanges off, so without it a client sending OldValues takes the branch below
|
|
1482
1409
|
// that hydrates from those values — never loading what the database actually holds.
|
|
1483
1410
|
const hasNarrowedAuditPayload = this.StripRecordChangePayloadFromClientInput(entityInfo, userInfo, input, clientNewValues);
|
|
1484
|
-
if (this.MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload)) {
|
|
1411
|
+
if (this.MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload, userInfo)) {
|
|
1485
1412
|
// We get here because the entity tracks record changes, OR the client did not provide OldValues,
|
|
1486
|
-
// OR field-level security is in play — in every case we need the true
|
|
1413
|
+
// OR field-level security or an Update row filter is in play — in every case we need the true
|
|
1414
|
+
// old values from the DB
|
|
1487
1415
|
const cKey = new CompositeKey(entityInfo.PrimaryKeys.map((pk) => {
|
|
1488
1416
|
return {
|
|
1489
1417
|
FieldName: pk.Name,
|
|
@@ -1552,13 +1480,35 @@ export class ResolverBase {
|
|
|
1552
1480
|
extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
|
|
1553
1481
|
});
|
|
1554
1482
|
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Checks whether `oldValues` (`input.OldValues___`) contains every writable in-table field of
|
|
1485
|
+
* `entityInfo` (plus primary keys).
|
|
1486
|
+
*
|
|
1487
|
+
* With the delta-only `Save()` wire protocol, `GraphQLDataProvider.Save()` sends only primary keys
|
|
1488
|
+
* and dirty writable fields in `vars.input` and `OldValues___` on normal updates. Hydrating an
|
|
1489
|
+
* entity directly from a partial-delta `OldValues___` via `entityObject.LoadFromData(oldValues)`
|
|
1490
|
+
* (without first loading the authoritative row via `InnerLoad`) would leave every untouched column
|
|
1491
|
+
* `NotLoaded = true` (or `null`), causing `spUpdate` or custom `Validate()` hooks to see incomplete
|
|
1492
|
+
* state. Returning `false` for partial deltas ensures {@link MustLoadTruthFromDatabase} routes
|
|
1493
|
+
* delta updates through `InnerLoad(cKey)` + {@link TestAndSetClientOldValuesToDBValues}, even on
|
|
1494
|
+
* entities with `TrackRecordChanges === false`.
|
|
1495
|
+
*/
|
|
1496
|
+
hasCompleteOldValues(entityInfo, oldValues) {
|
|
1497
|
+
if (!oldValues || oldValues.length === 0) {
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
const providedKeys = new Set(oldValues.map((kv) => kv.Key));
|
|
1501
|
+
return entityInfo.Fields.every((f) => (!f.IsPrimaryKey && (f.ReadOnly || f.IsVirtual)) || providedKeys.has(f.Name) || providedKeys.has(f.CodeName));
|
|
1502
|
+
}
|
|
1555
1503
|
/**
|
|
1556
1504
|
* Whether `UpdateRecord` must hydrate the entity from the DATABASE rather than from the
|
|
1557
1505
|
* client's `OldValues___`.
|
|
1558
1506
|
*
|
|
1559
|
-
* The `OldValues___` path exists as an optimization
|
|
1560
|
-
*
|
|
1561
|
-
*
|
|
1507
|
+
* The `OldValues___` path exists as an optimization when a client supplies the complete pre-save
|
|
1508
|
+
* record (e.g. `IgnoreDirtyState: true`) and nothing else requires the database's authoritative prior
|
|
1509
|
+
* state. Under the delta-only `Save()` wire protocol, normal updates send only primary keys and dirty
|
|
1510
|
+
* writable fields in `OldValues___`, so {@link hasCompleteOldValues} returns `false` and forces the
|
|
1511
|
+
* database load (`InnerLoad`) so untouched columns retain their stored values.
|
|
1562
1512
|
*
|
|
1563
1513
|
* `EnableFieldLevelSecurity` is the security-critical one, and it is NOT redundant with
|
|
1564
1514
|
* `hasDeniedReadFields`. That flag reports READ denials, and the canonical FLS configuration is
|
|
@@ -1576,21 +1526,41 @@ export class ResolverBase {
|
|
|
1576
1526
|
* it never applies them to the entity. An update-denied field can then only become dirty by being
|
|
1577
1527
|
* named in the mutation input itself, which is precisely the case that check does catch.
|
|
1578
1528
|
*
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1529
|
+
* The flag comes after the cheap terms, so the extra load lands only on entities that have the
|
|
1530
|
+
* feature switched on, which is almost none of them.
|
|
1581
1531
|
*
|
|
1582
1532
|
* `MJ: Record Changes` always loads from the database: its server class allows an update only when
|
|
1583
1533
|
* Comments is the one dirty field, and "dirty" compared against client-supplied OldValues lets a
|
|
1584
1534
|
* caller pin forged audit columns as both old and new values. It doesn't track its own changes, so
|
|
1585
1535
|
* nothing else forces the load.
|
|
1536
|
+
*
|
|
1537
|
+
* `MJ: Data Context Items` always loads from the database for the same reason: its server class
|
|
1538
|
+
* lets only an administrator save an item that holds SQL, and decides that from the item's `Type`
|
|
1539
|
+
* and `SQL`. Hydrated from OldValues, a client could leave both out, and the stored procedure would
|
|
1540
|
+
* keep the stored SQL item while the rule saw none.
|
|
1541
|
+
*
|
|
1542
|
+
* `MJ: Conversation Details` always loads for the same reason: its write gate decides from the stored
|
|
1543
|
+
* row (who wrote a person's message, its role, the owner's rating), and an OldValues-hydrated entity
|
|
1544
|
+
* would let a grantee claim to be a message's author while rewriting it. See
|
|
1545
|
+
* {@link ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW}.
|
|
1546
|
+
*
|
|
1547
|
+
* A caller under an Update row filter (a role's Update RLS filter or an API-key row filter) always
|
|
1548
|
+
* loads from the database too (#4919). The save's post-image check skips its query when no column
|
|
1549
|
+
* the filter reads is dirty, and dirtiness measured against client OldValues is the caller's to
|
|
1550
|
+
* choose: claiming the new value of a filter column as its old value made it clean, so the update
|
|
1551
|
+
* moved the row outside the caller's scope without the check ever running. Evaluated last: it
|
|
1552
|
+
* builds the filter clause, and only callers with such a filter pay for the load. A missing user
|
|
1553
|
+
* (a user-cache miss) also loads: with no user the filter can't be ruled out.
|
|
1586
1554
|
*/
|
|
1587
|
-
MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload) {
|
|
1555
|
+
MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload, user) {
|
|
1588
1556
|
return (entityInfo.TrackRecordChanges ||
|
|
1589
|
-
entityInfo.Name.trim().toLowerCase()
|
|
1590
|
-
!input.OldValues___ ||
|
|
1557
|
+
ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW.has(entityInfo.Name.trim().toLowerCase()) ||
|
|
1558
|
+
!this.hasCompleteOldValues(entityInfo, input.OldValues___) ||
|
|
1591
1559
|
hasDeniedReadFields ||
|
|
1592
1560
|
hasNarrowedAuditPayload ||
|
|
1593
|
-
entityInfo.EnableFieldLevelSecurity
|
|
1561
|
+
entityInfo.EnableFieldLevelSecurity ||
|
|
1562
|
+
!user ||
|
|
1563
|
+
entityInfo.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Update, '').length > 0);
|
|
1594
1564
|
}
|
|
1595
1565
|
/**
|
|
1596
1566
|
* Field-level security guard for the update path.
|
|
@@ -1736,19 +1706,35 @@ export class ResolverBase {
|
|
|
1736
1706
|
}
|
|
1737
1707
|
}
|
|
1738
1708
|
/**
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1709
|
+
* Compares the client-supplied `OldValues___` array in `input` against the authoritative database
|
|
1710
|
+
* values just loaded onto `entityObject` via `InnerLoad`, checks for overlapping concurrent field
|
|
1711
|
+
* modifications, and then applies `clientNewValues` onto `entityObject` via `SetMany`.
|
|
1742
1712
|
*
|
|
1743
|
-
*
|
|
1713
|
+
* Supports both full-record and delta-only `OldValues___` payloads: only keys present in
|
|
1714
|
+
* `input.OldValues___` (e.g. primary keys + dirty writable fields) are compared against `dbValues`,
|
|
1715
|
+
* and `entityObject.SetMany(clientNewValues)` mutates only the fields sent in `clientNewValues`
|
|
1716
|
+
* while preserving all untouched DB-loaded fields on `entityObject`.
|
|
1717
|
+
*
|
|
1718
|
+
* ASSUMES: `input` has an `OldValues___` property that is an array of Key/Value pairs representing
|
|
1719
|
+
* the pre-mutation values of the fields the client is updating.
|
|
1744
1720
|
*/
|
|
1721
|
+
/**
|
|
1722
|
+
* Resolves a field on `entityInfo` by `CodeName` or `Name` in $O(1)$ via `FieldByCodeName` /
|
|
1723
|
+
* `FieldByName` when available on `EntityInfo`, falling back to `.Fields.find()` for test stubs.
|
|
1724
|
+
*/
|
|
1725
|
+
findEntityFieldByCodeNameOrName(entityInfo, key) {
|
|
1726
|
+
if (typeof entityInfo.FieldByCodeName === 'function') {
|
|
1727
|
+
return entityInfo.FieldByCodeName(key) ?? entityInfo.FieldByName(key);
|
|
1728
|
+
}
|
|
1729
|
+
return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
|
|
1730
|
+
}
|
|
1745
1731
|
async TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject, contextUser) {
|
|
1746
1732
|
// we have OldValues, so we need to compare them to the values we just loaded from the DB
|
|
1747
1733
|
const clientOldValues = {};
|
|
1748
1734
|
// for each item in the oldValues array, add it to the clientOldValues object
|
|
1749
1735
|
input.OldValues___.forEach((item) => {
|
|
1750
1736
|
// we need to do a quick transform on the values to make sure they match the TS Type for the given field because item.Value will always be a string
|
|
1751
|
-
const field = entityObject.EntityInfo
|
|
1737
|
+
const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
|
|
1752
1738
|
const val = this.ClientOldValueToFieldValue(field, item.Value);
|
|
1753
1739
|
clientOldValues[item.Key] = val;
|
|
1754
1740
|
});
|
|
@@ -1758,19 +1744,20 @@ export class ResolverBase {
|
|
|
1758
1744
|
// now we need to compare clientOldValues and dbValues and have a new array that has entries for any differences and have FieldName, clientOldValue and dbValue as properties
|
|
1759
1745
|
const dbDifferences = [];
|
|
1760
1746
|
Object.keys(clientOldValues).forEach((key) => {
|
|
1761
|
-
const f = entityObject.EntityInfo
|
|
1747
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
1748
|
+
const dbVal = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
|
|
1762
1749
|
let different = false;
|
|
1763
1750
|
switch (typeof clientOldValues[key]) {
|
|
1764
1751
|
case 'number':
|
|
1765
|
-
different = clientOldValues[key] !==
|
|
1752
|
+
different = clientOldValues[key] !== dbVal;
|
|
1766
1753
|
break;
|
|
1767
1754
|
case 'boolean':
|
|
1768
|
-
different = clientOldValues[key] !==
|
|
1755
|
+
different = clientOldValues[key] !== dbVal;
|
|
1769
1756
|
break;
|
|
1770
1757
|
case 'object':
|
|
1771
1758
|
if (clientOldValues[key] instanceof Date) {
|
|
1772
1759
|
const clientDate = clientOldValues[key];
|
|
1773
|
-
const dbDate =
|
|
1760
|
+
const dbDate = dbVal;
|
|
1774
1761
|
if (dbDate == null || !(dbDate instanceof Date)) {
|
|
1775
1762
|
different = true;
|
|
1776
1763
|
}
|
|
@@ -1789,11 +1776,11 @@ export class ResolverBase {
|
|
|
1789
1776
|
}
|
|
1790
1777
|
}
|
|
1791
1778
|
else if (clientOldValues[key] === null) {
|
|
1792
|
-
different =
|
|
1779
|
+
different = dbVal !== null;
|
|
1793
1780
|
}
|
|
1794
1781
|
break;
|
|
1795
1782
|
default:
|
|
1796
|
-
different = clientOldValues[key] !==
|
|
1783
|
+
different = clientOldValues[key] !== dbVal;
|
|
1797
1784
|
break;
|
|
1798
1785
|
}
|
|
1799
1786
|
if (different && f && !f.ReadOnly) {
|
|
@@ -1801,7 +1788,7 @@ export class ResolverBase {
|
|
|
1801
1788
|
dbDifferences.push({
|
|
1802
1789
|
FieldName: key,
|
|
1803
1790
|
ClientOldValue: clientOldValues[key],
|
|
1804
|
-
DBValue:
|
|
1791
|
+
DBValue: dbVal,
|
|
1805
1792
|
});
|
|
1806
1793
|
}
|
|
1807
1794
|
});
|
|
@@ -1810,8 +1797,8 @@ export class ResolverBase {
|
|
|
1810
1797
|
// first step is to get clientNewValues into an object that is like clientOldValues, get the diff and then compare that diff to the differences array that shows diff between DB and ClientOld
|
|
1811
1798
|
const clientDifferences = [];
|
|
1812
1799
|
Object.keys(clientOldValues).forEach((key) => {
|
|
1813
|
-
const f = entityObject.EntityInfo
|
|
1814
|
-
if (clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
|
|
1800
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
1801
|
+
if (key in clientNewValues && clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
|
|
1815
1802
|
// only include updateable fields
|
|
1816
1803
|
clientDifferences.push({
|
|
1817
1804
|
FieldName: key,
|
|
@@ -1822,7 +1809,8 @@ export class ResolverBase {
|
|
|
1822
1809
|
});
|
|
1823
1810
|
// now we have clientDifferences which shows what the client thinks they are changing. And, we have the dbDifferences array that shows changes between the clientOldValues and the dbValues
|
|
1824
1811
|
// if there is ANY overlap in the FIELDS that appear in both arrays, we need to log a warning but allow the save to continue
|
|
1825
|
-
const
|
|
1812
|
+
const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
|
|
1813
|
+
const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
|
|
1826
1814
|
if (overlap.length > 0) {
|
|
1827
1815
|
const msg = {
|
|
1828
1816
|
Message: 'Inconsistency between old values provided for changed fields, and the values of one or more of those fields in the database. Save operation continued with warning.',
|