@memberjunction/server 6.1.4 → 6.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/README.md +30 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/SessionManager.d.ts +5 -2
  4. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  5. package/dist/agentSessions/SessionManager.js +17 -8
  6. package/dist/agentSessions/SessionManager.js.map +1 -1
  7. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  8. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  9. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  10. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  11. package/dist/agentSessions/index.d.ts +1 -0
  12. package/dist/agentSessions/index.d.ts.map +1 -1
  13. package/dist/agentSessions/index.js +1 -0
  14. package/dist/agentSessions/index.js.map +1 -1
  15. package/dist/auth/actionAuthorization.d.ts +40 -0
  16. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  17. package/dist/auth/actionAuthorization.js +94 -0
  18. package/dist/auth/actionAuthorization.js.map +1 -0
  19. package/dist/auth/exampleNewUserSubClass.js +2 -1
  20. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  21. package/dist/auth/magicLink/MagicLinkService.d.ts +17 -15
  22. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  23. package/dist/auth/magicLink/MagicLinkService.js +55 -38
  24. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  25. package/dist/auth/magicLink/magicLinkCore.d.ts +14 -33
  26. package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
  27. package/dist/auth/magicLink/magicLinkCore.js +20 -52
  28. package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
  29. package/dist/auth/newUsers.d.ts.map +1 -1
  30. package/dist/auth/newUsers.js +15 -11
  31. package/dist/auth/newUsers.js.map +1 -1
  32. package/dist/context.d.ts +6 -0
  33. package/dist/context.d.ts.map +1 -1
  34. package/dist/context.js +24 -34
  35. package/dist/context.js.map +1 -1
  36. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  37. package/dist/generated/graphql-schemas/__mj.js +3417 -3428
  38. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  39. package/dist/generic/CacheInvalidationResolver.d.ts +44 -8
  40. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  41. package/dist/generic/CacheInvalidationResolver.js +59 -19
  42. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  43. package/dist/generic/ResolverBase.d.ts +95 -11
  44. package/dist/generic/ResolverBase.d.ts.map +1 -1
  45. package/dist/generic/ResolverBase.js +186 -76
  46. package/dist/generic/ResolverBase.js.map +1 -1
  47. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  48. package/dist/generic/RunViewResolver.js +12 -0
  49. package/dist/generic/RunViewResolver.js.map +1 -1
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +44 -45
  53. package/dist/index.js.map +1 -1
  54. package/dist/isolatedProvider.d.ts +20 -0
  55. package/dist/isolatedProvider.d.ts.map +1 -0
  56. package/dist/isolatedProvider.js +22 -0
  57. package/dist/isolatedProvider.js.map +1 -0
  58. package/dist/multiTenancy/index.d.ts.map +1 -1
  59. package/dist/multiTenancy/index.js +22 -2
  60. package/dist/multiTenancy/index.js.map +1 -1
  61. package/dist/postgresqlCompat.d.ts +8 -0
  62. package/dist/postgresqlCompat.d.ts.map +1 -0
  63. package/dist/postgresqlCompat.js +19 -0
  64. package/dist/postgresqlCompat.js.map +1 -0
  65. package/dist/postgresqlPoolSettings.d.ts +108 -0
  66. package/dist/postgresqlPoolSettings.d.ts.map +1 -0
  67. package/dist/postgresqlPoolSettings.js +134 -0
  68. package/dist/postgresqlPoolSettings.js.map +1 -0
  69. package/dist/realtimeWidget/widgetGuestElevation.d.ts +4 -2
  70. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  71. package/dist/realtimeWidget/widgetGuestElevation.js +4 -2
  72. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  73. package/dist/resolvers/ActionResolver.d.ts +31 -5
  74. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  75. package/dist/resolvers/ActionResolver.js +75 -14
  76. package/dist/resolvers/ActionResolver.js.map +1 -1
  77. package/dist/resolvers/AdhocQueryResolver.d.ts +35 -17
  78. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  79. package/dist/resolvers/AdhocQueryResolver.js +92 -166
  80. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  81. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  82. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  83. package/dist/resolvers/DatasetResolver.js +57 -8
  84. package/dist/resolvers/DatasetResolver.js.map +1 -1
  85. package/dist/resolvers/EntityRecordNameResolver.d.ts +8 -23
  86. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  87. package/dist/resolvers/EntityRecordNameResolver.js +11 -48
  88. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  89. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
  90. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  91. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  92. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  93. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  94. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  95. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  96. package/dist/resolvers/GetDataResolver.js +13 -1
  97. package/dist/resolvers/GetDataResolver.js.map +1 -1
  98. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  99. package/dist/resolvers/QuerySystemUserResolver.js +2 -1
  100. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  101. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +83 -22
  102. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  103. package/dist/resolvers/RealtimeClientSessionResolver.js +174 -72
  104. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  105. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  106. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  107. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  108. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  109. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  110. package/dist/resolvers/RunAIAgentResolver.js +2 -1
  111. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  112. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  113. package/dist/resolvers/RunTemplateResolver.js +12 -11
  114. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  115. package/dist/resolvers/SearchKnowledgeResolver.d.ts +1 -0
  116. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  117. package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
  118. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  119. package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
  120. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
  121. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
  122. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  123. package/dist/resolvers/SyncDataResolver.js +6 -2
  124. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  125. package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
  126. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  127. package/dist/resolvers/VectorizeEntityResolver.js +14 -10
  128. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  129. package/dist/resolvers/adhocTimeout.d.ts +7 -0
  130. package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
  131. package/dist/resolvers/adhocTimeout.js +12 -0
  132. package/dist/resolvers/adhocTimeout.js.map +1 -0
  133. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  134. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  135. package/dist/rest/ViewOperationsHandler.js +29 -17
  136. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  137. package/dist/services/TaskGraphActionRunner.d.ts +21 -8
  138. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  139. package/dist/services/TaskGraphActionRunner.js +43 -16
  140. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  141. package/package.json +92 -92
  142. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  143. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
  144. package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
  145. package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
  146. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  147. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  148. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
  149. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  150. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  151. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  152. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  153. package/src/__tests__/RealtimeClientSessionResolver.test.ts +310 -31
  154. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  155. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +227 -0
  156. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  157. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  158. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  159. package/src/__tests__/ResolverBase.filterEscaping.test.ts +24 -2
  160. package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
  161. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  162. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  163. package/src/__tests__/SessionManager.test.ts +94 -17
  164. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  165. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +131 -0
  166. package/src/__tests__/TaskGraphActionRunner.storableOutputs.test.ts +76 -0
  167. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  168. package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
  169. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  170. package/src/__tests__/actionAuthorization.test.ts +130 -0
  171. package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
  172. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  173. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  174. package/src/__tests__/magicLink.test.ts +15 -76
  175. package/src/__tests__/magicLinkService.consume.test.ts +230 -0
  176. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  177. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  178. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  179. package/src/__tests__/multiTenancy.test.ts +1 -1
  180. package/src/__tests__/newUsers.test.ts +62 -5
  181. package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
  182. package/src/__tests__/translateBracketsToPG.test.ts +20 -0
  183. package/src/agentSessions/SessionManager.ts +23 -25
  184. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  185. package/src/agentSessions/index.ts +1 -0
  186. package/src/auth/actionAuthorization.ts +119 -0
  187. package/src/auth/exampleNewUserSubClass.ts +2 -2
  188. package/src/auth/magicLink/MagicLinkService.ts +61 -39
  189. package/src/auth/magicLink/magicLinkCore.ts +19 -56
  190. package/src/auth/newUsers.ts +15 -11
  191. package/src/context.ts +25 -34
  192. package/src/generated/graphql-schemas/__mj.ts +3417 -3428
  193. package/src/generic/CacheInvalidationResolver.ts +74 -19
  194. package/src/generic/ResolverBase.ts +207 -80
  195. package/src/generic/RunViewResolver.ts +17 -0
  196. package/src/index.ts +49 -46
  197. package/src/isolatedProvider.ts +22 -0
  198. package/src/multiTenancy/index.ts +24 -2
  199. package/src/postgresqlCompat.ts +21 -0
  200. package/src/postgresqlPoolSettings.ts +213 -0
  201. package/src/realtimeWidget/widgetGuestElevation.ts +4 -2
  202. package/src/resolvers/ActionResolver.ts +84 -21
  203. package/src/resolvers/AdhocQueryResolver.ts +105 -187
  204. package/src/resolvers/DatasetResolver.ts +70 -10
  205. package/src/resolvers/EntityRecordNameResolver.ts +11 -52
  206. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
  207. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  208. package/src/resolvers/GetDataResolver.ts +14 -1
  209. package/src/resolvers/QuerySystemUserResolver.ts +2 -1
  210. package/src/resolvers/RealtimeClientSessionResolver.ts +196 -77
  211. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  212. package/src/resolvers/RunAIAgentResolver.ts +4 -1
  213. package/src/resolvers/RunTemplateResolver.ts +13 -12
  214. package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
  215. package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
  216. package/src/resolvers/SyncDataResolver.ts +6 -2
  217. package/src/resolvers/VectorizeEntityResolver.ts +16 -11
  218. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  219. package/src/resolvers/adhocTimeout.ts +12 -0
  220. package/src/rest/ViewOperationsHandler.ts +37 -18
  221. package/src/services/TaskGraphActionRunner.ts +45 -16
  222. package/dist/resolvers/adhoc-query-helpers.d.ts +0 -27
  223. package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
  224. package/dist/resolvers/adhoc-query-helpers.js +0 -42
  225. package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
  226. package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
  227. package/src/resolvers/adhoc-query-helpers.ts +0 -50
@@ -0,0 +1,131 @@
1
+ /**
2
+ * End-to-end-in-miniature regression test for #4794.
3
+ *
4
+ * The bug: `BuildDurableDeferral` persisted a durable entity action's redacted params as a
5
+ * `LoggedParam[]` ARRAY (`RedactParamsToJSON`'s shape), while `TaskGraphActionRunner.buildParams`
6
+ * reads `Task.InputPayload` back as a name→value RECORD. Every released build with durable dispatch
7
+ * (v6.1.0 onward, including 6.1.4) hit the same real-world failure: `TaskGraphDispatcher`'s
8
+ * `mergedPayload` only merges plain objects, so it silently dropped the array before `buildParams`
9
+ * ever saw it, and the action ran with NONE of its declared inputs. `buildParams` itself has no such
10
+ * guard — `Object.assign(merged, arrayValue)` treats an array's indices as keys — so params named
11
+ * `'0'`, `'1'`, ... would only appear if the array reached it directly, bypassing the dispatcher's
12
+ * drop. That narrower path is exactly what this test exercises, by calling `RunActionForTask`
13
+ * on its own rather than through the dispatcher.
14
+ *
15
+ * This test proves the whole pipe, not just the writer or the reader in isolation: build the
16
+ * payload exactly as the deferral does (`RedactParamsToRecord`), push it through the same
17
+ * `JSON.stringify` / `JSON.parse` round trip `TaskGraphService` performs to persist and reload
18
+ * `Task.InputPayload`, then run it through the real `TaskGraphActionRunner` and assert the action
19
+ * receives its declared parameters BY NAME.
20
+ */
21
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
22
+
23
+ vi.mock('@memberjunction/actions', () => ({
24
+ ActionEngineServer: {
25
+ Instance: {
26
+ Config: vi.fn(),
27
+ Actions: [{ ID: 'A1', Name: 'Log Activity' }],
28
+ RunAction: vi.fn(),
29
+ },
30
+ },
31
+ }));
32
+
33
+ import { ActionEngineServer } from '@memberjunction/actions';
34
+ import { RedactParamsToRecord, ActionParam } from '@memberjunction/actions-base';
35
+ import type { MJActionExecutionLogEntity, MJActionParamEntity, MJEntityActionParamEntity } from '@memberjunction/core-entities';
36
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
37
+ import type { TaskActionRunParams } from '@memberjunction/task-graph';
38
+ import { TaskGraphActionRunner } from '../services/TaskGraphActionRunner';
39
+
40
+ // ── Test fixture builders — same shape as packages/Actions/Base/src/__tests__/ParamRedaction.test.ts ──
41
+
42
+ function actionParam(name: string, value: unknown): ActionParam {
43
+ return { Name: name, Value: value, Type: 'Input' } as ActionParam;
44
+ }
45
+
46
+ function definition(id: string, name: string, logValue: boolean = true): MJActionParamEntity {
47
+ return { ID: id, Name: name, LogValue: logValue } as MJActionParamEntity;
48
+ }
49
+
50
+ function binding(actionParamID: string, valueType: string, logValue: boolean | null = null): MJEntityActionParamEntity {
51
+ return { ActionParamID: actionParamID, ValueType: valueType, LogValue: logValue } as MJEntityActionParamEntity;
52
+ }
53
+
54
+ // The declared parameters for the durable entity action under test: three that log normally, and
55
+ // one ("InternalNotes") whose binding opts out of logging (rule 2 in ParamRedaction.ts).
56
+ const RUNTIME_PARAMS: ActionParam[] = [
57
+ actionParam('TypeCode', 'SystemEvent'),
58
+ actionParam('Title', 'Person created'),
59
+ actionParam('RecordID', 'REC-123'),
60
+ actionParam('InternalNotes', 'do not persist this'),
61
+ ];
62
+ const ACTION_PARAMS: MJActionParamEntity[] = [
63
+ definition('p1', 'TypeCode'),
64
+ definition('p2', 'Title'),
65
+ definition('p3', 'RecordID'),
66
+ definition('p4', 'InternalNotes'),
67
+ ];
68
+ const ENTITY_ACTION_PARAMS: MJEntityActionParamEntity[] = [binding('p4', 'Static', false)];
69
+
70
+ function baseRunParams(inputPayload: unknown): TaskActionRunParams {
71
+ return {
72
+ TaskID: 'T1',
73
+ ActionID: 'A1',
74
+ InputPayload: inputPayload,
75
+ DependencyOutputs: new Map(),
76
+ Provider: {} as IMetadataProvider,
77
+ ContextUser: {} as UserInfo,
78
+ };
79
+ }
80
+
81
+ describe('TaskGraphActionRunner — durable payload round-trips by param name (#4794)', () => {
82
+ beforeEach(() => {
83
+ // vitest's `restoreMocks: true` clears mock implementations before every test, including
84
+ // the ones assigned inside the `vi.mock` factory above — so the return value is (re)armed here.
85
+ // Call history on a vi.fn created inside vi.mock survives that restore, so clear it too.
86
+ vi.mocked(ActionEngineServer.Instance.Config).mockResolvedValue(undefined);
87
+ vi.mocked(ActionEngineServer.Instance.RunAction, { partial: true }).mockClear();
88
+ vi.mocked(ActionEngineServer.Instance.RunAction, { partial: true }).mockResolvedValue({
89
+ Success: true,
90
+ Params: [],
91
+ LogEntry: { ID: 'L1' } as MJActionExecutionLogEntity,
92
+ });
93
+ });
94
+
95
+ it('delivers RedactParamsToRecord\'s output to the action by name, after a JSON round trip', async () => {
96
+ // Exactly what the deferral writes, then exactly what TaskGraphService's JSON column does to it.
97
+ const stored = JSON.parse(JSON.stringify(RedactParamsToRecord(RUNTIME_PARAMS, ACTION_PARAMS, ENTITY_ACTION_PARAMS)));
98
+
99
+ const result = await new TaskGraphActionRunner().RunActionForTask(baseRunParams(stored));
100
+
101
+ expect(result.Success).toBe(true);
102
+ expect(result.ActionLogID).toBe('L1');
103
+
104
+ const runActionCall = vi.mocked(ActionEngineServer.Instance.RunAction).mock.calls[0][0];
105
+ const sentParams = runActionCall.Params as ActionParam[];
106
+
107
+ // The three logged declared names arrive, order-insensitive, with their values intact.
108
+ expect(sentParams.map((p) => p.Name).sort()).toEqual(['RecordID', 'Title', 'TypeCode']);
109
+ expect(sentParams.find((p) => p.Name === 'TypeCode')?.Value).toBe('SystemEvent');
110
+ expect(sentParams.find((p) => p.Name === 'Title')?.Value).toBe('Person created');
111
+ expect(sentParams.find((p) => p.Name === 'RecordID')?.Value).toBe('REC-123');
112
+
113
+ // The binding-suppressed parameter never reaches the action — absent, not present-with-undefined.
114
+ expect(sentParams.some((p) => p.Name === 'InternalNotes')).toBe(false);
115
+ });
116
+
117
+ it('does not turn dependency outputs into params named by the upstream task ID', async () => {
118
+ // The dispatcher keys DependencyOutputs by the upstream task's ID and has ALREADY merged their
119
+ // values into InputPayload (TaskGraphDispatcher.mergedPayload). A runner that also spread the
120
+ // map handed the action one extra param per dependency, named by a GUID and holding that
121
+ // task's whole output — which then landed, unredacted, in ActionExecutionLog.Params.
122
+ const upstreamID = '20924BD7-5128-4150-8404-FB44A55AEA0F';
123
+ const params = baseRunParams({ EntityName: 'MJ: Entities', Record: { ID: 'R1', Name: 'MJ: Tags' } });
124
+ params.DependencyOutputs = new Map([[upstreamID, { Record: { ID: 'R1', Name: 'MJ: Tags' } }]]);
125
+
126
+ await new TaskGraphActionRunner().RunActionForTask(params);
127
+
128
+ const sentParams = vi.mocked(ActionEngineServer.Instance.RunAction).mock.calls[0][0].Params as ActionParam[];
129
+ expect(sentParams.map((p) => p.Name).sort()).toEqual(['EntityName', 'Record']);
130
+ });
131
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * A durable step's Output is stored on the Task row as JSON. An action that hands back an output
3
+ * param holding a live object (an entity, an observable) used to surface much later, from the
4
+ * dispatcher, as a bare "Converting circular structure to JSON" with no hint of WHICH action or
5
+ * param — seen live with Execute Agent's AgentResult. The runner is the boundary that knows both
6
+ * names, so it refuses the output there and says which param it was.
7
+ *
8
+ * Backported from `next` (#4963), where these cases live in TaskGraphActionRunner.durablePayload.test.ts;
9
+ * that file arrived with #4796, which is not on this line.
10
+ */
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+
13
+ vi.mock('@memberjunction/actions', () => ({
14
+ ActionEngineServer: {
15
+ Instance: {
16
+ Config: vi.fn(),
17
+ Actions: [{ ID: 'A1', Name: 'Log Activity' }],
18
+ RunAction: vi.fn(),
19
+ },
20
+ },
21
+ }));
22
+
23
+ import { ActionEngineServer } from '@memberjunction/actions';
24
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
25
+ import type { TaskActionRunParams } from '@memberjunction/task-graph';
26
+ import { TaskGraphActionRunner } from '../services/TaskGraphActionRunner';
27
+
28
+ function baseRunParams(inputPayload: unknown): TaskActionRunParams {
29
+ return {
30
+ TaskID: 'T1',
31
+ ActionID: 'A1',
32
+ InputPayload: inputPayload,
33
+ DependencyOutputs: new Map(),
34
+ Provider: {} as IMetadataProvider,
35
+ ContextUser: {} as UserInfo,
36
+ };
37
+ }
38
+
39
+ describe('TaskGraphActionRunner: outputs must be storable', () => {
40
+ beforeEach(() => {
41
+ // vitest's `restoreMocks: true` clears implementations assigned inside the `vi.mock` factory,
42
+ // so Config's return value is (re)armed here.
43
+ vi.mocked(ActionEngineServer.Instance.Config).mockResolvedValue(undefined);
44
+ vi.mocked(ActionEngineServer.Instance.RunAction).mockReset();
45
+ });
46
+
47
+ it('fails the step, naming the action and the param, when an output param cannot be serialized', async () => {
48
+ const cyclic: Record<string, unknown> = { kind: 'OperatorSubscriber' };
49
+ cyclic.self = cyclic;
50
+ vi.mocked(ActionEngineServer.Instance.RunAction).mockResolvedValue({
51
+ Success: true,
52
+ Message: 'started',
53
+ Params: [{ Name: 'AgentRunID', Type: 'Output', Value: 'RUN-1' }, { Name: 'AgentResult', Type: 'Output', Value: cyclic }],
54
+ LogEntry: { ID: 'LOG-1' },
55
+ } as never);
56
+
57
+ const result = await new TaskGraphActionRunner().RunActionForTask(baseRunParams({}));
58
+
59
+ expect(result.Success).toBe(false);
60
+ expect(result.ErrorMessage).toMatch(/Log Activity.*AgentResult.*cannot be stored/i);
61
+ expect(result.ActionLogID).toBe('LOG-1');
62
+ });
63
+
64
+ it('passes plain outputs through unchanged', async () => {
65
+ vi.mocked(ActionEngineServer.Instance.RunAction).mockResolvedValue({
66
+ Success: true,
67
+ Message: 'ok',
68
+ Params: [{ Name: 'AgentRunID', Type: 'Output', Value: 'RUN-1' }],
69
+ LogEntry: { ID: 'LOG-2' },
70
+ } as never);
71
+
72
+ const result = await new TaskGraphActionRunner().RunActionForTask(baseRunParams({}));
73
+
74
+ expect(result).toMatchObject({ Success: true, Output: { AgentRunID: 'RUN-1' }, ActionLogID: 'LOG-2' });
75
+ });
76
+ });
@@ -0,0 +1,220 @@
1
+ // type-graphql decorators in the resolver graph need the Reflect.metadata polyfill at import time.
2
+ import 'reflect-metadata';
3
+ /**
4
+ * `UpdateMJDataContextItem` goes through `ResolverBase.UpdateRecord`. `MJ: Data Context Items` does
5
+ * not track record changes, so without special handling a client that sends `OldValues___` decides
6
+ * the record's starting state. It could repeat its new SQL as the old value, so the SQL field does
7
+ * not look changed. Or it could leave `Type` and `SQL` out entirely, so they are never loaded and
8
+ * the stored procedure keeps the stored SQL item. The server entity class for the item must still
9
+ * refuse a non-Owner's save of a SQL item on every one of these paths.
10
+ *
11
+ * The resolver base and the entity class are real. The provider hands out real entity objects,
12
+ * loads a fixed stored row, and records each save instead of writing it.
13
+ */
14
+ import { describe, it, expect, vi } from 'vitest';
15
+ import { EntityInfo, Metadata, UserInfo, UserRoleInfo } from '@memberjunction/core';
16
+ import type { BaseEntity, DatabaseProviderBase, IEntityDataProvider, IMetadataProvider } from '@memberjunction/core';
17
+ import { MJDataContextItemEntityServer } from '@memberjunction/core-entities-server';
18
+ import type { PubSubEngine } from 'type-graphql';
19
+ import { ResolverBase } from '../generic/ResolverBase.js';
20
+ import type { UserPayload } from '../types.js';
21
+
22
+ const ENTITY_ID = 'E0000000-0000-4000-8000-0000000000D2';
23
+ const UI_ROLE_ID = 'E1000000-0000-4000-8000-000000000001';
24
+ const DATA_CONTEXT_ID = 'B0000000-0000-4000-8000-000000000001';
25
+ const OTHER_DATA_CONTEXT_ID = 'B0000000-0000-4000-8000-000000000002';
26
+ const ITEM_ID = 'C0000000-0000-4000-8000-000000000001';
27
+ const STORED_SQL = 'SELECT 1 AS One';
28
+ const NEW_SQL = 'SELECT 2 AS Two';
29
+
30
+ interface StoredRow {
31
+ ID: string;
32
+ DataContextID: string;
33
+ Type: string;
34
+ SQL: string | null;
35
+ DataJSON: string | null;
36
+ Description: string | null;
37
+ }
38
+
39
+ /** A SQL item in someone else's data context. */
40
+ const STORED_SQL_ITEM: StoredRow = {
41
+ ID: ITEM_ID, DataContextID: DATA_CONTEXT_ID, Type: 'sql', SQL: STORED_SQL, DataJSON: null, Description: 'Revenue',
42
+ };
43
+
44
+ /** An item of another type, with no SQL. */
45
+ const STORED_ENTITY_ITEM: StoredRow = {
46
+ ID: ITEM_ID, DataContextID: DATA_CONTEXT_ID, Type: 'full_entity', SQL: null, DataJSON: null, Description: 'Notes',
47
+ };
48
+
49
+ /**
50
+ * `MJ: Data Context Items` as the database describes it: record changes and field-level security
51
+ * off, the API open, and the UI role holding every permission with no row-level filter.
52
+ */
53
+ const ITEM_ENTITY = new EntityInfo({
54
+ ID: ENTITY_ID,
55
+ Name: 'MJ: Data Context Items',
56
+ SchemaName: '__mj',
57
+ BaseTable: 'DataContextItem',
58
+ BaseView: 'vwDataContextItems',
59
+ Status: 'Active',
60
+ IncludeInAPI: true,
61
+ AllowCreateAPI: true,
62
+ AllowUpdateAPI: true,
63
+ AllowDeleteAPI: true,
64
+ TrackRecordChanges: false,
65
+ EnableFieldLevelSecurity: false,
66
+ EntityPermissions: [{
67
+ ID: 'P0000000-0000-4000-8000-000000000001', EntityID: ENTITY_ID, RoleID: UI_ROLE_ID,
68
+ CanCreate: true, CanRead: true, CanUpdate: true, CanDelete: true,
69
+ }],
70
+ EntityFields: [
71
+ { Name: 'ID', Type: 'uniqueidentifier', AllowsNull: false, IsPrimaryKey: true },
72
+ { Name: 'DataContextID', Type: 'uniqueidentifier', AllowsNull: false },
73
+ { Name: 'Type', Type: 'nvarchar', AllowsNull: false },
74
+ { Name: 'SQL', Type: 'nvarchar', AllowsNull: true },
75
+ { Name: 'DataJSON', Type: 'nvarchar', AllowsNull: true },
76
+ { Name: 'Description', Type: 'nvarchar', AllowsNull: true },
77
+ ].map((c, i) => ({
78
+ ID: `F0000000-0000-4000-8000-00000000000${i}`,
79
+ EntityID: ENTITY_ID,
80
+ Sequence: i + 1,
81
+ Length: 0,
82
+ Precision: 0,
83
+ Scale: 0,
84
+ AllowUpdateAPI: true,
85
+ ...c,
86
+ })),
87
+ });
88
+
89
+ /** A UI-role user who is not an administrator. */
90
+ const UI_USER = new UserInfo(null, {
91
+ ID: 'A0000000-0000-4000-8000-000000000002',
92
+ Email: 'ui-user@example.com',
93
+ Type: 'User',
94
+ UserRoles: [new UserRoleInfo({ UserID: 'A0000000-0000-4000-8000-000000000002', RoleID: UI_ROLE_ID })],
95
+ });
96
+
97
+ const PAYLOAD: UserPayload = { email: UI_USER.Email, userRecord: UI_USER, sessionId: 'test-session' };
98
+ const PUB_SUB = { publish: async (): Promise<void> => undefined } as unknown as PubSubEngine;
99
+
100
+ /** What one save would have written. */
101
+ interface RecordedSave {
102
+ DataContextID: string;
103
+ SQL: string | null;
104
+ DataJSON: string | null;
105
+ }
106
+
107
+ /** A database provider whose load returns `stored`, and which records each save it receives. */
108
+ function recordingDatabase(stored: StoredRow): { Saves: RecordedSave[]; Provider: DatabaseProviderBase } {
109
+ const saves: RecordedSave[] = [];
110
+ const entityData = {
111
+ async Load(): Promise<Record<string, unknown>> {
112
+ return { ...stored };
113
+ },
114
+ async Save(entity: BaseEntity): Promise<Record<string, unknown>> {
115
+ const item = entity as MJDataContextItemEntityServer;
116
+ saves.push({ DataContextID: item.DataContextID, SQL: item.SQL, DataJSON: item.DataJSON });
117
+ return entity.GetAll();
118
+ },
119
+ } as unknown as IEntityDataProvider;
120
+ const database = {
121
+ async GetEntityObject(_entityName: string, user: UserInfo): Promise<MJDataContextItemEntityServer> {
122
+ const item = new MJDataContextItemEntityServer(ITEM_ENTITY, entityData);
123
+ item.ContextCurrentUser = user;
124
+ return item;
125
+ },
126
+ } as unknown as DatabaseProviderBase;
127
+ return { Saves: saves, Provider: database };
128
+ }
129
+
130
+ /** Builds an `OldValues___` array from a plain object. */
131
+ function oldValues(values: Record<string, string | null>): Array<{ Key: string; Value: string | null }> {
132
+ return Object.entries(values).map(([Key, Value]) => ({ Key, Value }));
133
+ }
134
+
135
+ /** Exposes ResolverBase's generic update path, which the generated `UpdateMJDataContextItem` mutation calls. */
136
+ class UpdateHarness extends ResolverBase {
137
+ /** Runs the update and reports how it ended: 'saved', or the message it was refused with. */
138
+ public async Update(input: Record<string, unknown>, provider: DatabaseProviderBase): Promise<string> {
139
+ try {
140
+ await this.UpdateRecord('MJ: Data Context Items', input, provider, PAYLOAD, PUB_SUB);
141
+ return 'saved';
142
+ } catch (e) {
143
+ return e instanceof Error ? e.message : String(e);
144
+ }
145
+ }
146
+ }
147
+
148
+ const harness = new UpdateHarness();
149
+
150
+ describe('UpdateMJDataContextItem — a non-Owner may not change a SQL item', () => {
151
+ it('refuses new SQL when the update loads the stored row (no OldValues___)', async () => {
152
+ const db = recordingDatabase(STORED_SQL_ITEM);
153
+
154
+ const outcome = await harness.Update({ ...STORED_SQL_ITEM, SQL: NEW_SQL }, db.Provider);
155
+
156
+ expect(db.Saves).toEqual([]);
157
+ expect(outcome).toMatch(/Owner-type/);
158
+ });
159
+
160
+ it('refuses new SQL when OldValues___ repeats the new SQL, so the field does not look changed', async () => {
161
+ const db = recordingDatabase(STORED_SQL_ITEM);
162
+ const input = {
163
+ ...STORED_SQL_ITEM,
164
+ SQL: NEW_SQL,
165
+ Description: 'Renamed',
166
+ OldValues___: oldValues({ ID: ITEM_ID, DataContextID: DATA_CONTEXT_ID, Type: 'sql', SQL: NEW_SQL, Description: 'Revenue' }),
167
+ };
168
+
169
+ const outcome = await harness.Update(input, db.Provider);
170
+
171
+ expect(db.Saves).toEqual([]);
172
+ expect(outcome).toMatch(/Owner-type/);
173
+ });
174
+
175
+ it('refuses moving a SQL item to another data context when Type and SQL are left out of the input and OldValues___', async () => {
176
+ const db = recordingDatabase(STORED_SQL_ITEM);
177
+ const input = {
178
+ ID: ITEM_ID,
179
+ DataContextID: OTHER_DATA_CONTEXT_ID,
180
+ OldValues___: oldValues({ ID: ITEM_ID, DataContextID: DATA_CONTEXT_ID }),
181
+ };
182
+
183
+ const outcome = await harness.Update(input, db.Provider);
184
+
185
+ expect(db.Saves).toEqual([]);
186
+ expect(outcome).toMatch(/Owner-type/);
187
+ });
188
+
189
+ it('refuses changing the cached DataJSON of a SQL item when Type and SQL are left out of the input and OldValues___', async () => {
190
+ const db = recordingDatabase(STORED_SQL_ITEM);
191
+ const input = {
192
+ ID: ITEM_ID,
193
+ DataJSON: '[{"One":2}]',
194
+ OldValues___: oldValues({ ID: ITEM_ID, DataJSON: null }),
195
+ };
196
+
197
+ const outcome = await harness.Update(input, db.Provider);
198
+
199
+ expect(db.Saves).toEqual([]);
200
+ expect(outcome).toMatch(/Owner-type/);
201
+ });
202
+
203
+ it('still saves a non-Owner\'s change to an item of another type that sends OldValues___', async () => {
204
+ // The success path maps the saved row back through the process-wide metadata.
205
+ vi.spyOn(Metadata, 'Provider', 'get').mockReturnValue(
206
+ { Entities: [ITEM_ENTITY], EntityByName: () => ITEM_ENTITY } as unknown as IMetadataProvider,
207
+ );
208
+ const db = recordingDatabase(STORED_ENTITY_ITEM);
209
+ const input = {
210
+ ID: ITEM_ID,
211
+ Description: 'Renamed',
212
+ OldValues___: oldValues({ ID: ITEM_ID, Description: 'Notes' }),
213
+ };
214
+
215
+ const outcome = await harness.Update(input, db.Provider);
216
+
217
+ expect(outcome).toBe('saved');
218
+ expect(db.Saves).toEqual([{ DataContextID: DATA_CONTEXT_ID, SQL: null, DataJSON: null }]);
219
+ });
220
+ });
@@ -0,0 +1,106 @@
1
+ import 'reflect-metadata';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { DatabaseProviderBase, UserInfo, type IMetadataProvider } from '@memberjunction/core';
4
+ import sql from 'mssql';
5
+ import { VectorizeEntityResolver } from '../resolvers/VectorizeEntityResolver.js';
6
+ import type { AppContext, ProviderInfo } from '../types.js';
7
+
8
+ const mockSyncerConstructedWith: (IMetadataProvider | undefined)[] = [];
9
+
10
+ vi.mock('@memberjunction/ai-vector-sync', () => {
11
+ return {
12
+ EntityVectorSyncer: class {
13
+ constructor(provider?: IMetadataProvider) {
14
+ mockSyncerConstructedWith.push(provider);
15
+ }
16
+ Config() {
17
+ return Promise.resolve(undefined);
18
+ }
19
+ VectorizeEntity() {
20
+ return Promise.resolve({ success: true, status: 'Completed' });
21
+ }
22
+ },
23
+ };
24
+ });
25
+
26
+ vi.mock('../generic/PubSubManager.js', () => ({
27
+ PubSubManager: {
28
+ Instance: {
29
+ Publish: vi.fn(),
30
+ },
31
+ },
32
+ }));
33
+
34
+ function createAppContext(providers?: ProviderInfo[]): AppContext {
35
+ return {
36
+ dataSource: Object.create(sql.ConnectionPool.prototype) as sql.ConnectionPool,
37
+ dataSources: [],
38
+ providers: providers ?? [],
39
+ userPayload: {
40
+ email: 'test@example.com',
41
+ sessionId: 'test-session',
42
+ userRecord: new UserInfo(),
43
+ },
44
+ };
45
+ }
46
+
47
+ describe('VectorizeEntityResolver — provider propagation', () => {
48
+ beforeEach(() => {
49
+ mockSyncerConstructedWith.length = 0;
50
+ });
51
+
52
+ it('passes request-scoped Read-Write provider from AppContext to EntityVectorSyncer', async () => {
53
+ const mockProvider = Object.create(DatabaseProviderBase.prototype) as DatabaseProviderBase;
54
+ const providerInfo: ProviderInfo = {
55
+ provider: mockProvider,
56
+ type: 'Read-Write',
57
+ };
58
+
59
+ const resolver = new VectorizeEntityResolver();
60
+ const result = await resolver.VectorizeEntity(
61
+ 'doc-1',
62
+ 'entity-1',
63
+ 10,
64
+ createAppContext([providerInfo])
65
+ );
66
+
67
+ expect(result.Success).toBe(true);
68
+ expect(result.Status).toBe('Started');
69
+ expect(mockSyncerConstructedWith.length).toBe(1);
70
+ expect(mockSyncerConstructedWith[0]).toBe(mockProvider);
71
+ });
72
+
73
+ it('falls back to Read-Only provider when no Read-Write provider exists', async () => {
74
+ const readOnlyProvider = Object.create(DatabaseProviderBase.prototype) as DatabaseProviderBase;
75
+ const providerInfo: ProviderInfo = {
76
+ provider: readOnlyProvider,
77
+ type: 'Read-Only',
78
+ };
79
+
80
+ const resolver = new VectorizeEntityResolver();
81
+ const result = await resolver.VectorizeEntity(
82
+ 'doc-2',
83
+ 'entity-2',
84
+ 10,
85
+ createAppContext([providerInfo])
86
+ );
87
+
88
+ expect(result.Success).toBe(true);
89
+ expect(mockSyncerConstructedWith.length).toBe(1);
90
+ expect(mockSyncerConstructedWith[0]).toBe(readOnlyProvider);
91
+ });
92
+
93
+ it('passes undefined provider to EntityVectorSyncer when no providers in AppContext', async () => {
94
+ const resolver = new VectorizeEntityResolver();
95
+ const result = await resolver.VectorizeEntity(
96
+ 'doc-3',
97
+ 'entity-3',
98
+ 10,
99
+ createAppContext([])
100
+ );
101
+
102
+ expect(result.Success).toBe(true);
103
+ expect(mockSyncerConstructedWith.length).toBe(1);
104
+ expect(mockSyncerConstructedWith[0]).toBeUndefined();
105
+ });
106
+ });