@memberjunction/server 6.1.0-edge.0 → 6.1.0-edge.2
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/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +9 -5
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +1 -1
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.js +1 -1
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +208 -646
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1123 -3773
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +19 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +51 -17
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -42
- package/dist/index.js.map +1 -1
- package/dist/logging/secretRedactor.d.ts +9 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -1
- package/dist/logging/secretRedactor.js +39 -0
- package/dist/logging/secretRedactor.js.map +1 -1
- package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
- package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
- package/dist/middleware/BaseServerMiddleware.js +5 -0
- package/dist/middleware/BaseServerMiddleware.js.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +12 -0
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -1
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +9 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +3 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +85 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -0
- package/dist/resolvers/TaskGraphFrameResolver.js +163 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -0
- package/dist/resolvers/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +1 -1
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts +0 -4
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +1 -15
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/SignatureWebhookHandler.js +1 -1
- package/dist/rest/SignatureWebhookHandler.js.map +1 -1
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts +26 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -0
- package/dist/services/ScheduledJobsService.js +1 -1
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.d.ts +33 -0
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -0
- package/dist/services/StartTaskGraphDispatcher.js +46 -0
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -0
- package/dist/services/TaskGraphActionRunner.d.ts +34 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphActionRunner.js +95 -0
- package/dist/services/TaskGraphActionRunner.js.map +1 -0
- package/dist/services/TaskGraphAgentRunner.d.ts +28 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphAgentRunner.js +104 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +70 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -0
- package/dist/services/TaskGraphContinuationDeliverer.js +162 -0
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -0
- package/dist/services/TaskGraphPromptRunner.d.ts +31 -0
- package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphPromptRunner.js +134 -0
- package/dist/services/TaskGraphPromptRunner.js.map +1 -0
- package/dist/services/TaskGraphProviderFactory.d.ts +39 -0
- package/dist/services/TaskGraphProviderFactory.d.ts.map +1 -0
- package/dist/services/TaskGraphProviderFactory.js +28 -0
- package/dist/services/TaskGraphProviderFactory.js.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +1 -1
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
- package/dist/telephony/VonageTelephonyRouter.js +1 -1
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
- package/package.json +92 -89
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
- package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
- package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
- package/src/__tests__/SessionManager.test.ts +1 -1
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +249 -0
- package/src/__tests__/TaskGraphFrameResolver.test.ts +136 -0
- package/src/__tests__/WidgetSessionService.test.ts +1 -1
- package/src/__tests__/databaseAbstraction.test.ts +1 -1
- package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
- package/src/__tests__/resolverBase.rls.test.ts +3 -0
- package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
- package/src/__tests__/widgetGuestElevation.test.ts +1 -1
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/auth/index.ts +10 -6
- package/src/auth/magicLink/MagicLinkService.ts +1 -1
- package/src/auth/newUsers.ts +1 -1
- package/src/context.ts +2 -2
- package/src/generated/generated.ts +955 -2735
- package/src/generic/ResolverBase.ts +52 -17
- package/src/index.ts +108 -43
- package/src/logging/secretRedactor.ts +43 -0
- package/src/middleware/BaseServerMiddleware.ts +5 -0
- package/src/realtimeWidget/WidgetSessionService.ts +1 -1
- package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
- package/src/resolvers/ActionResolver.ts +13 -0
- package/src/resolvers/ComponentRegistryResolver.ts +1 -1
- package/src/resolvers/EntityPermissionResolver.ts +1 -1
- package/src/resolvers/IntegrationDiscoveryResolver.ts +12 -2
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
- package/src/resolvers/SearchEntitiesResolver.ts +1 -1
- package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
- package/src/resolvers/TaskGraphFrameResolver.ts +160 -0
- package/src/resolvers/UserFavoriteResolver.ts +1 -1
- package/src/rest/OAuthCallbackHandler.ts +1 -1
- package/src/rest/RESTEndpointHandler.ts +1 -15
- package/src/rest/SignatureWebhookHandler.ts +1 -1
- package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
- package/src/services/ScheduledJobsService.ts +1 -1
- package/src/services/StartTaskGraphDispatcher.ts +78 -0
- package/src/services/TaskGraphActionRunner.ts +99 -0
- package/src/services/TaskGraphAgentRunner.ts +115 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +181 -0
- package/src/services/TaskGraphPromptRunner.ts +144 -0
- package/src/services/TaskGraphProviderFactory.ts +53 -0
- package/src/telephony/RingCentralTelephonyService.ts +1 -1
- package/src/telephony/TwilioTelephonyRouter.ts +1 -1
- package/src/telephony/VonageTelephonyRouter.ts +1 -1
- package/dist/resolvers/ReportResolver.d.ts +0 -24
- package/dist/resolvers/ReportResolver.d.ts.map +0 -1
- package/dist/resolvers/ReportResolver.js +0 -198
- package/dist/resolvers/ReportResolver.js.map +0 -1
- package/dist/resolvers/TaskResolver.d.ts +0 -31
- package/dist/resolvers/TaskResolver.d.ts.map +0 -1
- package/dist/resolvers/TaskResolver.js +0 -157
- package/dist/resolvers/TaskResolver.js.map +0 -1
- package/dist/services/TaskOrchestrator.d.ts +0 -130
- package/dist/services/TaskOrchestrator.d.ts.map +0 -1
- package/dist/services/TaskOrchestrator.js +0 -692
- package/dist/services/TaskOrchestrator.js.map +0 -1
- package/src/resolvers/ReportResolver.ts +0 -165
- package/src/resolvers/TaskResolver.ts +0 -148
- package/src/resolvers/__tests__/ReportResolver.test.ts +0 -279
- package/src/services/TaskOrchestrator.ts +0 -873
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/server",
|
|
3
|
-
"version": "6.1.0-edge.
|
|
3
|
+
"version": "6.1.0-edge.2",
|
|
4
4
|
"description": "MemberJunction: This project provides API access via GraphQL to the common data store.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -19,94 +19,95 @@
|
|
|
19
19
|
"@as-integrations/express5": "^1.0.0",
|
|
20
20
|
"@graphql-tools/schema": "^10.0.38",
|
|
21
21
|
"@graphql-tools/utils": "^11.0.0",
|
|
22
|
-
"@memberjunction/actions": "6.1.0-edge.
|
|
23
|
-
"@memberjunction/actions-apollo": "6.1.0-edge.
|
|
24
|
-
"@memberjunction/actions-base": "6.1.0-edge.
|
|
25
|
-
"@memberjunction/actions-bizapps-accounting": "6.1.0-edge.
|
|
26
|
-
"@memberjunction/actions-bizapps-crm": "6.1.0-edge.
|
|
27
|
-
"@memberjunction/actions-bizapps-formbuilders": "6.1.0-edge.
|
|
28
|
-
"@memberjunction/actions-bizapps-lms": "6.1.0-edge.
|
|
29
|
-
"@memberjunction/actions-bizapps-social": "6.1.0-edge.
|
|
30
|
-
"@memberjunction/ai": "6.1.0-edge.
|
|
31
|
-
"@memberjunction/ai-agent-manager": "6.1.0-edge.
|
|
32
|
-
"@memberjunction/ai-agent-manager-actions": "6.1.0-edge.
|
|
33
|
-
"@memberjunction/ai-agents": "6.1.0-edge.
|
|
34
|
-
"@memberjunction/ai-bridge-base": "6.1.0-edge.
|
|
35
|
-
"@memberjunction/ai-bridge-ringcentral": "6.1.0-edge.
|
|
36
|
-
"@memberjunction/ai-bridge-server": "6.1.0-edge.
|
|
37
|
-
"@memberjunction/ai-bridge-teams": "6.1.0-edge.
|
|
38
|
-
"@memberjunction/ai-bridge-twilio": "6.1.0-edge.
|
|
39
|
-
"@memberjunction/ai-bridge-vonage": "6.1.0-edge.
|
|
40
|
-
"@memberjunction/ai-core-plus": "6.1.0-edge.
|
|
41
|
-
"@memberjunction/ai-engine-base": "6.1.0-edge.
|
|
42
|
-
"@memberjunction/ai-mcp-client": "6.1.0-edge.
|
|
43
|
-
"@memberjunction/ai-prompts": "6.1.0-edge.
|
|
44
|
-
"@memberjunction/ai-provider-bundle": "6.1.0-edge.
|
|
45
|
-
"@memberjunction/ai-vector-sync": "6.1.0-edge.
|
|
46
|
-
"@memberjunction/ai-vectordb": "6.1.0-edge.
|
|
47
|
-
"@memberjunction/ai-vectors-pinecone": "6.1.0-edge.
|
|
48
|
-
"@memberjunction/aiengine": "6.1.0-edge.
|
|
49
|
-
"@memberjunction/api-keys": "6.1.0-edge.
|
|
50
|
-
"@memberjunction/auth-providers": "6.1.0-edge.
|
|
51
|
-
"@memberjunction/clustering-engine": "6.1.0-edge.
|
|
52
|
-
"@memberjunction/codegen-lib": "6.1.0-edge.
|
|
53
|
-
"@memberjunction/communication-engine": "6.1.0-edge.
|
|
54
|
-
"@memberjunction/communication-ms-graph": "6.1.0-edge.
|
|
55
|
-
"@memberjunction/communication-sendgrid": "6.1.0-edge.
|
|
56
|
-
"@memberjunction/communication-types": "6.1.0-edge.
|
|
57
|
-
"@memberjunction/component-registry-client-sdk": "6.1.0-edge.
|
|
58
|
-
"@memberjunction/computer-use": "6.1.0-edge.
|
|
59
|
-
"@memberjunction/computer-use-engine": "6.1.0-edge.
|
|
60
|
-
"@memberjunction/config": "6.1.0-edge.
|
|
61
|
-
"@memberjunction/core": "6.1.0-edge.
|
|
62
|
-
"@memberjunction/core-actions": "6.1.0-edge.
|
|
63
|
-
"@memberjunction/core-entities": "6.1.0-edge.
|
|
64
|
-
"@memberjunction/core-entities-server": "6.1.0-edge.
|
|
65
|
-
"@memberjunction/credentials": "6.1.0-edge.
|
|
66
|
-
"@memberjunction/data-context": "6.1.0-edge.
|
|
67
|
-
"@memberjunction/data-context-server": "6.1.0-edge.
|
|
68
|
-
"@memberjunction/doc-utils": "6.1.0-edge.
|
|
69
|
-
"@memberjunction/encryption": "6.1.0-edge.
|
|
70
|
-
"@memberjunction/entity-communications-base": "6.1.0-edge.
|
|
71
|
-
"@memberjunction/entity-communications-server": "6.1.0-edge.
|
|
72
|
-
"@memberjunction/esignature": "6.1.0-edge.
|
|
73
|
-
"@memberjunction/external-change-detection": "6.1.0-edge.
|
|
74
|
-
"@memberjunction/generic-database-provider": "6.1.0-edge.
|
|
75
|
-
"@memberjunction/global": "6.1.0-edge.
|
|
76
|
-
"@memberjunction/graphql-dataprovider": "6.1.0-edge.
|
|
77
|
-
"@memberjunction/integration-engine": "6.1.0-edge.
|
|
78
|
-
"@memberjunction/integration-engine-base": "6.1.0-edge.
|
|
79
|
-
"@memberjunction/integration-progress-artifacts": "6.1.0-edge.
|
|
80
|
-
"@memberjunction/integration-schema-builder": "6.1.0-edge.
|
|
81
|
-
"@memberjunction/interactive-component-types": "6.1.0-edge.
|
|
82
|
-
"@memberjunction/lists": "6.1.0-edge.
|
|
83
|
-
"@memberjunction/lists-base": "6.1.0-edge.
|
|
84
|
-
"@memberjunction/livekit-room-server": "6.1.0-edge.
|
|
85
|
-
"@memberjunction/notifications": "6.1.0-edge.
|
|
86
|
-
"@memberjunction/postgresql-dataprovider": "6.1.0-edge.
|
|
87
|
-
"@memberjunction/queue": "6.1.0-edge.
|
|
88
|
-
"@memberjunction/record-comparison": "6.1.0-edge.
|
|
89
|
-
"@memberjunction/redis-provider": "6.1.0-edge.
|
|
90
|
-
"@memberjunction/remote-browser-base": "6.1.0-edge.
|
|
91
|
-
"@memberjunction/remote-browser-cdp": "6.1.0-edge.
|
|
92
|
-
"@memberjunction/remote-browser-selfhost": "6.1.0-edge.
|
|
93
|
-
"@memberjunction/remote-browser-server": "6.1.0-edge.
|
|
94
|
-
"@memberjunction/scheduling-actions": "6.1.0-edge.
|
|
95
|
-
"@memberjunction/scheduling-base-types": "6.1.0-edge.
|
|
96
|
-
"@memberjunction/scheduling-engine": "6.1.0-edge.
|
|
97
|
-
"@memberjunction/scheduling-engine-base": "6.1.0-edge.
|
|
98
|
-
"@memberjunction/schema-engine": "6.1.0-edge.
|
|
99
|
-
"@memberjunction/search-engine": "6.1.0-edge.
|
|
100
|
-
"@memberjunction/server-extensions-core": "6.1.0-edge.
|
|
101
|
-
"@memberjunction/sql-dialect": "6.1.0-edge.
|
|
102
|
-
"@memberjunction/sqlserver-dataprovider": "6.1.0-edge.
|
|
103
|
-
"@memberjunction/storage": "6.1.0-edge.
|
|
104
|
-
"@memberjunction/tag-engine": "6.1.0-edge.
|
|
105
|
-
"@memberjunction/tag-engine-base": "6.1.0-edge.
|
|
106
|
-
"@memberjunction/
|
|
107
|
-
"@memberjunction/
|
|
108
|
-
"@memberjunction/testing-engine
|
|
109
|
-
"@memberjunction/
|
|
22
|
+
"@memberjunction/actions": "6.1.0-edge.2",
|
|
23
|
+
"@memberjunction/actions-apollo": "6.1.0-edge.2",
|
|
24
|
+
"@memberjunction/actions-base": "6.1.0-edge.2",
|
|
25
|
+
"@memberjunction/actions-bizapps-accounting": "6.1.0-edge.2",
|
|
26
|
+
"@memberjunction/actions-bizapps-crm": "6.1.0-edge.2",
|
|
27
|
+
"@memberjunction/actions-bizapps-formbuilders": "6.1.0-edge.2",
|
|
28
|
+
"@memberjunction/actions-bizapps-lms": "6.1.0-edge.2",
|
|
29
|
+
"@memberjunction/actions-bizapps-social": "6.1.0-edge.2",
|
|
30
|
+
"@memberjunction/ai": "6.1.0-edge.2",
|
|
31
|
+
"@memberjunction/ai-agent-manager": "6.1.0-edge.2",
|
|
32
|
+
"@memberjunction/ai-agent-manager-actions": "6.1.0-edge.2",
|
|
33
|
+
"@memberjunction/ai-agents": "6.1.0-edge.2",
|
|
34
|
+
"@memberjunction/ai-bridge-base": "6.1.0-edge.2",
|
|
35
|
+
"@memberjunction/ai-bridge-ringcentral": "6.1.0-edge.2",
|
|
36
|
+
"@memberjunction/ai-bridge-server": "6.1.0-edge.2",
|
|
37
|
+
"@memberjunction/ai-bridge-teams": "6.1.0-edge.2",
|
|
38
|
+
"@memberjunction/ai-bridge-twilio": "6.1.0-edge.2",
|
|
39
|
+
"@memberjunction/ai-bridge-vonage": "6.1.0-edge.2",
|
|
40
|
+
"@memberjunction/ai-core-plus": "6.1.0-edge.2",
|
|
41
|
+
"@memberjunction/ai-engine-base": "6.1.0-edge.2",
|
|
42
|
+
"@memberjunction/ai-mcp-client": "6.1.0-edge.2",
|
|
43
|
+
"@memberjunction/ai-prompts": "6.1.0-edge.2",
|
|
44
|
+
"@memberjunction/ai-provider-bundle": "6.1.0-edge.2",
|
|
45
|
+
"@memberjunction/ai-vector-sync": "6.1.0-edge.2",
|
|
46
|
+
"@memberjunction/ai-vectordb": "6.1.0-edge.2",
|
|
47
|
+
"@memberjunction/ai-vectors-pinecone": "6.1.0-edge.2",
|
|
48
|
+
"@memberjunction/aiengine": "6.1.0-edge.2",
|
|
49
|
+
"@memberjunction/api-keys": "6.1.0-edge.2",
|
|
50
|
+
"@memberjunction/auth-providers": "6.1.0-edge.2",
|
|
51
|
+
"@memberjunction/clustering-engine": "6.1.0-edge.2",
|
|
52
|
+
"@memberjunction/codegen-lib": "6.1.0-edge.2",
|
|
53
|
+
"@memberjunction/communication-engine": "6.1.0-edge.2",
|
|
54
|
+
"@memberjunction/communication-ms-graph": "6.1.0-edge.2",
|
|
55
|
+
"@memberjunction/communication-sendgrid": "6.1.0-edge.2",
|
|
56
|
+
"@memberjunction/communication-types": "6.1.0-edge.2",
|
|
57
|
+
"@memberjunction/component-registry-client-sdk": "6.1.0-edge.2",
|
|
58
|
+
"@memberjunction/computer-use": "6.1.0-edge.2",
|
|
59
|
+
"@memberjunction/computer-use-engine": "6.1.0-edge.2",
|
|
60
|
+
"@memberjunction/config": "6.1.0-edge.2",
|
|
61
|
+
"@memberjunction/core": "6.1.0-edge.2",
|
|
62
|
+
"@memberjunction/core-actions": "6.1.0-edge.2",
|
|
63
|
+
"@memberjunction/core-entities": "6.1.0-edge.2",
|
|
64
|
+
"@memberjunction/core-entities-server": "6.1.0-edge.2",
|
|
65
|
+
"@memberjunction/credentials": "6.1.0-edge.2",
|
|
66
|
+
"@memberjunction/data-context": "6.1.0-edge.2",
|
|
67
|
+
"@memberjunction/data-context-server": "6.1.0-edge.2",
|
|
68
|
+
"@memberjunction/doc-utils": "6.1.0-edge.2",
|
|
69
|
+
"@memberjunction/encryption": "6.1.0-edge.2",
|
|
70
|
+
"@memberjunction/entity-communications-base": "6.1.0-edge.2",
|
|
71
|
+
"@memberjunction/entity-communications-server": "6.1.0-edge.2",
|
|
72
|
+
"@memberjunction/esignature": "6.1.0-edge.2",
|
|
73
|
+
"@memberjunction/external-change-detection": "6.1.0-edge.2",
|
|
74
|
+
"@memberjunction/generic-database-provider": "6.1.0-edge.2",
|
|
75
|
+
"@memberjunction/global": "6.1.0-edge.2",
|
|
76
|
+
"@memberjunction/graphql-dataprovider": "6.1.0-edge.2",
|
|
77
|
+
"@memberjunction/integration-engine": "6.1.0-edge.2",
|
|
78
|
+
"@memberjunction/integration-engine-base": "6.1.0-edge.2",
|
|
79
|
+
"@memberjunction/integration-progress-artifacts": "6.1.0-edge.2",
|
|
80
|
+
"@memberjunction/integration-schema-builder": "6.1.0-edge.2",
|
|
81
|
+
"@memberjunction/interactive-component-types": "6.1.0-edge.2",
|
|
82
|
+
"@memberjunction/lists": "6.1.0-edge.2",
|
|
83
|
+
"@memberjunction/lists-base": "6.1.0-edge.2",
|
|
84
|
+
"@memberjunction/livekit-room-server": "6.1.0-edge.2",
|
|
85
|
+
"@memberjunction/notifications": "6.1.0-edge.2",
|
|
86
|
+
"@memberjunction/postgresql-dataprovider": "6.1.0-edge.2",
|
|
87
|
+
"@memberjunction/queue": "6.1.0-edge.2",
|
|
88
|
+
"@memberjunction/record-comparison": "6.1.0-edge.2",
|
|
89
|
+
"@memberjunction/redis-provider": "6.1.0-edge.2",
|
|
90
|
+
"@memberjunction/remote-browser-base": "6.1.0-edge.2",
|
|
91
|
+
"@memberjunction/remote-browser-cdp": "6.1.0-edge.2",
|
|
92
|
+
"@memberjunction/remote-browser-selfhost": "6.1.0-edge.2",
|
|
93
|
+
"@memberjunction/remote-browser-server": "6.1.0-edge.2",
|
|
94
|
+
"@memberjunction/scheduling-actions": "6.1.0-edge.2",
|
|
95
|
+
"@memberjunction/scheduling-base-types": "6.1.0-edge.2",
|
|
96
|
+
"@memberjunction/scheduling-engine": "6.1.0-edge.2",
|
|
97
|
+
"@memberjunction/scheduling-engine-base": "6.1.0-edge.2",
|
|
98
|
+
"@memberjunction/schema-engine": "6.1.0-edge.2",
|
|
99
|
+
"@memberjunction/search-engine": "6.1.0-edge.2",
|
|
100
|
+
"@memberjunction/server-extensions-core": "6.1.0-edge.2",
|
|
101
|
+
"@memberjunction/sql-dialect": "6.1.0-edge.2",
|
|
102
|
+
"@memberjunction/sqlserver-dataprovider": "6.1.0-edge.2",
|
|
103
|
+
"@memberjunction/storage": "6.1.0-edge.2",
|
|
104
|
+
"@memberjunction/tag-engine": "6.1.0-edge.2",
|
|
105
|
+
"@memberjunction/tag-engine-base": "6.1.0-edge.2",
|
|
106
|
+
"@memberjunction/task-graph": "6.1.0-edge.2",
|
|
107
|
+
"@memberjunction/templates": "6.1.0-edge.2",
|
|
108
|
+
"@memberjunction/testing-engine": "6.1.0-edge.2",
|
|
109
|
+
"@memberjunction/testing-engine-base": "6.1.0-edge.2",
|
|
110
|
+
"@memberjunction/version-history": "6.1.0-edge.2",
|
|
110
111
|
"@octokit/auth-app": "^7.1.5",
|
|
111
112
|
"@octokit/rest": "^21.1.1",
|
|
112
113
|
"@types/compression": "^1.8.1",
|
|
@@ -148,6 +149,7 @@
|
|
|
148
149
|
"devDependencies": {
|
|
149
150
|
"@types/express": "^5.0.6",
|
|
150
151
|
"pkgroll": "2.23.0",
|
|
152
|
+
"rimraf": "6.1.2",
|
|
151
153
|
"typescript": "^5.9.3",
|
|
152
154
|
"vitest": "^3.1.1"
|
|
153
155
|
},
|
|
@@ -156,6 +158,7 @@
|
|
|
156
158
|
"url": "https://github.com/MemberJunction/MJ"
|
|
157
159
|
},
|
|
158
160
|
"scripts": {
|
|
161
|
+
"prebuild": "rimraf dist",
|
|
159
162
|
"build": "tsc && tsc-alias -f",
|
|
160
163
|
"bundle": "tsc -noEmit && pkgroll --sourcemap --minify",
|
|
161
164
|
"watch": "tsc -w",
|
|
@@ -148,7 +148,7 @@ vi.mock('../util.js', () => ({
|
|
|
148
148
|
// --- Partial mock: control ONLY UserCache.GetSystemUser (the scoped-anonymous elevation seam,
|
|
149
149
|
// issue #3371); everything else in the data provider stays real ---
|
|
150
150
|
const getSystemUserMock = vi.fn<[], UserInfo | undefined>();
|
|
151
|
-
vi.mock('@memberjunction/
|
|
151
|
+
vi.mock('@memberjunction/generic-database-provider', async (importOriginal) => {
|
|
152
152
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
153
153
|
return {
|
|
154
154
|
...actual,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// ResolverBase transitively pulls in type-graphql decorators, which need the
|
|
2
|
+
// Reflect.metadata polyfill at import time.
|
|
3
|
+
import 'reflect-metadata';
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
6
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `MapFieldNamesToCodeNames` must not write into the rows it is handed
|
|
10
|
+
* (PR #3425 review, finding M2).
|
|
11
|
+
*
|
|
12
|
+
* This is the SINGLE-RECORD half of the transport rename. The RunView half was fixed to
|
|
13
|
+
* copy-then-map, but this one still renamed `__mj_*` → `_mj__*` by writing onto its argument —
|
|
14
|
+
* and its arguments are frequently rows straight out of `findBy`/`RunView`, i.e. the server
|
|
15
|
+
* cache's own objects.
|
|
16
|
+
*
|
|
17
|
+
* Now that `LocalCacheManager` deep-freezes cached rows, that in-place rename does not corrupt
|
|
18
|
+
* the cache — it throws. Reproduced live against a running MJAPI before the fix:
|
|
19
|
+
*
|
|
20
|
+
* UserByEmail → "Cannot add property _mj__CreatedAt, object is not extensible"
|
|
21
|
+
*
|
|
22
|
+
* deterministically, on every call, because `MJ: Users` has caching enabled. `UserByID` and
|
|
23
|
+
* `UserByEmployeeID` share the code path, and so does every CodeGen-generated single-record
|
|
24
|
+
* resolver in `generated.ts` (`MapFieldNamesToCodeNames(entity, rows[0], ...)`), which is why
|
|
25
|
+
* the fix belongs in the shared helper rather than at the call sites.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Exposes the two protected mappers and supplies metadata without a live provider. */
|
|
29
|
+
class MappingProbe extends ResolverBase {
|
|
30
|
+
public MapOne(entityName: string, dataObject: unknown, provider: IMetadataProvider): Promise<unknown> {
|
|
31
|
+
return this.MapFieldNamesToCodeNames(entityName, dataObject, undefined, provider);
|
|
32
|
+
}
|
|
33
|
+
public MapMany(entityName: string, rows: unknown[], provider: IMetadataProvider, contextUser?: UserInfo): Promise<unknown[]> {
|
|
34
|
+
return this.ArrayMapFieldNamesToCodeNames(entityName, rows as Record<string, unknown>[], contextUser, provider);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ENTITY_NAME = 'MJ: Users';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Minimal metadata: the field set drives the rename, `EncryptedFields` empty so the
|
|
42
|
+
* EncryptionEngine is never consulted.
|
|
43
|
+
*/
|
|
44
|
+
function fakeProvider(): IMetadataProvider {
|
|
45
|
+
return {
|
|
46
|
+
EntityByName: (name: string) =>
|
|
47
|
+
name === ENTITY_NAME
|
|
48
|
+
? {
|
|
49
|
+
Name: ENTITY_NAME,
|
|
50
|
+
Fields: [
|
|
51
|
+
{ Name: 'ID', CodeName: 'ID' },
|
|
52
|
+
{ Name: 'Name', CodeName: 'Name' },
|
|
53
|
+
{ Name: '__mj_CreatedAt', CodeName: '__mj_CreatedAt' },
|
|
54
|
+
{ Name: '__mj_UpdatedAt', CodeName: '__mj_UpdatedAt' },
|
|
55
|
+
],
|
|
56
|
+
EncryptedFields: [],
|
|
57
|
+
}
|
|
58
|
+
: undefined,
|
|
59
|
+
} as unknown as IMetadataProvider;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A row as the cache hands it out: deep-frozen. */
|
|
63
|
+
function frozenCachedRow(): Record<string, unknown> {
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
ID: 'u-1',
|
|
66
|
+
Name: 'Ada',
|
|
67
|
+
__mj_CreatedAt: 'T0',
|
|
68
|
+
__mj_UpdatedAt: 'T1',
|
|
69
|
+
}) as Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('MapFieldNamesToCodeNames on frozen cache rows', () => {
|
|
73
|
+
it('maps a FROZEN row to transport keys instead of throwing (the live UserByEmail 500)', async () => {
|
|
74
|
+
const row = frozenCachedRow();
|
|
75
|
+
|
|
76
|
+
const mapped = (await new MappingProbe().MapOne(ENTITY_NAME, row, fakeProvider())) as Record<string, unknown>;
|
|
77
|
+
|
|
78
|
+
expect(mapped._mj__CreatedAt).toBe('T0');
|
|
79
|
+
expect(mapped._mj__UpdatedAt).toBe('T1');
|
|
80
|
+
expect(mapped.ID).toBe('u-1');
|
|
81
|
+
expect(mapped.Name).toBe('Ada');
|
|
82
|
+
// The transport aliases replace the originals in the OUTGOING shape.
|
|
83
|
+
expect(mapped.__mj_CreatedAt).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns a copy — the caller\'s row keeps its entity field names', async () => {
|
|
87
|
+
// Unfrozen input, so a regression to in-place mapping would silently pass the frozen
|
|
88
|
+
// test above only if it also stopped mapping. This one pins non-mutation directly.
|
|
89
|
+
const row: Record<string, unknown> = { ID: 'u-1', Name: 'Ada', __mj_CreatedAt: 'T0', __mj_UpdatedAt: 'T1' };
|
|
90
|
+
|
|
91
|
+
const mapped = (await new MappingProbe().MapOne(ENTITY_NAME, row, fakeProvider())) as Record<string, unknown>;
|
|
92
|
+
|
|
93
|
+
expect(row.__mj_CreatedAt).toBe('T0');
|
|
94
|
+
expect(row._mj__CreatedAt).toBeUndefined();
|
|
95
|
+
expect(mapped).not.toBe(row);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('still returns null for empty/absent input (contract unchanged)', async () => {
|
|
99
|
+
const probe = new MappingProbe();
|
|
100
|
+
expect(await probe.MapOne(ENTITY_NAME, null, fakeProvider())).toBeNull();
|
|
101
|
+
expect(await probe.MapOne(ENTITY_NAME, {}, fakeProvider())).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('leaves rows with no __mj_ fields structurally intact', async () => {
|
|
105
|
+
const row = Object.freeze({ ID: 'u-1', Name: 'Ada' }) as Record<string, unknown>;
|
|
106
|
+
|
|
107
|
+
const mapped = (await new MappingProbe().MapOne(ENTITY_NAME, row, fakeProvider())) as Record<string, unknown>;
|
|
108
|
+
|
|
109
|
+
expect(mapped).toEqual({ ID: 'u-1', Name: 'Ada' });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('ArrayMapFieldNamesToCodeNames on frozen cache rows', () => {
|
|
114
|
+
it('maps a frozen ARRAY of frozen rows without mutating either', async () => {
|
|
115
|
+
// The array itself is frozen too — LocalCacheManager freezes the array as well as the
|
|
116
|
+
// rows, because `results.sort()`/`.push()` on a live cache array is its own hazard.
|
|
117
|
+
const rows = Object.freeze([frozenCachedRow(), frozenCachedRow()]) as unknown as Record<string, unknown>[];
|
|
118
|
+
|
|
119
|
+
const mapped = (await new MappingProbe().MapMany(ENTITY_NAME, rows, fakeProvider())) as Record<string, unknown>[];
|
|
120
|
+
|
|
121
|
+
expect(mapped).toHaveLength(2);
|
|
122
|
+
expect(mapped[0]._mj__CreatedAt).toBe('T0');
|
|
123
|
+
expect(mapped[1]._mj__UpdatedAt).toBe('T1');
|
|
124
|
+
// Inputs untouched.
|
|
125
|
+
expect(rows[0].__mj_CreatedAt).toBe('T0');
|
|
126
|
+
expect(rows[0]._mj__CreatedAt).toBeUndefined();
|
|
127
|
+
expect(mapped[0]).not.toBe(rows[0]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('passes an empty array straight through', async () => {
|
|
131
|
+
expect(await new MappingProbe().MapMany(ENTITY_NAME, [], fakeProvider())).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// ResolverBase transitively pulls in type-graphql decorators, which need the
|
|
2
|
+
// Reflect.metadata polyfill at import time.
|
|
3
|
+
import 'reflect-metadata';
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import type { DatabaseProviderBase, RunViewParams, RunViewResult, UserInfo } from '@memberjunction/core';
|
|
6
|
+
import type { MJUserViewEntityExtended } from '@memberjunction/core-entities';
|
|
7
|
+
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
8
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
9
|
+
import type { UserPayload } from '../types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Regression guard for the server-cache corruption behind
|
|
13
|
+
* "Field _mj__CreatedAt does not exist on MJ: Template Categories".
|
|
14
|
+
*
|
|
15
|
+
* GraphQL reserves the `__` prefix, so MJ renames `__mj_*` columns to the
|
|
16
|
+
* transport alias `_mj__*` on the way out. `ResolverBase` used to apply that
|
|
17
|
+
* rename IN PLACE over `result.Results` — but those are the data provider's own
|
|
18
|
+
* row objects, and the server's cache holds them BY REFERENCE. Preparing one
|
|
19
|
+
* GraphQL response therefore rewrote the keys inside the live cache, and every
|
|
20
|
+
* subsequent read served from it handed the client transport-shaped rows that
|
|
21
|
+
* `BaseEntity.SetMany` rejects. Process-wide cache ⇒ one response poisoned every
|
|
22
|
+
* later request, across all workers.
|
|
23
|
+
*
|
|
24
|
+
* The fix is copy-then-map. The first block exercises the REAL resolver path (so a
|
|
25
|
+
* revert to in-place mapping fails here); the second pins the two `FieldMapper`
|
|
26
|
+
* invariants that make the fix correct.
|
|
27
|
+
*
|
|
28
|
+
* These copies are now LOAD-BEARING rather than merely defensive: `LocalCacheManager`
|
|
29
|
+
* deep-freezes cached rows on reference-sharing storage providers, so mapping in place
|
|
30
|
+
* would throw a `TypeError` instead of silently corrupting the cache. The third block
|
|
31
|
+
* pins that the copy-then-map pattern still works on frozen input — the shape the cache
|
|
32
|
+
* actually hands out — and that in-place mapping is now refused outright.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** A cache-held row, shaped the way the provider hands them back. */
|
|
36
|
+
type CachedRow = Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reaches the protected `RunViewGenericInternal` and records what
|
|
40
|
+
* `ArrayFilterEncryptedFieldsForAPI` was handed — the second in-place mutator that
|
|
41
|
+
* must also see copies, and stubbed here so the assertion needs no live Metadata.
|
|
42
|
+
*/
|
|
43
|
+
class Probe extends ResolverBase {
|
|
44
|
+
public filteredRows: Record<string, unknown>[] | null = null;
|
|
45
|
+
|
|
46
|
+
protected override async ArrayFilterEncryptedFieldsForAPI(
|
|
47
|
+
_entityName: string,
|
|
48
|
+
dataObjectArray: Record<string, unknown>[]
|
|
49
|
+
): Promise<Record<string, unknown>[]> {
|
|
50
|
+
this.filteredRows = dataObjectArray;
|
|
51
|
+
return dataObjectArray;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public Run(provider: DatabaseProviderBase, viewInfo: MJUserViewEntityExtended, userPayload: UserPayload) {
|
|
55
|
+
return this.RunViewGenericInternal(
|
|
56
|
+
provider,
|
|
57
|
+
viewInfo,
|
|
58
|
+
'', // extraFilter
|
|
59
|
+
'', // orderBy
|
|
60
|
+
'', // userSearchString
|
|
61
|
+
undefined, // excludeUserViewRunID
|
|
62
|
+
undefined, // overrideExcludeFilter
|
|
63
|
+
undefined, // saveViewResults
|
|
64
|
+
undefined, // fields
|
|
65
|
+
undefined, // ignoreMaxRows
|
|
66
|
+
undefined, // excludeDataFromAllPriorViewRuns
|
|
67
|
+
undefined, // forceAuditLog
|
|
68
|
+
undefined, // auditLogDescription
|
|
69
|
+
'simple', // resultType
|
|
70
|
+
userPayload,
|
|
71
|
+
undefined, // maxRows
|
|
72
|
+
undefined // startRow
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ENTITY_NAME = 'MJ: Template Categories';
|
|
78
|
+
|
|
79
|
+
/** Minimal provider: the entity lookup + the RunView the resolver awaits. */
|
|
80
|
+
function fakeProvider(rows: CachedRow[]): DatabaseProviderBase {
|
|
81
|
+
return {
|
|
82
|
+
Entities: [{ Name: ENTITY_NAME, PrimaryKeys: [{ Name: 'ID' }] }],
|
|
83
|
+
RunView: async (_params: RunViewParams): Promise<RunViewResult> =>
|
|
84
|
+
({ Success: true, Results: rows, RowCount: rows.length, TotalRowCount: rows.length, ErrorMessage: '' } as RunViewResult),
|
|
85
|
+
} as unknown as DatabaseProviderBase;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fakeViewInfo = () =>
|
|
89
|
+
({ ID: 'view-1', Name: 'Test View', Entity: ENTITY_NAME } as unknown as MJUserViewEntityExtended);
|
|
90
|
+
|
|
91
|
+
/** `userRecord` short-circuits the UserCache lookup; no apiKeyHash skips the scope check. */
|
|
92
|
+
const fakePayload = () =>
|
|
93
|
+
({ email: 'tester@example.com', userRecord: { Email: 'tester@example.com' } as UserInfo } as UserPayload);
|
|
94
|
+
|
|
95
|
+
describe('ResolverBase.RunViewGenericInternal — cache safety', () => {
|
|
96
|
+
it('leaves the provider\'s (cache-held) rows untouched while returning transport keys', async () => {
|
|
97
|
+
const cachedRow: CachedRow = { ID: 'a1', Name: 'Cat', __mj_CreatedAt: 'T0', __mj_UpdatedAt: 'T1' };
|
|
98
|
+
const probe = new Probe();
|
|
99
|
+
|
|
100
|
+
const result = await probe.Run(fakeProvider([cachedRow]), fakeViewInfo(), fakePayload());
|
|
101
|
+
|
|
102
|
+
// The pre-fix code renamed these keys in place — this is the assertion that fails on a revert.
|
|
103
|
+
expect(cachedRow.__mj_CreatedAt).toBe('T0');
|
|
104
|
+
expect(cachedRow.__mj_UpdatedAt).toBe('T1');
|
|
105
|
+
expect(cachedRow._mj__CreatedAt).toBeUndefined();
|
|
106
|
+
|
|
107
|
+
// The outgoing rows carry the GraphQL-legal aliases, and are NOT the cached objects.
|
|
108
|
+
const wire = result?.Results as Record<string, unknown>[];
|
|
109
|
+
expect(wire[0]._mj__CreatedAt).toBe('T0');
|
|
110
|
+
expect(wire[0]).not.toBe(cachedRow);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('hands the encrypted-field filter the copies, not the cached rows', async () => {
|
|
114
|
+
const cachedRow: CachedRow = { ID: 'a1', Secret: 'plaintext', __mj_CreatedAt: 'T0' };
|
|
115
|
+
const probe = new Probe();
|
|
116
|
+
|
|
117
|
+
await probe.Run(fakeProvider([cachedRow]), fakeViewInfo(), fakePayload());
|
|
118
|
+
|
|
119
|
+
expect(probe.filteredRows).not.toBeNull();
|
|
120
|
+
expect(probe.filteredRows![0]).not.toBe(cachedRow);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('FieldMapper transport mapping — the invariants the fix rests on', () => {
|
|
125
|
+
it('MapFields MUTATES its argument (the hazard the resolver must not expose the cache to)', () => {
|
|
126
|
+
// Documents WHY the resolver must copy. If this ever becomes non-mutating,
|
|
127
|
+
// the copy in ResolverBase is redundant rather than load-bearing — and this
|
|
128
|
+
// test failing is the signal to revisit it.
|
|
129
|
+
const row: Record<string, unknown> = { ID: 'a1', Name: 'Cat', __mj_CreatedAt: 'T0' };
|
|
130
|
+
|
|
131
|
+
new FieldMapper().MapFields(row);
|
|
132
|
+
|
|
133
|
+
expect(row.__mj_CreatedAt).toBeUndefined();
|
|
134
|
+
expect(row._mj__CreatedAt).toBe('T0');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('post-map mutation of the copy cannot reach the cached row', () => {
|
|
138
|
+
// ArrayFilterEncryptedFieldsForAPI redacts in place after mapping; on the
|
|
139
|
+
// pre-fix code that stripped secrets out of the CACHED row too.
|
|
140
|
+
const mapper = new FieldMapper();
|
|
141
|
+
const cachedRow: Record<string, unknown> = { ID: 'a1', Secret: 'plaintext', __mj_CreatedAt: 'T0' };
|
|
142
|
+
|
|
143
|
+
const wireRow = mapper.MapFields({ ...cachedRow })!;
|
|
144
|
+
wireRow.Secret = null; // stand-in for the encrypted-field filter
|
|
145
|
+
|
|
146
|
+
expect(cachedRow.Secret).toBe('plaintext');
|
|
147
|
+
expect(wireRow.Secret).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('round-trips a mapped copy back to entity field names', () => {
|
|
151
|
+
// The client's ConvertBackToMJFields must undo exactly what the server did.
|
|
152
|
+
const mapper = new FieldMapper();
|
|
153
|
+
const original: Record<string, unknown> = { ID: 'a1', __mj_CreatedAt: 'T0', __mj_UpdatedAt: 'T1' };
|
|
154
|
+
|
|
155
|
+
const wire = mapper.MapFields({ ...original })!;
|
|
156
|
+
const back = mapper.ReverseMapFields({ ...wire });
|
|
157
|
+
|
|
158
|
+
expect(back).toEqual(original);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('leaves rows without __mj_ fields structurally unchanged', () => {
|
|
162
|
+
const mapper = new FieldMapper();
|
|
163
|
+
const row = { ID: 'a1', Name: 'Cat' };
|
|
164
|
+
|
|
165
|
+
expect(mapper.MapFields({ ...row })).toEqual(row);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('transport mapping against FROZEN cache rows', () => {
|
|
170
|
+
// The cache deep-freezes rows it hands out (LocalCacheManager freeze-on-write), so the
|
|
171
|
+
// resolver's input is frozen in the deployment that matters — MJAPI on the in-memory
|
|
172
|
+
// storage provider. That turns the copy from a precaution into a requirement.
|
|
173
|
+
it('copy-then-map works on a frozen row', () => {
|
|
174
|
+
const mapper = new FieldMapper();
|
|
175
|
+
const cachedRow = Object.freeze({ ID: 'a1', Name: 'Cat', __mj_CreatedAt: 'T0' }) as Record<string, unknown>;
|
|
176
|
+
|
|
177
|
+
const wireRow = mapper.MapFields({ ...cachedRow })!;
|
|
178
|
+
|
|
179
|
+
expect(wireRow._mj__CreatedAt).toBe('T0');
|
|
180
|
+
expect(wireRow.__mj_CreatedAt).toBeUndefined();
|
|
181
|
+
// The copy is mutable, so the encrypted-field filter can still redact in place.
|
|
182
|
+
expect(Object.isFrozen(wireRow)).toBe(false);
|
|
183
|
+
// And the frozen cached row is untouched.
|
|
184
|
+
expect(cachedRow.__mj_CreatedAt).toBe('T0');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('in-place mapping of a frozen row THROWS — silent corruption is now impossible', () => {
|
|
188
|
+
// This is the safety net the freeze adds on top of the copy: if a future refactor
|
|
189
|
+
// reverts to in-place mapping, it fails loudly at the offending line instead of
|
|
190
|
+
// quietly rewriting process-wide state.
|
|
191
|
+
const mapper = new FieldMapper();
|
|
192
|
+
const cachedRow = Object.freeze({ ID: 'a1', __mj_CreatedAt: 'T0' }) as Record<string, unknown>;
|
|
193
|
+
|
|
194
|
+
expect(() => mapper.MapFields(cachedRow)).toThrow(TypeError);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The BATCH path (`RunViewsGenericInternal`), which the single-view tests above do not reach
|
|
200
|
+
* (PR #3425 review, finding M8).
|
|
201
|
+
*
|
|
202
|
+
* This is the path multi-view clients actually take — MJExplorer batches its view loads — so
|
|
203
|
+
* leaving it uncovered meant the original corruption bug could be reintroduced on the more
|
|
204
|
+
* heavily used of the two legs with CI still green.
|
|
205
|
+
*/
|
|
206
|
+
class BatchProbe extends ResolverBase {
|
|
207
|
+
public filteredRows: Record<string, unknown>[] | null = null;
|
|
208
|
+
|
|
209
|
+
protected override async ArrayFilterEncryptedFieldsForAPI(
|
|
210
|
+
_entityName: string,
|
|
211
|
+
dataObjectArray: Record<string, unknown>[]
|
|
212
|
+
): Promise<Record<string, unknown>[]> {
|
|
213
|
+
this.filteredRows = dataObjectArray;
|
|
214
|
+
return dataObjectArray;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public RunBatch(provider: DatabaseProviderBase, viewInfos: MJUserViewEntityExtended[], userPayload: UserPayload) {
|
|
218
|
+
return this.RunViewsGenericInternal(
|
|
219
|
+
viewInfos.map(viewInfo => ({ provider, viewInfo, userPayload, resultType: 'simple' })) as never
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Batch provider: `RunViews` returns one result per param, in order. */
|
|
225
|
+
function fakeBatchProvider(rowsPerView: CachedRow[][]): DatabaseProviderBase {
|
|
226
|
+
return {
|
|
227
|
+
Entities: [{ Name: ENTITY_NAME, PrimaryKeys: [{ Name: 'ID' }] }],
|
|
228
|
+
EntityByName: (name: string) => (name === ENTITY_NAME ? { Name: ENTITY_NAME } : undefined),
|
|
229
|
+
RunViews: async (params: RunViewParams[]): Promise<RunViewResult[]> =>
|
|
230
|
+
params.map((_p, i) => {
|
|
231
|
+
const rows = rowsPerView[i] ?? [];
|
|
232
|
+
return { Success: true, Results: rows, RowCount: rows.length, TotalRowCount: rows.length, ErrorMessage: '' } as RunViewResult;
|
|
233
|
+
}),
|
|
234
|
+
} as unknown as DatabaseProviderBase;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
describe('ResolverBase.RunViewsGenericInternal (batch) — cache safety', () => {
|
|
238
|
+
it('leaves every view\'s cache-held rows untouched while returning transport keys', async () => {
|
|
239
|
+
const viewARow: CachedRow = { ID: 'a1', Name: 'Cat', __mj_CreatedAt: 'T0', __mj_UpdatedAt: 'T1' };
|
|
240
|
+
const viewBRow: CachedRow = { ID: 'b1', Name: 'Dog', __mj_CreatedAt: 'T2', __mj_UpdatedAt: 'T3' };
|
|
241
|
+
const probe = new BatchProbe();
|
|
242
|
+
|
|
243
|
+
const results = await probe.RunBatch(
|
|
244
|
+
fakeBatchProvider([[viewARow], [viewBRow]]),
|
|
245
|
+
[fakeViewInfo(), fakeViewInfo()],
|
|
246
|
+
fakePayload()
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Both source rows keep entity field names — the assertion that fails on a revert
|
|
250
|
+
// to in-place mapping in the batch loop.
|
|
251
|
+
expect(viewARow.__mj_CreatedAt).toBe('T0');
|
|
252
|
+
expect(viewARow._mj__CreatedAt).toBeUndefined();
|
|
253
|
+
expect(viewBRow.__mj_CreatedAt).toBe('T2');
|
|
254
|
+
expect(viewBRow._mj__CreatedAt).toBeUndefined();
|
|
255
|
+
|
|
256
|
+
// ...and both outgoing views carry the aliases, on objects that are not the cached ones.
|
|
257
|
+
const wireA = results[0].Results as Record<string, unknown>[];
|
|
258
|
+
const wireB = results[1].Results as Record<string, unknown>[];
|
|
259
|
+
expect(wireA[0]._mj__CreatedAt).toBe('T0');
|
|
260
|
+
expect(wireB[0]._mj__CreatedAt).toBe('T2');
|
|
261
|
+
expect(wireA[0]).not.toBe(viewARow);
|
|
262
|
+
expect(wireB[0]).not.toBe(viewBRow);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('maps FROZEN cache rows rather than throwing on them', async () => {
|
|
266
|
+
// The shape the cache actually hands out on a hit.
|
|
267
|
+
const frozenRow = Object.freeze({ ID: 'a1', Name: 'Cat', __mj_CreatedAt: 'T0' }) as CachedRow;
|
|
268
|
+
const probe = new BatchProbe();
|
|
269
|
+
|
|
270
|
+
const results = await probe.RunBatch(
|
|
271
|
+
fakeBatchProvider([[frozenRow]]),
|
|
272
|
+
[fakeViewInfo()],
|
|
273
|
+
fakePayload()
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect((results[0].Results as Record<string, unknown>[])[0]._mj__CreatedAt).toBe('T0');
|
|
277
|
+
expect(frozenRow.__mj_CreatedAt).toBe('T0');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('hands the encrypted-field filter copies, not the cached rows', async () => {
|
|
281
|
+
const cachedRow: CachedRow = { ID: 'a1', Secret: 'plaintext', __mj_CreatedAt: 'T0' };
|
|
282
|
+
const probe = new BatchProbe();
|
|
283
|
+
|
|
284
|
+
await probe.RunBatch(fakeBatchProvider([[cachedRow]]), [fakeViewInfo()], fakePayload());
|
|
285
|
+
|
|
286
|
+
expect(probe.filteredRows).not.toBeNull();
|
|
287
|
+
expect(probe.filteredRows![0]).not.toBe(cachedRow);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -43,7 +43,7 @@ vi.mock('@memberjunction/core', async (importOriginal) => {
|
|
|
43
43
|
// --- Partial mock: control ONLY UserCache.GetSystemUser (the scoped-anonymous elevation seam,
|
|
44
44
|
// issue #3371); everything else in the data provider stays real ---
|
|
45
45
|
const getSystemUserMock = vi.fn<[], UserInfo | undefined>();
|
|
46
|
-
vi.mock('@memberjunction/
|
|
46
|
+
vi.mock('@memberjunction/generic-database-provider', async (importOriginal) => {
|
|
47
47
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
48
48
|
return {
|
|
49
49
|
...actual,
|